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
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)) }

View File

@@ -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<PostFilesProps> = ({
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 (
<section id="file-display-section">
{hasMultipleFiles && (
{files.length > 1 && (
<FileCarousel
files={files}
totalFiles={totalFiles}
totalFiles={files.length}
selectedIndex={selectedIndex}
onFileSelect={handleFileSelect}
/>

View File

@@ -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 %>
<!-- 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(
"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"
}