Tanya de2144be2a feat: Add new scripts and update project structure for database management and user authentication
This commit introduces several new scripts for managing database operations, including user creation, permission grants, and data migrations. It also adds new documentation files to guide users through the setup and configuration processes. Additionally, the project structure is updated to enhance organization and maintainability, ensuring a smoother development experience for contributors. These changes support the ongoing transition to a web-based architecture and improve overall project functionality.
2026-01-06 13:53:24 -05:00

173 lines
4.9 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import Image from 'next/image';
import { Photo, Person } from '@prisma/client';
import { ChevronLeft, ChevronRight, X } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { isUrl, getImageSrc } from '@/lib/photo-utils';
interface PhotoWithDetails extends Photo {
faces?: Array<{
person: Person | null;
}>;
photoTags?: Array<{
tag: {
tagName: string;
};
}>;
}
interface PhotoViewerProps {
photo: PhotoWithDetails;
previousId: number | null;
nextId: number | null;
}
export function PhotoViewer({ photo, previousId, nextId }: PhotoViewerProps) {
const router = useRouter();
const [loading, setLoading] = useState(false);
const { data: session } = useSession();
const isLoggedIn = Boolean(session);
// Keyboard navigation
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'ArrowLeft' && previousId) {
navigateToPhoto(previousId);
} else if (e.key === 'ArrowRight' && nextId) {
navigateToPhoto(nextId);
} else if (e.key === 'Escape') {
router.back();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [previousId, nextId, router]);
const navigateToPhoto = (photoId: number) => {
setLoading(true);
router.push(`/photo/${photoId}`);
};
const handlePrevious = () => {
if (previousId) {
navigateToPhoto(previousId);
}
};
const handleNext = () => {
if (nextId) {
navigateToPhoto(nextId);
}
};
const handleClose = () => {
// Use router.back() to return to the previous page without reloading
// This preserves filters, pagination, and scroll position
router.back();
};
const peopleNames = photo.faces
?.map((face) => face.person)
.filter((person): person is Person => person !== null)
.map((person) => `${person.firstName} ${person.lastName}`.trim()) || [];
const tags = photo.photoTags?.map((pt) => pt.tag.tagName) || [];
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black">
{/* Close Button */}
<Button
variant="ghost"
size="icon"
className="absolute top-4 right-4 z-10 text-white hover:bg-white/20"
onClick={handleClose}
aria-label="Close"
>
<X className="h-6 w-6" />
</Button>
{/* Previous Button */}
{previousId && (
<Button
variant="ghost"
size="icon"
className="absolute left-4 z-10 text-white hover:bg-white/20 disabled:opacity-30"
onClick={handlePrevious}
disabled={loading}
aria-label="Previous photo"
>
<ChevronLeft className="h-8 w-8" />
</Button>
)}
{/* Next Button */}
{nextId && (
<Button
variant="ghost"
size="icon"
className="absolute right-4 z-10 text-white hover:bg-white/20 disabled:opacity-30"
onClick={handleNext}
disabled={loading}
aria-label="Next photo"
>
<ChevronRight className="h-8 w-8" />
</Button>
)}
{/* Photo Container */}
<div className="relative h-full w-full flex items-center justify-center p-4">
{loading ? (
<div className="text-white">Loading...</div>
) : (
<div className="relative h-full w-full max-h-[90vh] max-w-full">
<Image
src={getImageSrc(photo, { watermark: !isLoggedIn })}
alt={photo.filename}
fill
className="object-contain"
priority
unoptimized={!isUrl(photo.path)}
sizes="100vw"
/>
</div>
)}
</div>
{/* Photo Info Overlay */}
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 to-transparent p-6 text-white">
<div className="container mx-auto">
<h2 className="text-xl font-semibold mb-2">{photo.filename}</h2>
{photo.dateTaken && (
<p className="text-sm text-gray-300 mb-2">
{new Date(photo.dateTaken).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</p>
)}
{peopleNames.length > 0 && (
<p className="text-sm text-gray-300 mb-1">
<span className="font-medium">People: </span>
{peopleNames.join(', ')}
</p>
)}
{tags.length > 0 && (
<p className="text-sm text-gray-300">
<span className="font-medium">Tags: </span>
{tags.join(', ')}
</p>
)}
</div>
</div>
</div>
);
}