diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css index f1d8c73..b260630 100644 --- a/app/assets/stylesheets/application.tailwind.css +++ b/app/assets/stylesheets/application.tailwind.css @@ -1 +1,93 @@ @import "tailwindcss"; + +@layer components { + [data-stream-view-mode="table"] .streams-table { + display: table !important; + } + + [data-stream-view-mode="table"] .streams-thead { + display: table-header-group !important; + } + + [data-stream-view-mode="table"] .streams-container { + overflow-x: auto !important; + } + + [data-stream-view-mode="table"] .streams-card-sort { + display: none !important; + } + + [data-stream-view-mode="table"] .streams-tbody { + display: table-row-group !important; + } + + [data-stream-view-mode="table"] .streams-tbody > :not([hidden]) ~ :not([hidden]) { + margin-top: 0 !important; + } + + [data-stream-view-mode="table"] .streams-row { + display: table-row !important; + border: 0 !important; + border-radius: 0 !important; + box-shadow: none !important; + overflow: visible !important; + background-color: transparent !important; + } + + [data-stream-view-mode="table"] .streams-row:hover { + background-color: rgb(249 250 251) !important; + } + + [data-stream-view-mode="table"] .streams-cell { + display: table-cell !important; + } + + [data-stream-view-mode="table"] .streams-cell::before { + display: none !important; + } + + [data-stream-view-mode="card"] .streams-table { + display: block !important; + } + + [data-stream-view-mode="card"] .streams-thead { + display: none !important; + } + + [data-stream-view-mode="card"] .streams-container { + overflow: visible !important; + } + + [data-stream-view-mode="card"] .streams-card-sort { + display: flex !important; + } + + [data-stream-view-mode="card"] .streams-tbody { + display: block !important; + } + + [data-stream-view-mode="card"] .streams-tbody > :not([hidden]) ~ :not([hidden]) { + margin-top: 1rem !important; + } + + [data-stream-view-mode="card"] .streams-row { + display: block !important; + border: 1px solid rgb(229 231 235) !important; + border-radius: 0.5rem !important; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05) !important; + overflow: hidden !important; + background-color: white !important; + } + + [data-stream-view-mode="card"] .streams-row:hover { + background-color: rgb(249 250 251) !important; + } + + [data-stream-view-mode="card"] .streams-cell { + display: block !important; + } + + [data-stream-view-mode="card"] .streams-cell::before { + display: block !important; + } +} diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index 4696740..eab401e 100644 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -6,6 +6,7 @@ import SearchController from './search_controller' import MobileMenuController from './mobile_menu_controller' import CollaborativeSpreadsheetController from './collaborative_spreadsheet_controller' import StreamTablePreferencesController from './stream_table_preferences_controller' +import StreamViewController from './stream_view_controller' import ToastController from './toast_controller' // Register controllers @@ -14,4 +15,5 @@ application.register('search', SearchController) application.register('mobile-menu', MobileMenuController) application.register('collaborative-spreadsheet', CollaborativeSpreadsheetController) application.register('stream-table-preferences', StreamTablePreferencesController) +application.register('stream-view', StreamViewController) application.register('toast', ToastController) diff --git a/app/javascript/controllers/stream_view_controller.js b/app/javascript/controllers/stream_view_controller.js new file mode 100644 index 0000000..52dca85 --- /dev/null +++ b/app/javascript/controllers/stream_view_controller.js @@ -0,0 +1,115 @@ +import { Controller } from '@hotwired/stimulus' + +export default class extends Controller { + static targets = ['toggle'] + static values = { + storageKey: { type: String, default: 'streamsViewMode' } + } + + connect () { + this.boundHandleMediaChange = this.handleMediaChange.bind(this) + this.mediaQuery = window.matchMedia('(min-width: 768px)') + + this.applySavedOrDefault() + this.addMediaListener() + } + + disconnect () { + this.removeMediaListener() + } + + select (event) { + const mode = event.currentTarget.dataset.mode + if (!this.isValidMode(mode)) return + + this.storeMode(mode) + this.applyMode(mode) + } + + handleMediaChange () { + this.applySavedOrDefault() + } + + defaultMode () { + return this.mediaQuery.matches ? 'table' : 'card' + } + + applySavedOrDefault () { + const savedMode = this.loadSavedMode() + if (savedMode) { + this.applyMode(savedMode) + return + } + + this.applyMode(this.defaultMode()) + } + + applyMode (mode) { + if (!this.isValidMode(mode)) return + + this.element.dataset.streamViewMode = mode + this.updateToggleStyles(mode) + } + + updateToggleStyles (mode) { + if (!this.hasToggleTarget) return + + this.toggleTargets.forEach(toggle => { + const active = toggle.dataset.mode === mode + toggle.setAttribute('aria-pressed', active ? 'true' : 'false') + toggle.classList.toggle('bg-indigo-600', active) + toggle.classList.toggle('text-white', active) + toggle.classList.toggle('hover:bg-indigo-700', active) + toggle.classList.toggle('bg-white', !active) + toggle.classList.toggle('text-gray-600', !active) + toggle.classList.toggle('hover:text-gray-900', !active) + toggle.classList.toggle('hover:bg-gray-50', !active) + }) + } + + isValidMode (mode) { + return mode === 'table' || mode === 'card' + } + + loadSavedMode () { + try { + const mode = window.localStorage.getItem(this.storageKey()) + return this.isValidMode(mode) ? mode : null + } catch { + return null + } + } + + storeMode (mode) { + try { + window.localStorage.setItem(this.storageKey(), mode) + } catch { + // Ignore storage errors (private mode, disabled storage, etc.) + } + } + + storageKey () { + const suffix = this.mediaQuery && this.mediaQuery.matches ? 'desktop' : 'mobile' + return `${this.storageKeyValue}-${suffix}` + } + + addMediaListener () { + if (!this.mediaQuery) return + + if (this.mediaQuery.addEventListener) { + this.mediaQuery.addEventListener('change', this.boundHandleMediaChange) + } else if (this.mediaQuery.addListener) { + this.mediaQuery.addListener(this.boundHandleMediaChange) + } + } + + removeMediaListener () { + if (!this.mediaQuery) return + + if (this.mediaQuery.removeEventListener) { + this.mediaQuery.removeEventListener('change', this.boundHandleMediaChange) + } else if (this.mediaQuery.removeListener) { + this.mediaQuery.removeListener(this.boundHandleMediaChange) + } + } +} diff --git a/app/views/admin/streams/_spreadsheet.html.erb b/app/views/admin/streams/_spreadsheet.html.erb index 9c94e3a..e242dfa 100644 --- a/app/views/admin/streams/_spreadsheet.html.erb +++ b/app/views/admin/streams/_spreadsheet.html.erb @@ -1,50 +1,83 @@ -
-
-
- - Columns - -
-
Show columns
-
- <% stream_table_columns.each do |column| %> -
- - -
- <% end %> +
+
+
+ + Columns + +
+
Show columns
+
+ <% stream_table_columns.each do |column| %> +
+ + +
+ <% end %> +
+
+ Drag columns in the menu to reorder. +
+ +
+ View +
+ +
-
- Drag columns in the menu to reorder. +
@@ -61,11 +94,40 @@
+ <% sort_filter_params = params.except(:page, :sort, :direction) + .permit(:status, :platform, :kind, :orientation, :user_id, :search, :is_pinned, :is_archived) + .to_h %> + <% sort_options = stream_table_columns.select { |column| column[:sortable] } + .map { |column| [column[:label], column[:key]] } %> + + +
+
Sort
+ <%= form_with url: admin_streams_path, + method: :get, + data: { controller: "search", search_target: "form", turbo_frame: "streams_list", turbo_action: "advance" }, + class: "flex flex-1 flex-wrap items-center gap-2" do |form| %> + <% sort_filter_params.each do |key, value| %> + <%= form.hidden_field key, value: value %> + <% end %> + <%= form.select :sort, + options_for_select(sort_options, @sort_column), + { include_blank: "Sort by" }, + class: "flex-1 min-w-[160px] rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm", + data: { action: "change->search#submit" } %> + <%= form.select :direction, + options_for_select([["Ascending", "asc"], ["Descending", "desc"]], @sort_direction), + {}, + class: "min-w-[140px] rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm", + data: { action: "change->search#submit" } %> + <% end %> +
+ -
- - +
+ - + <%= render partial: "admin/streams/spreadsheet_row", collection: @streams, as: :stream %>
diff --git a/app/views/admin/streams/_spreadsheet_row.html.erb b/app/views/admin/streams/_spreadsheet_row.html.erb index 0f62f3d..fc1c422 100644 --- a/app/views/admin/streams/_spreadsheet_row.html.erb +++ b/app/views/admin/streams/_spreadsheet_row.html.erb @@ -1,6 +1,8 @@ - + - " + " + data-label="Streamer" data-column="streamer" data-stream-table-preferences-target="column">
@@ -37,7 +39,8 @@ - " + " + data-label="Title" data-column="title" data-stream-table-preferences-target="column">
- " + " + data-label="Source" data-column="source" data-stream-table-preferences-target="column">
- " + " + data-label="Link" data-column="link" data-stream-table-preferences-target="column">
@@ -74,7 +79,7 @@ data-stream-id="<%= stream.id %>" data-field="link" data-original-value="<%= stream.link %>" - class="text-sm text-gray-900 cursor-pointer hover:bg-gray-100 px-2 py-1 rounded transition-colors max-w-xs truncate min-h-[1.5rem] flex-1 min-w-0"> + class="text-sm text-gray-900 cursor-pointer hover:bg-gray-100 px-2 py-1 rounded transition-colors md:max-w-xs md:truncate min-h-[1.5rem] flex-1 min-w-0 break-words"> <%= stream.link %>
<% if stream.link.present? %> @@ -94,7 +99,8 @@ - " + " + data-label="Platform" data-column="platform" data-stream-table-preferences-target="column">
- " + " + data-label="Status" data-column="status" data-stream-table-preferences-target="column">
- " + " + data-label="City" data-column="city" data-stream-table-preferences-target="column">
- " + " + data-label="State" data-column="state" data-stream-table-preferences-target="column">
- " + " + data-label="Kind" data-column="kind" data-stream-table-preferences-target="column">
- " + " + data-label="Orientation" data-column="orientation" data-stream-table-preferences-target="column">
- " + " + data-label="Started At" data-column="started_at" data-stream-table-preferences-target="column">
@@ -200,7 +212,8 @@ - " + " + data-label="Last Checked" data-column="last_checked_at" data-stream-table-preferences-target="column">
@@ -209,7 +222,8 @@ - " + " + data-label="Last Live" data-column="last_live_at" data-stream-table-preferences-target="column">
@@ -218,7 +232,8 @@ - " + " + data-label="Actions" data-column="actions" data-stream-table-preferences-target="column">