From f2f8a9c34a7415adfd2bba46ad24c9b24dd1c588 Mon Sep 17 00:00:00 2001 From: Dylan Knutson Date: Sat, 9 Aug 2025 00:59:26 +0000 Subject: [PATCH] Refactor PostFiles component to use URL parameters and simplify implementation - Change from hash fragments (#file=2) to URL parameters (?idx=2) for server-side prerendering support - Simplify React component by removing complex client-side hydration logic - Remove unnecessary props: totalFiles, hasMultipleFiles (derive from files.length) - Remove redundant useCallback and popstate handlers - Update Rails helper to read URL parameter and pass correct initialSelectedIndex - Maintain all functionality: carousel, keyboard navigation, URL state management --- app/helpers/domain/posts_helper.rb | 23 ++++--- .../bundles/Main/components/PostFiles.tsx | 61 ++++++++++++++++--- .../posts/default/_section_file.html.erb | 7 ++- 3 files changed, 73 insertions(+), 18 deletions(-) diff --git a/app/helpers/domain/posts_helper.rb b/app/helpers/domain/posts_helper.rb index fe627130..c318faa8 100644 --- a/app/helpers/domain/posts_helper.rb +++ b/app/helpers/domain/posts_helper.rb @@ -195,11 +195,12 @@ module Domain::PostsHelper end sig do - params(ok_files: T::Array[Domain::PostFile]).returns( - T::Hash[Symbol, T.untyped], - ) + params( + ok_files: T::Array[Domain::PostFile], + initial_file_index: T.nilable(Integer), + ).returns(T::Hash[Symbol, T.untyped]) end - def props_for_post_files(ok_files) + def props_for_post_files(ok_files, initial_file_index: nil) files_data = ok_files.map.with_index do |file, index| thumbnail_path = nil @@ -266,12 +267,14 @@ module Domain::PostsHelper } end - { - files: files_data, - totalFiles: ok_files.count, - initialSelectedIndex: 0, - hasMultipleFiles: ok_files.count > 1, - } + # Validate initial_file_index + validated_initial_index = 0 + if initial_file_index && initial_file_index >= 0 && + initial_file_index < ok_files.count + validated_initial_index = initial_file_index + end + + { files: files_data, initialSelectedIndex: validated_initial_index } end sig { params(url: String).returns(T.nilable(String)) } diff --git a/app/javascript/bundles/Main/components/PostFiles.tsx b/app/javascript/bundles/Main/components/PostFiles.tsx index 82a20643..9999771b 100644 --- a/app/javascript/bundles/Main/components/PostFiles.tsx +++ b/app/javascript/bundles/Main/components/PostFiles.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useState } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { FileCarousel } from './FileCarousel'; import { DisplayedFile } from './DisplayedFile'; @@ -14,31 +14,78 @@ export interface FileData { interface PostFilesProps { files: FileData[]; - totalFiles: number; initialSelectedIndex?: number; - hasMultipleFiles: boolean; } export const PostFiles: React.FC = ({ files, - totalFiles, initialSelectedIndex = 0, - hasMultipleFiles, }) => { const [selectedIndex, setSelectedIndex] = useState(initialSelectedIndex); + // Update URL parameter when selected file changes + const updateUrlWithFileIndex = (index: number) => { + if (typeof window === 'undefined' || files.length <= 1) return; + + const url = new URL(window.location.href); + url.searchParams.set('idx', index.toString()); + window.history.replaceState({}, '', url.toString()); + }; + const handleFileSelect = (fileId: number, index: number) => { setSelectedIndex(index); + updateUrlWithFileIndex(index); }; + const navigateToNextFile = () => { + if (files.length > 1) { + const nextIndex = (selectedIndex + 1) % files.length; + handleFileSelect(files[nextIndex].id, nextIndex); + } + }; + + const navigateToPreviousFile = () => { + if (files.length > 1) { + const prevIndex = (selectedIndex - 1 + files.length) % files.length; + handleFileSelect(files[prevIndex].id, prevIndex); + } + }; + + // Add keyboard navigation + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + // Only handle arrow keys if we have multiple files + if (files.length <= 1) return; + + switch (event.key) { + case 'ArrowLeft': + event.preventDefault(); + navigateToPreviousFile(); + break; + case 'ArrowRight': + event.preventDefault(); + navigateToNextFile(); + break; + } + }; + + // Add event listener to document + document.addEventListener('keydown', handleKeyDown); + + // Cleanup event listener on unmount + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [selectedIndex, files.length]); + const selectedFile = files[selectedIndex]; return (
- {hasMultipleFiles && ( + {files.length > 1 && ( diff --git a/app/views/domain/posts/default/_section_file.html.erb b/app/views/domain/posts/default/_section_file.html.erb index c89a3182..b725f065 100644 --- a/app/views/domain/posts/default/_section_file.html.erb +++ b/app/views/domain/posts/default/_section_file.html.erb @@ -3,11 +3,16 @@ <% current_file = ok_files.first || post.primary_file_for_view %> <% if ok_files.any? && current_file&.log_entry&.status_code == 200 %> + <% + # Extract file index from URL parameter + idx_param = params[:idx] + initial_file_index = idx_param.present? ? idx_param.to_i : nil + %> <%= react_component( "PostFiles", { prerender: true, - props: props_for_post_files(ok_files), + props: props_for_post_files(ok_files, initial_file_index: initial_file_index), html_options: { id: "post-files-component" }