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
This commit is contained in:
Dylan Knutson
2025-08-09 00:59:26 +00:00
parent 36ceae80fe
commit f2f8a9c34a
3 changed files with 73 additions and 18 deletions

View File

@@ -195,11 +195,12 @@ module Domain::PostsHelper
end end
sig do sig do
params(ok_files: T::Array[Domain::PostFile]).returns( params(
T::Hash[Symbol, T.untyped], ok_files: T::Array[Domain::PostFile],
) initial_file_index: T.nilable(Integer),
).returns(T::Hash[Symbol, T.untyped])
end end
def props_for_post_files(ok_files) def props_for_post_files(ok_files, initial_file_index: nil)
files_data = files_data =
ok_files.map.with_index do |file, index| ok_files.map.with_index do |file, index|
thumbnail_path = nil thumbnail_path = nil
@@ -266,12 +267,14 @@ module Domain::PostsHelper
} }
end end
{ # Validate initial_file_index
files: files_data, validated_initial_index = 0
totalFiles: ok_files.count, if initial_file_index && initial_file_index >= 0 &&
initialSelectedIndex: 0, initial_file_index < ok_files.count
hasMultipleFiles: ok_files.count > 1, validated_initial_index = initial_file_index
} end
{ files: files_data, initialSelectedIndex: validated_initial_index }
end end
sig { params(url: String).returns(T.nilable(String)) } sig { params(url: String).returns(T.nilable(String)) }

View File

@@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { useState } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { FileCarousel } from './FileCarousel'; import { FileCarousel } from './FileCarousel';
import { DisplayedFile } from './DisplayedFile'; import { DisplayedFile } from './DisplayedFile';
@@ -14,31 +14,78 @@ export interface FileData {
interface PostFilesProps { interface PostFilesProps {
files: FileData[]; files: FileData[];
totalFiles: number;
initialSelectedIndex?: number; initialSelectedIndex?: number;
hasMultipleFiles: boolean;
} }
export const PostFiles: React.FC<PostFilesProps> = ({ export const PostFiles: React.FC<PostFilesProps> = ({
files, files,
totalFiles,
initialSelectedIndex = 0, initialSelectedIndex = 0,
hasMultipleFiles,
}) => { }) => {
const [selectedIndex, setSelectedIndex] = useState(initialSelectedIndex); 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) => { const handleFileSelect = (fileId: number, index: number) => {
setSelectedIndex(index); 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]; const selectedFile = files[selectedIndex];
return ( return (
<section id="file-display-section"> <section id="file-display-section">
{hasMultipleFiles && ( {files.length > 1 && (
<FileCarousel <FileCarousel
files={files} files={files}
totalFiles={totalFiles} totalFiles={files.length}
selectedIndex={selectedIndex} selectedIndex={selectedIndex}
onFileSelect={handleFileSelect} onFileSelect={handleFileSelect}
/> />

View File

@@ -3,11 +3,16 @@
<% current_file = ok_files.first || post.primary_file_for_view %> <% current_file = ok_files.first || post.primary_file_for_view %>
<% if ok_files.any? && current_file&.log_entry&.status_code == 200 %> <% if ok_files.any? && current_file&.log_entry&.status_code == 200 %>
<!-- React PostFiles Component handles everything --> <!-- React PostFiles Component handles everything -->
<%
# Extract file index from URL parameter
idx_param = params[:idx]
initial_file_index = idx_param.present? ? idx_param.to_i : nil
%>
<%= react_component( <%= react_component(
"PostFiles", "PostFiles",
{ {
prerender: true, prerender: true,
props: props_for_post_files(ok_files), props: props_for_post_files(ok_files, initial_file_index: initial_file_index),
html_options: { html_options: {
id: "post-files-component" id: "post-files-component"
} }