"use client"; import { useEffect, useMemo, useState } from "react"; import { useMutation } from "convex/react"; import { AlertCircle, ImageIcon, Loader2 } from "lucide-react"; import { api } from "@/convex/_generated/api"; import type { Id } from "@/convex/_generated/dataModel"; import { useAuthQuery } from "@/hooks/use-auth-query"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { collectMediaStorageIdsForResolution, resolveMediaPreviewUrl, } from "@/components/media/media-preview-utils"; const DEFAULT_LIMIT = 200; const MIN_LIMIT = 1; const MAX_LIMIT = 500; export type MediaLibraryMetadataItem = { storageId: Id<"_storage">; previewStorageId?: Id<"_storage">; filename?: string; mimeType?: string; width?: number; height?: number; previewWidth?: number; previewHeight?: number; sourceCanvasId: Id<"canvases">; sourceNodeId: Id<"nodes">; createdAt: number; }; export type MediaLibraryItem = MediaLibraryMetadataItem & { url?: string; }; export type MediaLibraryDialogProps = { open: boolean; onOpenChange: (open: boolean) => void; onPick?: (item: MediaLibraryItem) => void | Promise; title?: string; description?: string; limit?: number; pickCtaLabel?: string; }; function normalizeLimit(limit: number | undefined): number { if (typeof limit !== "number" || !Number.isFinite(limit)) { return DEFAULT_LIMIT; } return Math.min(MAX_LIMIT, Math.max(MIN_LIMIT, Math.floor(limit))); } function formatDimensions(width: number | undefined, height: number | undefined): string | null { if (typeof width !== "number" || typeof height !== "number") { return null; } return `${width} x ${height}px`; } export function MediaLibraryDialog({ open, onOpenChange, onPick, title = "Mediathek", description = "Waehle ein Bild aus deiner LemonSpace-Mediathek.", limit, pickCtaLabel = "Auswaehlen", }: MediaLibraryDialogProps) { const normalizedLimit = useMemo(() => normalizeLimit(limit), [limit]); const metadata = useAuthQuery( api.dashboard.listMediaLibrary, open ? { limit: normalizedLimit } : "skip", ); const resolveUrls = useMutation(api.storage.batchGetUrlsForUserMedia); const [urlMap, setUrlMap] = useState>({}); const [isResolvingUrls, setIsResolvingUrls] = useState(false); const [urlError, setUrlError] = useState(null); const [pendingPickStorageId, setPendingPickStorageId] = useState | null>(null); useEffect(() => { let isCancelled = false; async function run() { if (!open) { setUrlMap({}); setUrlError(null); setIsResolvingUrls(false); return; } if (!metadata) { return; } const storageIds = collectMediaStorageIdsForResolution(metadata); if (storageIds.length === 0) { setUrlMap({}); setUrlError(null); setIsResolvingUrls(false); return; } setIsResolvingUrls(true); setUrlError(null); try { const resolved = await resolveUrls({ storageIds }); if (isCancelled) { return; } setUrlMap(resolved); } catch (error) { if (isCancelled) { return; } setUrlMap({}); setUrlError(error instanceof Error ? error.message : "URLs konnten nicht geladen werden."); } finally { if (!isCancelled) { setIsResolvingUrls(false); } } } void run(); return () => { isCancelled = true; }; }, [metadata, open, resolveUrls]); const items: MediaLibraryItem[] = useMemo(() => { if (!metadata) { return []; } return metadata.map((item) => ({ ...item, url: resolveMediaPreviewUrl(item, urlMap), })); }, [metadata, urlMap]); const isMetadataLoading = open && metadata === undefined; const isInitialLoading = isMetadataLoading || (metadata !== undefined && isResolvingUrls); const isPreviewMode = typeof onPick !== "function"; async function handlePick(item: MediaLibraryItem): Promise { if (!onPick || pendingPickStorageId) { return; } setPendingPickStorageId(item.storageId); try { await onPick(item); } finally { setPendingPickStorageId(null); } } return ( {title} {description}
{isInitialLoading ? (
{Array.from({ length: 12 }).map((_, index) => (
))}
) : urlError ? (

Mediathek konnte nicht geladen werden

{urlError}

) : items.length === 0 ? (

Keine Medien vorhanden

Sobald du Bilder hochlaedst oder generierst, erscheinen sie hier.

) : (
{items.map((item) => { const dimensions = formatDimensions(item.width, item.height); const isPickingThis = pendingPickStorageId === item.storageId; return (
{item.url ? ( // eslint-disable-next-line @next/next/no-img-element {item.filename ) : (
)}

{item.filename ?? "Unbenanntes Bild"}

{dimensions ?? "Groesse unbekannt"}

{isPreviewMode ? (

Nur Vorschau

) : ( )}
); })}
)}
); }