Files
lynkeduppro-crm/src/components/dashboard/smart-gallery.tsx
T

123 lines
4.6 KiB
TypeScript

"use client";
// ============================================================
// Smart Gallery — the tenant's media library, powered by @photo-gallery/sdk embedded
// inside the dashboard shell. The SDK owns the gallery experience (grid, lightbox,
// photo + video editors, map, people, versions, comments, AI); this file owns the
// LynkedUp chrome around it: page head, demo-mode banner, sizing, and failure
// containment so a gallery fault can never take the dashboard down.
//
// Storage + identity come from `@/lib/gallery-api` (be-crm data door when the Shell is
// configured, device-local otherwise). Theming comes from GALLERY_THEME_TOKENS, which
// maps the SDK's tokens onto this dashboard's own CSS variables.
// ============================================================
import { Component, type ErrorInfo, type ReactNode } from "react";
import dynamic from "next/dynamic";
import { Icon } from "./ui";
import { useGalleryFeatures, useGalleryStorage } from "@/lib/gallery-api";
const SmartGalleryMount = dynamic(() => import("./smart-gallery-mount"), {
ssr: false,
loading: () => <GalleryPlaceholder label="Loading your library…" />,
});
export function SmartGallery({ theme }: { theme: "dark" | "light" }) {
const { live } = useGalleryStorage();
// Whole-view gate. `media.view` with the same permissive fallback the feature toggles use, so a
// superadmin/owner and the demo always pass, and a member is only blocked if they hold some Media
// perms but not `media.view`. The sidebar keeps the row visible (see sidebar.tsx §4) — the real
// gate is here.
const { canView } = useGalleryFeatures();
return (
<div className="view gal">
{/* Slim header — the big PageHead ate vertical space the gallery needs. The CRM topbar already
shows the "Smart Gallery" title; this compact row (~40px) just adds context and the icon. */}
<div className="gal-head">
<span className="gal-head-ic">
<Icon name="gallery" size={16} />
</span>
<h1 className="gal-head-title">Smart Gallery</h1>
<span className="gal-head-sub">Every photo and video for your jobs searchable, editable and shareable.</span>
</div>
{!live && (
<div className="gal-banner">
<Icon name="info" size={14} />
<span>Demo mode stored on this device only. It goes live once the Shell + be-crm are connected.</span>
</div>
)}
{canView ? (
<div className="gal-shell">
<GalleryBoundary>
<SmartGalleryMount theme={theme} />
</GalleryBoundary>
</div>
) : (
<div className="gal-shell">
<div className="gal-placeholder">
<span className="gal-placeholder-ic">
<Icon name="lock" size={28} />
</span>
<h3>You don&apos;t have access to the gallery</h3>
<p>Ask a workspace admin to grant you the &ldquo;View Smart Gallery&rdquo; permission.</p>
</div>
</div>
)}
</div>
);
}
/* ------------------------------------------------------------------ */
function GalleryPlaceholder({ label }: { label: string }) {
return (
<div className="gal-placeholder">
<span className="gal-placeholder-ic">
<Icon name="gallery" size={30} />
</span>
<p>{label}</p>
</div>
);
}
interface BoundaryState {
error: Error | null;
}
/**
* The gallery is a large third-party surface with canvas, workers and optional ML models. A render
* fault inside it must degrade to a message rather than blanking the whole dashboard, so it gets its
* own error boundary. (Error boundaries still require a class component in React 19.)
*/
class GalleryBoundary extends Component<{ children: ReactNode }, BoundaryState> {
state: BoundaryState = { error: null };
static getDerivedStateFromError(error: Error): BoundaryState {
return { error };
}
componentDidCatch(error: Error, info: ErrorInfo): void {
console.error("[smart-gallery] render failed", error, info.componentStack);
}
render(): ReactNode {
const { error } = this.state;
if (!error) return this.props.children;
return (
<div className="gal-placeholder gal-placeholder-error">
<span className="gal-placeholder-ic">
<Icon name="alert" size={30} />
</span>
<h3>The gallery could not be displayed</h3>
<p>{error.message || "An unexpected error occurred."}</p>
<button className="ds-btn v-outline s-sm" onClick={() => this.setState({ error: null })}>
<Icon name="refresh" size={14} /> Try again
</button>
</div>
);
}
}