refactor: replace dynamic slug route with query params
This commit is contained in:
parent
287bc6bd69
commit
e48a15fad0
9 changed files with 32 additions and 473 deletions
|
|
@ -20,9 +20,9 @@ export const CustomTemplateCard = React.memo(function CustomTemplateCard({ templ
|
|||
const { previewLayouts, loading, totalLayouts } = useCustomTemplatePreview(`${template.id}`);
|
||||
const handleOpen = useCallback(() => {
|
||||
if (template.id.startsWith('custom-')) {
|
||||
router.push(`/template-preview/${template.id}`)
|
||||
router.push(`/template-preview?slug=${template.id}`)
|
||||
} else {
|
||||
router.push(`/template-preview/custom-${template.id}`)
|
||||
router.push(`/template-preview?slug=custom-${template.id}`)
|
||||
}
|
||||
}
|
||||
, [router, template.id]);
|
||||
|
|
@ -46,7 +46,7 @@ export const CustomTemplateCard = React.memo(function CustomTemplateCard({ templ
|
|||
[...Array(Math.min(4, template.layoutCount))].map((_, index) => (
|
||||
<div
|
||||
key={`${template.id}-loading-${index}`}
|
||||
className="relative bg-gradient-to-br from-purple-50 to-blue-50 border border-gray-200 overflow-hidden aspect-video rounded flex items-center justify-center"
|
||||
className="relative bg-linear-to-br from-purple-50 to-blue-50 border border-gray-200 overflow-hidden aspect-video rounded flex items-center justify-center"
|
||||
>
|
||||
<Loader2 className="w-4 h-4 text-purple-300 animate-spin" />
|
||||
</div>
|
||||
|
|
@ -172,7 +172,7 @@ const LayoutPreview = () => {
|
|||
}
|
||||
}, []);
|
||||
|
||||
const handleOpenPreview = useCallback((id: string) => router.push(`/template-preview/${id}`), [router]);
|
||||
const handleOpenPreview = useCallback((id: string) => router.push(`/template-preview?slug=${id}`), [router]);
|
||||
|
||||
|
||||
|
||||
|
|
@ -193,7 +193,7 @@ const LayoutPreview = () => {
|
|||
return (
|
||||
<div className="min-h-screen relative font-syne">
|
||||
<div
|
||||
className='fixed z-0 bottom-[-16.5rem] left-0 w-full h-full'
|
||||
className='fixed z-0 -bottom-[16.5rem] left-0 w-full h-full'
|
||||
style={{
|
||||
height: "341px",
|
||||
borderRadius: '1440px',
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export const SaveLayoutModal: React.FC<SaveLayoutModalProps> = ({
|
|||
const id = await onSave(layoutName.trim(), description.trim());
|
||||
if (id) {
|
||||
// Redirect to the new template preview page
|
||||
router.push(`/template-preview/custom-${id}`);
|
||||
router.push(`/template-preview?slug=custom-${id}`);
|
||||
}
|
||||
// Reset form after navigation decision
|
||||
setLayoutName("");
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ const CustomTemplatePage = () => {
|
|||
trackEvent(MixpanelEvent.CustomTemplate_Save_Templates_API_Call);
|
||||
const id = await saveLayout(layoutName, description);
|
||||
if (id) {
|
||||
router.push(`/template-preview/custom-${id}`);
|
||||
router.push(`/template-preview?slug=custom-${id}`);
|
||||
}
|
||||
return id;
|
||||
};
|
||||
|
|
@ -94,7 +94,7 @@ const CustomTemplatePage = () => {
|
|||
|
||||
}
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100">
|
||||
<div className="min-h-screen bg-linear-to-br from-slate-50 to-slate-100">
|
||||
<Header />
|
||||
<div className="max-w-[1440px] aspect-video mx-auto px-6">
|
||||
{/* Header */}
|
||||
|
|
|
|||
|
|
@ -36,9 +36,9 @@ const Header = () => {
|
|||
<span className="text-sm font-medium font-inter">Create Template</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/template-preview"
|
||||
href="/templates"
|
||||
prefetch={false}
|
||||
onClick={() => trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/template-preview" })}
|
||||
onClick={() => trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/templates" })}
|
||||
className="flex items-center gap-2 px-3 py-2 text-white hover:bg-primary/80 rounded-md transition-colors outline-none"
|
||||
role="menuitem"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,185 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useRef, useCallback, useState } from 'react';
|
||||
import { getHeader } from '@/app/(presentation-generator)/services/api/header';
|
||||
import { ApiResponseHandler } from '@/app/(presentation-generator)/services/api/api-error-handler';
|
||||
import { ProcessedSlide } from '@/app/(presentation-generator)/custom-template/types';
|
||||
import { CustomTemplateLayout } from '@/app/hooks/useCustomTemplates';
|
||||
import { getApiUrl } from '@/utils/api';
|
||||
|
||||
interface LayoutPayload {
|
||||
layout_id: string;
|
||||
layout_code: string;
|
||||
layout_name: string;
|
||||
}
|
||||
|
||||
/** Slide state for template preview: ProcessedSlide plus saved layout code and name */
|
||||
export type TemplatePreviewSlideState = ProcessedSlide & {
|
||||
react?: string;
|
||||
layout_name?: string;
|
||||
};
|
||||
|
||||
interface UseTemplateLayoutsAutoSaveOptions {
|
||||
templateId: string | null;
|
||||
layouts: CustomTemplateLayout[];
|
||||
slideStates: TemplatePreviewSlideState[];
|
||||
debounceMs?: number;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export type SaveStatus = 'idle' | 'saving' | 'saved' | 'error';
|
||||
|
||||
export const useTemplateLayoutsAutoSave = ({
|
||||
templateId,
|
||||
layouts,
|
||||
slideStates,
|
||||
debounceMs = 2000,
|
||||
enabled = true,
|
||||
}: UseTemplateLayoutsAutoSaveOptions) => {
|
||||
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const lastSavedDataRef = useRef<string>('');
|
||||
const [saveStatus, setSaveStatus] = useState<SaveStatus>('idle');
|
||||
const isSavingRef = useRef<boolean>(false);
|
||||
const [lastSavedAt, setLastSavedAt] = useState<Date | null>(null);
|
||||
|
||||
// Build the payload for saving
|
||||
const buildPayload = useCallback((): LayoutPayload[] => {
|
||||
const payload: LayoutPayload[] = [];
|
||||
|
||||
layouts.forEach((layout, index) => {
|
||||
const slideState = slideStates[index];
|
||||
if (slideState?.react && layout.rawLayoutId) {
|
||||
payload.push({
|
||||
layout_id: layout.rawLayoutId,
|
||||
layout_code: slideState.react,
|
||||
layout_name: slideState.layout_name || `Slide${index + 1}`
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return payload;
|
||||
}, [layouts, slideStates]);
|
||||
|
||||
// Save function
|
||||
const saveLayouts = useCallback(async (payload: LayoutPayload[]) => {
|
||||
if (!templateId || payload.length === 0 || isSavingRef.current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentDataString = JSON.stringify(payload);
|
||||
|
||||
// Skip if data hasn't changed since last save
|
||||
if (currentDataString === lastSavedDataRef.current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
isSavingRef.current = true;
|
||||
setSaveStatus('saving');
|
||||
console.log('🔄 Auto-saving template layouts...');
|
||||
|
||||
const response = await fetch(getApiUrl('/api/v1/ppt/template/update'), {
|
||||
method: 'PUT',
|
||||
headers: getHeader(),
|
||||
body: JSON.stringify({
|
||||
id: templateId,
|
||||
|
||||
layouts: payload,
|
||||
}),
|
||||
});
|
||||
|
||||
await ApiResponseHandler.handleResponse(response, 'Failed to auto-save layouts');
|
||||
|
||||
// Update last saved data reference
|
||||
lastSavedDataRef.current = currentDataString;
|
||||
setLastSavedAt(new Date());
|
||||
setSaveStatus('saved');
|
||||
console.log('✅ Auto-save successful');
|
||||
|
||||
// Reset to idle after showing "saved" briefly
|
||||
setTimeout(() => {
|
||||
setSaveStatus('idle');
|
||||
}, 2000);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ Auto-save failed:', error);
|
||||
setSaveStatus('error');
|
||||
|
||||
// Reset to idle after showing error briefly
|
||||
setTimeout(() => {
|
||||
setSaveStatus('idle');
|
||||
}, 3000);
|
||||
|
||||
return false;
|
||||
} finally {
|
||||
isSavingRef.current = false;
|
||||
}
|
||||
}, [templateId]);
|
||||
|
||||
// Debounced save trigger
|
||||
const debouncedSave = useCallback(() => {
|
||||
if (!enabled || !templateId) return;
|
||||
|
||||
// Clear existing timeout
|
||||
if (saveTimeoutRef.current) {
|
||||
clearTimeout(saveTimeoutRef.current);
|
||||
}
|
||||
|
||||
// Set new timeout
|
||||
saveTimeoutRef.current = setTimeout(() => {
|
||||
const payload = buildPayload();
|
||||
if (payload.length > 0) {
|
||||
saveLayouts(payload);
|
||||
}
|
||||
}, debounceMs);
|
||||
}, [enabled, templateId, buildPayload, saveLayouts, debounceMs]);
|
||||
|
||||
// Watch for changes in slideStates
|
||||
useEffect(() => {
|
||||
if (!enabled || !templateId || slideStates.length === 0) return;
|
||||
|
||||
// Check if any slide is still processing
|
||||
const hasProcessingSlide = Array.from(slideStates.values()).some(
|
||||
slide => slide.processing
|
||||
);
|
||||
|
||||
if (hasProcessingSlide) return;
|
||||
|
||||
debouncedSave();
|
||||
|
||||
// Cleanup timeout on unmount or when dependencies change
|
||||
return () => {
|
||||
if (saveTimeoutRef.current) {
|
||||
clearTimeout(saveTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [slideStates, enabled, templateId, debouncedSave]);
|
||||
|
||||
// Manual save function
|
||||
const saveNow = useCallback(async () => {
|
||||
// Clear any pending debounced save
|
||||
if (saveTimeoutRef.current) {
|
||||
clearTimeout(saveTimeoutRef.current);
|
||||
}
|
||||
|
||||
const payload = buildPayload();
|
||||
return saveLayouts(payload);
|
||||
}, [buildPayload, saveLayouts]);
|
||||
|
||||
// Cleanup on unmount - save any pending changes
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (saveTimeoutRef.current) {
|
||||
clearTimeout(saveTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
saveStatus,
|
||||
lastSavedAt,
|
||||
saveNow,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import GroupLayoutPreview from './components/TemplatePreviewClient';
|
||||
|
||||
// Allow dynamic params for custom templates
|
||||
export const dynamicParams = true;
|
||||
|
||||
// Generate static params for built-in template groups
|
||||
export async function generateStaticParams() {
|
||||
// Pre-render built-in template routes at build time
|
||||
// Custom templates (custom-*) will be generated on-demand
|
||||
return [
|
||||
{ slug: 'neo-general' },
|
||||
{ slug: 'neo-standard' },
|
||||
{ slug: 'neo-modern' },
|
||||
{ slug: 'general' },
|
||||
{ slug: 'modern' },
|
||||
{ slug: 'standard' },
|
||||
];
|
||||
}
|
||||
|
||||
export default function GroupLayoutPreviewPage() {
|
||||
return <GroupLayoutPreview />;
|
||||
}
|
||||
|
|
@ -1,35 +1,30 @@
|
|||
"use client";
|
||||
import React, { useEffect } from "react";
|
||||
import { useParams, usePathname, useRouter } from "next/navigation";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowLeft, Home, Loader2, Trash2 } from "lucide-react";
|
||||
|
||||
import { MixpanelEvent, trackEvent } from "@/utils/mixpanel";
|
||||
import TemplateService from "../../../services/api/template";
|
||||
import Header from "../../../dashboard/components/Header";
|
||||
import TemplateService from "../../services/api/template";
|
||||
import Header from "../../(dashboard)/dashboard/components/Header";
|
||||
import { toast } from "sonner";
|
||||
import { CustomTemplateLayout, useCustomTemplateDetails } from "@/app/hooks/useCustomTemplates";
|
||||
import { templates as templateGroups, getTemplatesByTemplateName } from "@/app/presentation-templates";
|
||||
|
||||
const GroupLayoutPreview = () => {
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const templateParams = params.slug as string;
|
||||
const templateParams = searchParams.get("slug") || "";
|
||||
|
||||
// Check if this is a custom template
|
||||
const isCustom = templateParams.startsWith("custom-");
|
||||
const customTemplateId = isCustom ? templateParams.split("custom-")[1] : null;
|
||||
|
||||
|
||||
// Fetch static templates if not custom
|
||||
const staticTemplates = !isCustom ? getTemplatesByTemplateName(templateParams) : [];
|
||||
|
||||
const staticGroup = !isCustom ? templateGroups.find((g: { id: string }) => g.id === templateParams) : null;
|
||||
|
||||
// Fetch custom template details if custom
|
||||
const {
|
||||
template: customTemplate,
|
||||
loading: customLoading,
|
||||
|
|
@ -37,8 +32,6 @@ const GroupLayoutPreview = () => {
|
|||
fonts: customFonts,
|
||||
} = useCustomTemplateDetails({ id: templateParams?.split("custom-")[1] || "", name: "", description: "" });
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const existingScript = document.querySelector('script[src*="tailwindcss.com"]');
|
||||
if (!existingScript) {
|
||||
|
|
@ -60,15 +53,13 @@ const GroupLayoutPreview = () => {
|
|||
const success = await TemplateService.deleteCustomTemplate(customTemplateId);
|
||||
if (success.success) {
|
||||
toast.success("Template deleted successfully");
|
||||
router.push("/template-preview");
|
||||
router.push("/templates");
|
||||
} else {
|
||||
toast.error("Failed to delete template");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Loading state for custom templates
|
||||
if (isCustom && (customLoading)) {
|
||||
if (isCustom && customLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Header />
|
||||
|
|
@ -80,7 +71,6 @@ const GroupLayoutPreview = () => {
|
|||
);
|
||||
}
|
||||
|
||||
// Error state
|
||||
if (isCustom && customError) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
|
|
@ -88,7 +78,7 @@ const GroupLayoutPreview = () => {
|
|||
<div className="flex flex-col items-center justify-center py-24">
|
||||
<h2 className="text-2xl font-bold text-red-600 mb-4">Error loading template</h2>
|
||||
<p className="text-gray-600 mb-4">{customError}</p>
|
||||
<Button onClick={() => router.push("/template-preview")}>
|
||||
<Button onClick={() => router.push("/templates")}>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Back to Templates
|
||||
</Button>
|
||||
|
|
@ -97,10 +87,9 @@ const GroupLayoutPreview = () => {
|
|||
);
|
||||
}
|
||||
|
||||
// Empty state
|
||||
if (
|
||||
(!isCustom && (!staticGroup || staticTemplates.length === 0)) ||
|
||||
(isCustom && (!customTemplate))
|
||||
(isCustom && !customTemplate)
|
||||
) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
|
|
@ -109,7 +98,7 @@ const GroupLayoutPreview = () => {
|
|||
<h2 className="text-2xl font-bold text-gray-900 mb-4">
|
||||
Template not found
|
||||
</h2>
|
||||
<Button onClick={() => router.push("/template-preview")}>
|
||||
<Button onClick={() => router.push("/templates")}>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Back to Templates
|
||||
</Button>
|
||||
|
|
@ -118,7 +107,6 @@ const GroupLayoutPreview = () => {
|
|||
);
|
||||
}
|
||||
|
||||
// Determine what to render
|
||||
const templateName = isCustom ? customTemplate?.template.name || "Custom Template" : staticGroup?.name || "";
|
||||
const templateDescription = isCustom
|
||||
? customTemplate?.template.description || ""
|
||||
|
|
@ -127,13 +115,10 @@ const GroupLayoutPreview = () => {
|
|||
? customTemplate?.layouts.length || 0
|
||||
: staticTemplates.length;
|
||||
|
||||
console.log('compileLayout', customTemplate)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Header />
|
||||
|
||||
{/* Header */}
|
||||
<header className="bg-white shadow-sm border-b sticky top-0 z-30">
|
||||
<div className=" mx-auto px-6 py-6">
|
||||
<div className="flex items-center justify-between mb-4 max-w-[1440px] mx-auto">
|
||||
|
|
@ -155,7 +140,7 @@ const GroupLayoutPreview = () => {
|
|||
size="sm"
|
||||
onClick={() => {
|
||||
trackEvent(MixpanelEvent.TemplatePreview_All_Groups_Button_Clicked, { pathname });
|
||||
router.push("/template-preview");
|
||||
router.push("/templates");
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
|
|
@ -166,7 +151,6 @@ const GroupLayoutPreview = () => {
|
|||
|
||||
{isCustom && (
|
||||
<div className="flex items-center gap-4">
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
|
|
@ -199,12 +183,9 @@ const GroupLayoutPreview = () => {
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</header>
|
||||
|
||||
{/* Layout Grid - Wrapped in SchemaHighlightProvider for custom templates */}
|
||||
<main className="mx-auto px-2 py-8" id="presentation-page">
|
||||
{/* Static Templates */}
|
||||
{!isCustom && (
|
||||
<div className="space-y-12 w-[1440px] h-[720px] aspect-video mx-auto">
|
||||
{staticTemplates.map((template: any, index: number) => {
|
||||
|
|
@ -251,12 +232,8 @@ const GroupLayoutPreview = () => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Custom Templates - with page-level schema editor */}
|
||||
{isCustom && (
|
||||
|
||||
<div className="flex flex-col items-center justify-center w-full gap-10 aspect-video mx-auto">
|
||||
{/* Slides List */}
|
||||
|
||||
<div className="flex flex-col items-center justify-center w-full gap-10 aspect-video mx-auto">
|
||||
{customTemplate && customTemplate.layouts.map((layout: CustomTemplateLayout, index: number) => {
|
||||
const LayoutComponent = layout.component;
|
||||
return (
|
||||
|
|
@ -280,7 +257,6 @@ const GroupLayoutPreview = () => {
|
|||
<span className="px-3 py-1 bg-gray-100 text-gray-600 rounded text-sm font-mono">
|
||||
{templateParams}:{layout.layoutId}
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -295,8 +271,6 @@ const GroupLayoutPreview = () => {
|
|||
</Card>
|
||||
);
|
||||
})}
|
||||
|
||||
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
|
@ -1,222 +1,14 @@
|
|||
"use client";
|
||||
import React, { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { ExternalLink, Loader2, Plus } from "lucide-react";
|
||||
|
||||
import { templates } from "@/app/presentation-templates";
|
||||
import type { TemplateLayoutsWithSettings } from "@/app/presentation-templates/utils";
|
||||
import { TemplateWithData } from "@/app/presentation-templates/utils";
|
||||
import {
|
||||
useCustomTemplateSummaries,
|
||||
useCustomTemplatePreview,
|
||||
CustomTemplates,
|
||||
} from "@/app/hooks/useCustomTemplates";
|
||||
import { CompiledLayout } from "@/app/hooks/compileLayout";
|
||||
import Header from "../(dashboard)/dashboard/components/Header";
|
||||
|
||||
// Component for rendering custom template card with lazy-loaded previews
|
||||
const CustomTemplateCard = ({ template }: { template: CustomTemplates }) => {
|
||||
const router = useRouter();
|
||||
const { previewLayouts, loading, totalLayouts } = useCustomTemplatePreview(template.id);
|
||||
|
||||
const handleNavigate = () => {
|
||||
if (template.id.startsWith('custom-')) {
|
||||
router.push(`/template-preview/${template.id}`);
|
||||
} else {
|
||||
router.push(`/template-preview/custom-${template.id}`);
|
||||
}
|
||||
}
|
||||
import React, { Suspense } from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import GroupLayoutPreview from "./components/TemplatePreviewClient";
|
||||
|
||||
const TemplatePreviewPage = () => {
|
||||
return (
|
||||
<Card
|
||||
className="cursor-pointer hover:shadow-lg transition-all duration-200 group overflow-hidden"
|
||||
onClick={handleNavigate}
|
||||
>
|
||||
<div className="p-5">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="text-xl font-bold text-gray-900">
|
||||
{template.name}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="px-2.5 py-0.5 bg-purple-100 text-purple-800 rounded-full text-sm font-medium">
|
||||
{totalLayouts}
|
||||
</span>
|
||||
<ExternalLink className="w-4 h-4 text-gray-400 group-hover:text-purple-600 transition-colors" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* Layout previews */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{loading ? (
|
||||
// Loading placeholders
|
||||
[...Array(Math.min(4, template.layoutCount))].map((_, index) => (
|
||||
<div
|
||||
key={`${template.id}-loading-${index}`}
|
||||
className="relative bg-gradient-to-br from-purple-50 to-blue-50 border border-gray-200 overflow-hidden aspect-video rounded flex items-center justify-center"
|
||||
>
|
||||
<Loader2 className="w-4 h-4 text-purple-300 animate-spin" />
|
||||
</div>
|
||||
))
|
||||
) : previewLayouts.length > 0 ? (
|
||||
// Actual layout previews
|
||||
previewLayouts.slice(0, 4).map((layout: CompiledLayout, index: number) => {
|
||||
const LayoutComponent = layout.component;
|
||||
return (
|
||||
<div
|
||||
key={`${template.id}-preview-${index}`}
|
||||
className="relative bg-gray-100 border border-gray-200 overflow-hidden aspect-video rounded"
|
||||
>
|
||||
<div className="absolute inset-0 bg-transparent z-10" />
|
||||
<div
|
||||
className="transform scale-[0.12] origin-top-left"
|
||||
style={{ width: "833.33%", height: "833.33%" }}
|
||||
>
|
||||
<LayoutComponent data={layout.sampleData} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
// Empty state placeholders
|
||||
[...Array(Math.min(4, template.layoutCount))].map((_, index) => (
|
||||
<div
|
||||
key={`${template.id}-empty-${index}`}
|
||||
className="relative bg-gray-100 border border-gray-200 overflow-hidden aspect-video rounded flex items-center justify-center"
|
||||
>
|
||||
<span className="text-xs text-gray-400">No preview</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</Card>
|
||||
<Suspense fallback={<div className="min-h-screen bg-gray-50 flex items-center justify-center"><Loader2 className="w-8 h-8 animate-spin text-blue-600" /></div>}>
|
||||
<GroupLayoutPreview />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
const LayoutPreview = () => {
|
||||
const router = useRouter();
|
||||
const { templates: customTemplates, loading: customLoading } = useCustomTemplateSummaries();
|
||||
useEffect(() => {
|
||||
const existingScript = document.querySelector('script[src*="tailwindcss.com"]');
|
||||
if (!existingScript) {
|
||||
const script = document.createElement("script");
|
||||
script.src = "https://cdn.tailwindcss.com";
|
||||
script.async = true;
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const totalStaticLayouts = templates.reduce((acc: number, g: TemplateLayoutsWithSettings) => acc + g.layouts.length, 0);
|
||||
const totalCustomLayouts = customTemplates.reduce((acc: number, t: CustomTemplates) => acc + t.layoutCount, 0);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Header />
|
||||
<div className="max-w-7xl mx-auto px-6 py-8">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">All Templates</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
{totalStaticLayouts + totalCustomLayouts} layouts across{" "}
|
||||
{templates.length + customTemplates.length} templates
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Inbuilt Templates Section */}
|
||||
<section className="mb-12">
|
||||
<h2 className="text-xl font-semibold text-gray-800 mb-6">Inbuilt Templates</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{templates.map((template: TemplateLayoutsWithSettings) => {
|
||||
const previewLayouts = template.layouts.slice(0, 4);
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={template.id}
|
||||
className="cursor-pointer hover:shadow-lg transition-all duration-200 group overflow-hidden"
|
||||
onClick={() => router.push(`/template-preview/${template.id}`)}
|
||||
>
|
||||
<div className="p-5">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="text-xl font-bold text-gray-900 capitalize">
|
||||
{template.name}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="px-2.5 py-0.5 bg-blue-100 text-blue-800 rounded-full text-sm font-medium">
|
||||
{template.layouts.length}
|
||||
</span>
|
||||
<ExternalLink className="w-4 h-4 text-gray-400 group-hover:text-blue-600 transition-colors" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-600 mb-4 line-clamp-2">
|
||||
{template.description}
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{previewLayouts.map((layout: TemplateWithData, index: number) => {
|
||||
const LayoutComponent = layout.component;
|
||||
return (
|
||||
<div
|
||||
key={`${template.id}-preview-${index}`}
|
||||
className="relative bg-gray-100 border border-gray-200 overflow-hidden aspect-video rounded"
|
||||
>
|
||||
<div className="absolute inset-0 bg-transparent z-10" />
|
||||
<div
|
||||
className="transform scale-[0.12] origin-top-left"
|
||||
style={{ width: "833.33%", height: "833.33%" }}
|
||||
>
|
||||
<LayoutComponent data={layout.sampleData} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Custom Templates Section */}
|
||||
<section>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
|
||||
<h2 className="text-xl font-semibold text-gray-800 ">
|
||||
My Custom Templates
|
||||
</h2>
|
||||
<a href="/custom-template" className="text-sm flex font-bold font-inter items-center justify-center gap-2 bg-[#5146E5] text-white px-4 py-2 rounded-md">
|
||||
<Plus className="w-4 h-4" /> Create new template
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{customLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
|
||||
<span className="ml-3 text-gray-600">Loading custom templates...</span>
|
||||
</div>
|
||||
) : customTemplates.length === 0 ? (
|
||||
<Card className="p-8 text-center">
|
||||
<p className="text-gray-500">No custom templates yet.</p>
|
||||
<p className="text-sm text-gray-400 mt-2">
|
||||
Custom templates you create will appear here.
|
||||
</p>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{customTemplates.map((template: CustomTemplates) => (
|
||||
<CustomTemplateCard key={template.id} template={template} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayoutPreview;
|
||||
export default TemplatePreviewPage;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Layout, Plus } from "lucide-react";
|
|||
|
||||
const Header: React.FC = () => {
|
||||
return (
|
||||
<header className="w-full border-b bg-white/60 backdrop-blur supports-[backdrop-filter]:bg-white/60 sticky top-0 z-50">
|
||||
<header className="w-full border-b bg-white/60 backdrop-blur supports-backdrop-filter:bg-white/60 sticky top-0 z-50">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex h-16 items-center justify-between">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
|
|
@ -18,7 +18,7 @@ const Header: React.FC = () => {
|
|||
<Plus className="w-5 h-5" />
|
||||
<span className="text-sm font-medium font-inter">Create Template</span>
|
||||
</Link>
|
||||
<Link href="/template-preview" className="inline-flex items-center gap-2 text-gray-700 hover:text-gray-900">
|
||||
<Link href="/templates" className="inline-flex items-center gap-2 text-gray-700 hover:text-gray-900">
|
||||
<Layout className="w-5 h-5" />
|
||||
<span className="text-sm font-medium font-inter">Templates</span>
|
||||
</Link>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue