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:
@@ -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)) }
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user