Files
redux-scraper/app/javascript/bundles/Main/components/PostFiles.tsx
Dylan Knutson 73f6f77596 Add comprehensive Bluesky tests to posts_helper_spec
- Add extensive test coverage for Bluesky user profile URL matching
- Test handle-based and DID-based profile URLs with various formats
- Add edge cases and error condition tests for malformed URLs
- Test user avatar icon path and model path generation
- Verify fallback behavior for users without display names
- Test priority logic for handle vs DID lookup
- Add tests for special characters and very long handles
- All 82 tests now pass successfully
2025-08-17 00:10:31 +00:00

124 lines
3.3 KiB
TypeScript

import * as React from 'react';
import { useState, useEffect, useCallback } from 'react';
import { FileCarousel } from './FileCarousel';
import { DisplayedFile } from './DisplayedFile';
import { FileDetailsProps } from './FileDetails';
export type PostFileState =
| 'pending'
| 'ok'
| 'file_error'
| 'retryable_error'
| 'terminal_error'
| 'removed';
export interface FileData {
id: number;
fileState: PostFileState;
thumbnailPath?: { type: 'icon' | 'url'; value: string };
hasContent: boolean;
index: number;
contentHtml?: string;
fileDetails?: FileDetailsProps;
}
interface PostFilesProps {
files: FileData[];
initialSelectedIndex?: number;
}
export const PostFiles: React.FC<PostFilesProps> = ({
files,
initialSelectedIndex,
}) => {
if (initialSelectedIndex == null) {
initialSelectedIndex = files.findIndex((file) => file.fileState === 'ok');
if (initialSelectedIndex === -1) {
initialSelectedIndex = 0;
}
}
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">
{files.length == 0 && (
<div className="flex grow justify-center text-slate-500">
<div className="flex items-center gap-2">
<i className="fa-solid fa-file-circle-exclamation"></i>
No files
</div>
</div>
)}
{files.length > 1 && (
<FileCarousel
files={files}
totalFiles={files.length}
selectedIndex={selectedIndex}
onFileSelect={handleFileSelect}
/>
)}
{selectedFile && <DisplayedFile file={selectedFile} />}
</section>
);
};
export default PostFiles;