Files
k-photos/k-photos-frontend/app/(app)/layout.tsx
Gabriel Kaszewski 957737ac9b feat: frontend MVP — auth, timeline, upload, albums, admin, image viewer
Backend:
- user roles (DB + JWT + first-user-is-admin)
- volume-aware file resolver (multi-volume asset serving)
- directory scanner uses volume URI directly
- date-summary endpoint (capture date from EXIF)
- timeline ordered by capture date
- list endpoints: volumes, plugins, pipelines, library paths
- delete endpoints: volumes, library paths
- configurable upload body limit (MAX_UPLOAD_BYTES)

Frontend:
- auth: login/register, token refresh, role-based admin gate
- timeline: date-grouped grid, infinite scroll, date scrubber
- image viewer: fullscreen zoom/pan/pinch, metadata sidebar
- upload: drag-drop, sequential upload, progress tracking
- albums: create, add/remove photos, asset picker dialog
- admin: storage (import library), jobs (pagination, error details),
  plugins (list + toggle), pipelines, sidecars, duplicates
- multi-select mode with add-to-album action
- TanStack Query for all data fetching
2026-06-01 01:35:43 +02:00

77 lines
2.3 KiB
TypeScript

"use client"
import { useEffect } from "react"
import { useRouter } from "next/navigation"
import { useAuth } from "@/hooks/use-auth"
import {
SidebarProvider,
Sidebar,
SidebarContent,
SidebarHeader,
SidebarInset,
SidebarTrigger,
} from "@/components/ui/sidebar"
import { Separator } from "@/components/ui/separator"
import { AlbumSidebar } from "@/components/album-sidebar"
import { AdminSidebar } from "@/components/admin-sidebar"
import { UploadDialog } from "@/components/upload-dialog"
import { Spinner } from "@/components/ui/spinner"
import { CameraIcon, LogOutIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import Link from "next/link"
export default function AppLayout({ children }: { children: React.ReactNode }) {
const { user, isAuthenticated, isLoading, logout } = useAuth()
const router = useRouter()
useEffect(() => {
if (!isLoading && !isAuthenticated) {
router.replace("/login")
}
}, [isLoading, isAuthenticated, router])
if (isLoading) {
return (
<div className="flex min-h-svh items-center justify-center">
<Spinner />
</div>
)
}
if (!isAuthenticated) return null
return (
<SidebarProvider>
<Sidebar>
<SidebarHeader className="flex flex-row items-center gap-2 px-4 py-3">
<Link href="/" className="flex items-center gap-2 font-semibold">
<CameraIcon className="h-5 w-5" />
K-Photos
</Link>
</SidebarHeader>
<SidebarContent>
<AlbumSidebar />
<AdminSidebar />
</SidebarContent>
<div className="flex items-center justify-between border-t px-4 py-2">
<span className="truncate text-xs text-muted-foreground">
{user?.username}
</span>
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={logout}>
<LogOutIcon className="h-3.5 w-3.5" />
</Button>
</div>
</Sidebar>
<SidebarInset>
<header className="flex h-12 items-center gap-2 border-b px-4">
<SidebarTrigger />
<Separator orientation="vertical" className="h-4" />
<div className="flex-1" />
<UploadDialog />
</header>
<main className="flex-1 p-4">{children}</main>
</SidebarInset>
</SidebarProvider>
)
}