feat: add album and media management features, including album creation, media upload, and routing

This commit is contained in:
2025-11-16 01:19:17 +01:00
parent 252491bd2f
commit 43157cef4e
18 changed files with 814 additions and 8 deletions

View File

@@ -9,6 +9,8 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import type { QueryClient } from "@tanstack/react-query";
import { useAuthStorage } from "@/hooks/use-auth-storage";
import { useEffect } from "react";
import { Sidebar } from "@/components/layout/sidebar";
import { UploadDialog } from "@/components/media/upload-dialog";
export const Route = createRootRouteWithContext<{
queryClient: QueryClient;
@@ -29,7 +31,7 @@ function RootComponent() {
const navigate = useNavigate();
useEffect(() => {
if (!token) {
if (!token && location.pathname !== "/login") {
navigate({
to: "/login",
replace: true,
@@ -37,12 +39,29 @@ function RootComponent() {
}
}, [token, navigate]);
if (!token) {
return (
<>
<Outlet />
<ReactQueryDevtools buttonPosition="top-right" />
<TanStackRouterDevtools position="bottom-right" />
</>
);
}
return (
<>
<div className="flex h-screen bg-gray-100">
{/* <Sidebar /> */}
<div className="flex-1 flex flex-col">
{/* <Header /> */}
<Sidebar /> {/* */}
<div className="flex-1 flex flex-col h-screen">
<header className="bg-white shadow-sm border-b border-gray-200">
<div className="mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-end h-16 items-center">
<UploadDialog />
</div>
</div>
</header>
<main className="flex-1 p-6 overflow-y-auto">
<Outlet />
</main>

View File

@@ -0,0 +1,19 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/albums/$albumId")({
component: AlbumDetailPage,
});
function AlbumDetailPage() {
const { albumId } = Route.useParams();
return (
<div>
<h1 className="text-3xl font-bold">Album: {albumId}</h1>
<p className="mt-4">
This page will show the details and photos for a single album.
</p>
{/* TODO: Fetch album details and display media grid */}
</div>
);
}

View File

@@ -0,0 +1,34 @@
import { createFileRoute } from "@tanstack/react-router";
import { useGetAlbums } from "@/features/albums/use-albums";
import { AlbumCard } from "@/components/albums/album-card";
import { CreateAlbumDialog } from "@/components/albums/create-album-dialog";
import { Separator } from "@/components/ui/separator";
export const Route = createFileRoute("/albums/")({
component: AlbumsPage,
});
function AlbumsPage() {
const { data: albums, isLoading, error } = useGetAlbums();
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold">Albums</h1>
<CreateAlbumDialog />
</div>
<Separator />
{isLoading && <p>Loading albums...</p>}
{error && <p>Error loading albums: {error.message}</p>}
{albums && (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{albums.map((album) => (
<AlbumCard key={album.id} album={album} />
))}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,72 @@
import { useGetMediaList } from "@/features/media/use-media";
import { createFileRoute } from "@tanstack/react-router";
import { Button } from "@/components/ui/button";
import { AuthenticatedImage } from "@/components/media/authenticated-image";
import type { Media } from "@/domain/types";
import { useState } from "react";
import { MediaViewer } from "@/components/media/media-viewer";
export const Route = createFileRoute("/media/")({
component: MediaPage,
});
function MediaPage() {
const {
data,
isLoading,
error,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useGetMediaList();
const [selectedMedia, setSelectedMedia] = useState<Media | null>(null);
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold">All Photos</h1>
</div>
{isLoading && <p>Loading photos...</p>}
{error && <p>Error loading photos: {error.message}</p>}
{data && (
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 gap-2">
{data.pages.map((page) =>
page.data.map((media) => (
<div
key={media.id}
className="aspect-square bg-gray-200 rounded-md overflow-hidden cursor-pointer hover:opacity-80 transition-opacity"
onClick={() => setSelectedMedia(media)}
>
<AuthenticatedImage
src={media.thumbnail_url ?? media.file_url}
alt={media.original_filename}
className="w-full h-full object-cover"
/>
</div>
))
)}
</div>
)}
{hasNextPage && (
<div className="flex justify-center mt-6">
<Button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? "Loading more..." : "Load More"}
</Button>
</div>
)}
<MediaViewer
media={selectedMedia}
onOpenChange={(open) => {
if (!open) {
setSelectedMedia(null);
}
}}
/>
</div>
);
}

View File

@@ -0,0 +1,17 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/people/")({
component: PeoplePage,
});
function PeoplePage() {
return (
<div>
<h1 className="text-3xl font-bold">People</h1>
<p className="mt-4">
This is where you'll see all the people identified in your photos.
</p>
{/* TODO: Add 'Cluster Faces' button */}
</div>
);
}