forked from Goutam/lynkeduppro-crm
123 lines
4.6 KiB
TypeScript
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't have access to the gallery</h3>
|
|
<p>Ask a workspace admin to grant you the “View Smart Gallery” 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>
|
|
);
|
|
}
|
|
}
|