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
This commit is contained in:
76
k-photos-frontend/app/(app)/layout.tsx
Normal file
76
k-photos-frontend/app/(app)/layout.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user