update: send template name and description and show them
This commit is contained in:
parent
9b9f6b88de
commit
2f653d78cf
10 changed files with 273 additions and 110 deletions
|
|
@ -12,13 +12,14 @@ from utils.asset_directory_utils import get_images_directory
|
|||
from services.database import get_async_session
|
||||
from models.sql.presentation_layout_code import PresentationLayoutCodeModel
|
||||
from .prompts import GENERATE_HTML_SYSTEM_PROMPT, HTML_TO_REACT_SYSTEM_PROMPT, HTML_EDIT_SYSTEM_PROMPT
|
||||
from models.sql.template import TemplateModel
|
||||
|
||||
|
||||
# Create separate routers for each functionality
|
||||
SLIDE_TO_HTML_ROUTER = APIRouter(prefix="/slide-to-html", tags=["slide-to-html"])
|
||||
HTML_TO_REACT_ROUTER = APIRouter(prefix="/html-to-react", tags=["html-to-react"])
|
||||
HTML_EDIT_ROUTER = APIRouter(prefix="/html-edit", tags=["html-edit"])
|
||||
LAYOUT_MANAGEMENT_ROUTER = APIRouter(prefix="/layout-management", tags=["layout-management"])
|
||||
LAYOUT_MANAGEMENT_ROUTER = APIRouter(prefix="/template-management", tags=["template-management"])
|
||||
|
||||
|
||||
# Request/Response models for slide-to-html endpoint
|
||||
|
|
@ -74,12 +75,14 @@ class GetLayoutsResponse(BaseModel):
|
|||
success: bool
|
||||
layouts: list[LayoutData]
|
||||
message: Optional[str] = None
|
||||
template: Optional[dict] = None
|
||||
|
||||
|
||||
class PresentationSummary(BaseModel):
|
||||
presentation_id: str
|
||||
layout_count: int
|
||||
last_updated_at: Optional[datetime] = None
|
||||
template: Optional[dict] = None
|
||||
|
||||
|
||||
class GetPresentationSummaryResponse(BaseModel):
|
||||
|
|
@ -96,6 +99,25 @@ class ErrorResponse(BaseModel):
|
|||
error_code: Optional[str] = None
|
||||
|
||||
|
||||
class TemplateCreateRequest(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class TemplateCreateResponse(BaseModel):
|
||||
success: bool
|
||||
template: dict
|
||||
message: Optional[str] = None
|
||||
|
||||
|
||||
class TemplateInfo(BaseModel):
|
||||
id: str
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
created_at: Optional[datetime] = None
|
||||
|
||||
|
||||
async def generate_html_from_slide(base64_image: str, media_type: str, xml_content: str, api_key: str, fonts: Optional[List[str]] = None) -> str:
|
||||
"""
|
||||
Generate HTML content from slide image and XML using OpenAI GPT-5 Responses API.
|
||||
|
|
@ -623,7 +645,7 @@ async def edit_html_with_images_endpoint(
|
|||
|
||||
# ENDPOINT 4: Save layouts for a presentation
|
||||
@LAYOUT_MANAGEMENT_ROUTER.post(
|
||||
"/save-layouts",
|
||||
"/save-templates",
|
||||
response_model=SaveLayoutsResponse,
|
||||
responses={
|
||||
400: {"model": ErrorResponse, "description": "Validation error"},
|
||||
|
|
@ -739,7 +761,7 @@ async def save_layouts(
|
|||
|
||||
# ENDPOINT 5: Get layouts for a presentation
|
||||
@LAYOUT_MANAGEMENT_ROUTER.get(
|
||||
"/get-layouts/{presentation_id}",
|
||||
"/get-templates/{presentation_id}",
|
||||
response_model=GetLayoutsResponse,
|
||||
responses={
|
||||
400: {"model": ErrorResponse, "description": "Invalid presentation ID"},
|
||||
|
|
@ -798,10 +820,22 @@ async def get_layouts(
|
|||
for layout in layouts_db
|
||||
]
|
||||
|
||||
# Fetch template meta
|
||||
template_meta = await session.get(TemplateModel, presentation_id)
|
||||
template = None
|
||||
if template_meta:
|
||||
template = {
|
||||
"id": template_meta.id,
|
||||
"name": template_meta.name,
|
||||
"description": template_meta.description,
|
||||
"created_at": template_meta.created_at,
|
||||
}
|
||||
|
||||
return GetLayoutsResponse(
|
||||
success=True,
|
||||
layouts=layouts,
|
||||
message=f"Retrieved {len(layouts)} layout(s) for presentation {presentation_id}"
|
||||
message=f"Retrieved {len(layouts)} layout(s) for presentation {presentation_id}",
|
||||
template=template,
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
|
|
@ -823,11 +857,11 @@ async def get_layouts(
|
|||
description="Retrieve a summary of all presentations and the number of layouts in each",
|
||||
responses={
|
||||
200: {"model": GetPresentationSummaryResponse, "description": "Presentations summary retrieved successfully"},
|
||||
500: {"model": ErrorResponse, "description": "Internal server error"}
|
||||
}
|
||||
500: {"model": ErrorResponse, "description": "Internal server error"},
|
||||
},
|
||||
)
|
||||
async def get_presentations_summary(
|
||||
session: AsyncSession = Depends(get_async_session)
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
"""
|
||||
Get summary of all presentations with their layout counts.
|
||||
|
|
@ -843,16 +877,27 @@ async def get_presentations_summary(
|
|||
result = await session.execute(stmt)
|
||||
presentation_data = result.all()
|
||||
|
||||
# Convert to response format
|
||||
presentations = [
|
||||
PresentationSummary(
|
||||
presentation_id=row.presentation_id,
|
||||
layout_count=row.layout_count,
|
||||
last_updated_at=row.last_updated_at
|
||||
# Convert to response format with template info if available
|
||||
presentations = []
|
||||
for row in presentation_data:
|
||||
template_meta = await session.get(TemplateModel, row.presentation_id)
|
||||
template = None
|
||||
if template_meta:
|
||||
template = {
|
||||
"id": template_meta.id,
|
||||
"name": template_meta.name,
|
||||
"description": template_meta.description,
|
||||
"created_at": template_meta.created_at,
|
||||
}
|
||||
presentations.append(
|
||||
PresentationSummary(
|
||||
presentation_id=row.presentation_id,
|
||||
layout_count=row.layout_count,
|
||||
last_updated_at=row.last_updated_at,
|
||||
template=template,
|
||||
)
|
||||
)
|
||||
for row in presentation_data
|
||||
]
|
||||
|
||||
|
||||
# Calculate totals
|
||||
total_presentations = len(presentations)
|
||||
total_layouts = sum(p.layout_count for p in presentations)
|
||||
|
|
@ -862,7 +907,7 @@ async def get_presentations_summary(
|
|||
presentations=presentations,
|
||||
total_presentations=total_presentations,
|
||||
total_layouts=total_layouts,
|
||||
message=f"Retrieved {total_presentations} presentation(s) with {total_layouts} total layout(s)"
|
||||
message=f"Retrieved {total_presentations} presentation(s) with {total_layouts} total layout(s)",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -870,4 +915,53 @@ async def get_presentations_summary(
|
|||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Internal server error while retrieving presentations summary: {str(e)}"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@LAYOUT_MANAGEMENT_ROUTER.post(
|
||||
"/templates",
|
||||
response_model=TemplateCreateResponse,
|
||||
responses={
|
||||
400: {"model": ErrorResponse, "description": "Validation error"},
|
||||
500: {"model": ErrorResponse, "description": "Internal server error"},
|
||||
},
|
||||
)
|
||||
async def create_template(
|
||||
request: TemplateCreateRequest,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
try:
|
||||
if not request.id or not request.name:
|
||||
raise HTTPException(status_code=400, detail="id and name are required")
|
||||
|
||||
# Upsert template by id
|
||||
existing = await session.get(TemplateModel, request.id)
|
||||
if existing:
|
||||
existing.name = request.name
|
||||
existing.description = request.description
|
||||
else:
|
||||
session.add(
|
||||
TemplateModel(
|
||||
id=request.id, name=request.name, description=request.description
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
# Read back
|
||||
template = await session.get(TemplateModel, request.id)
|
||||
return TemplateCreateResponse(
|
||||
success=True,
|
||||
template={
|
||||
"id": template.id,
|
||||
"name": template.name,
|
||||
"description": template.description,
|
||||
"created_at": template.created_at,
|
||||
},
|
||||
message="Template saved",
|
||||
)
|
||||
except HTTPException:
|
||||
await session.rollback()
|
||||
raise
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
raise HTTPException(status_code=500, detail=f"Failed to save template: {str(e)}")
|
||||
|
|
@ -14,6 +14,7 @@ from models.sql.ollama_pull_status import OllamaPullStatus
|
|||
from models.sql.presentation import PresentationModel
|
||||
from models.sql.slide import SlideModel
|
||||
from models.sql.presentation_layout_code import PresentationLayoutCodeModel
|
||||
from models.sql.template import TemplateModel
|
||||
from utils.get_env import get_app_data_directory_env, get_database_url_env
|
||||
|
||||
|
||||
|
|
@ -70,10 +71,11 @@ async def create_db_and_tables():
|
|||
SlideModel.__table__,
|
||||
KeyValueSqlModel.__table__,
|
||||
ImageAsset.__table__,
|
||||
PresentationLayoutCodeModel.__table__,
|
||||
],
|
||||
)
|
||||
)
|
||||
PresentationLayoutCodeModel.__table__,
|
||||
TemplateModel.__table__,
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
async with container_db_engine.begin() as conn:
|
||||
await conn.run_sync(
|
||||
|
|
|
|||
|
|
@ -345,7 +345,7 @@ export const LayoutProvider: React.FC<{
|
|||
const fullDataByGroup = new Map<string, FullDataInfo[]>();
|
||||
try {
|
||||
const customGroupResponse = await fetch(
|
||||
"/api/v1/ppt/layout-management/summary"
|
||||
"/api/v1/ppt/template-management/summary"
|
||||
);
|
||||
const customGroupData = await customGroupResponse.json();
|
||||
|
||||
|
|
@ -359,7 +359,7 @@ export const LayoutProvider: React.FC<{
|
|||
}
|
||||
const presentationId = group.presentation_id;
|
||||
const customLayoutResponse = await fetch(
|
||||
`/api/v1/ppt/layout-management/get-layouts/${presentationId}`
|
||||
`/api/v1/ppt/template-management/get-templates/${presentationId}`
|
||||
);
|
||||
const customLayoutsData = await customLayoutResponse.json();
|
||||
const allLayout = customLayoutsData.layouts;
|
||||
|
|
|
|||
|
|
@ -97,8 +97,7 @@ const FontManager: React.FC<FontManagerProps> = ({
|
|||
Font Management
|
||||
</CardTitle>
|
||||
<p className="text-sm text-gray-600">
|
||||
Manage fonts across all slides. Upload fonts once and they'll be
|
||||
available for all slides.
|
||||
We couldn't load these fonts automatically. Please upload them manually. Make sure naem of the font should be exactly as shown.
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
|
|
|
|||
|
|
@ -134,9 +134,16 @@ export const useLayoutSaving = (
|
|||
}
|
||||
console.log(reactComponents);
|
||||
|
||||
// First create/update the template metadata
|
||||
await fetch("/api/v1/ppt/template-management/templates", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ id: presentationId, name: layoutName, description }),
|
||||
});
|
||||
|
||||
// Save the layout components to the app_data/layouts folder
|
||||
const saveResponse = await fetch(
|
||||
"/api/v1/ppt/layout-management/save-layouts",
|
||||
"/api/v1/ppt/template-management/save-templates",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
|
@ -144,8 +151,6 @@ export const useLayoutSaving = (
|
|||
},
|
||||
body: JSON.stringify({
|
||||
layouts: reactComponents,
|
||||
layout_name: layoutName,
|
||||
description: description,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -20,18 +20,22 @@ const LayoutSelection: React.FC<LayoutSelectionProps> = ({
|
|||
loading
|
||||
} = useLayout();
|
||||
|
||||
const [summaryMap, setSummaryMap] = React.useState<Record<string, number>>({});
|
||||
const [summaryMap, setSummaryMap] = React.useState<Record<string, { lastUpdatedAt?: number; name?: string; description?: string }>>({});
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch custom templates summary to get last_updated_at for sorting
|
||||
fetch("/api/v1/ppt/layout-management/summary")
|
||||
// Fetch custom templates summary to get last_updated_at and template meta for sorting and display
|
||||
fetch("/api/v1/ppt/template-management/summary")
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
const map: Record<string, number> = {};
|
||||
const map: Record<string, { lastUpdatedAt?: number; name?: string; description?: string }> = {};
|
||||
if (data && Array.isArray(data.presentations)) {
|
||||
for (const p of data.presentations) {
|
||||
// groups are named custom-<presentation_id>
|
||||
map[`custom-${p.presentation_id}`] = p.last_updated_at ? new Date(p.last_updated_at).getTime() : 0;
|
||||
const slug = `custom-${p.presentation_id}`;
|
||||
map[slug] = {
|
||||
lastUpdatedAt: p.last_updated_at ? new Date(p.last_updated_at).getTime() : 0,
|
||||
name: p.template?.name,
|
||||
description: p.template?.description,
|
||||
};
|
||||
}
|
||||
}
|
||||
setSummaryMap(map);
|
||||
|
|
@ -45,10 +49,12 @@ const LayoutSelection: React.FC<LayoutSelectionProps> = ({
|
|||
|
||||
const Groups: LayoutGroup[] = groups.map(groupName => {
|
||||
const settings = getGroupSetting(groupName);
|
||||
const customMeta = summaryMap[groupName];
|
||||
const isCustom = groupName.toLowerCase().startsWith("custom-");
|
||||
return {
|
||||
id: groupName,
|
||||
name: groupName,
|
||||
description: settings?.description || `${groupName} presentation templates`,
|
||||
name: isCustom && customMeta?.name ? customMeta.name : groupName,
|
||||
description: (isCustom && customMeta?.description) ? customMeta.description : (settings?.description || `${groupName} presentation templates`),
|
||||
ordered: settings?.ordered || false,
|
||||
default: settings?.default || false,
|
||||
};
|
||||
|
|
@ -60,16 +66,16 @@ const LayoutSelection: React.FC<LayoutSelectionProps> = ({
|
|||
if (!a.default && b.default) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}, [getAllGroups, getLayoutsByGroup, getGroupSetting]);
|
||||
}, [getAllGroups, getLayoutsByGroup, getGroupSetting, summaryMap]);
|
||||
|
||||
const inBuiltGroups = React.useMemo(
|
||||
() => layoutGroups.filter(g => !g.name.toLowerCase().startsWith("custom-")),
|
||||
() => layoutGroups.filter(g => !g.id.toLowerCase().startsWith("custom-")),
|
||||
[layoutGroups]
|
||||
);
|
||||
const customGroups = React.useMemo(() => {
|
||||
const unsorted = layoutGroups.filter(g => g.name.toLowerCase().startsWith("custom-"));
|
||||
// Sort by last_updated_at desc using summaryMap
|
||||
return unsorted.sort((a, b) => (summaryMap[b.name] || 0) - (summaryMap[a.name] || 0));
|
||||
const unsorted = layoutGroups.filter(g => g.id.toLowerCase().startsWith("custom-"));
|
||||
// Sort by last_updated_at desc using summaryMap keyed by slug id
|
||||
return unsorted.sort((a, b) => (summaryMap[b.id]?.lastUpdatedAt || 0) - (summaryMap[a.id]?.lastUpdatedAt || 0));
|
||||
}, [layoutGroups, summaryMap]);
|
||||
|
||||
// Auto-select first group when groups are loaded
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ import { useDrawingCanvas } from "../../custom-template/hooks/useDrawingCanvas";
|
|||
const presentationId = slug.replace('custom-','');
|
||||
refetch();
|
||||
router.back();
|
||||
const response = await fetch(`/api/v1/ppt/layout-management/delete-layouts/${presentationId}`, {
|
||||
const response = await fetch(`/api/v1/ppt/template-management/delete-templates/${presentationId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (response.ok) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { Card } from "@/components/ui/card";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowLeft, Home, Trash2 } from "lucide-react";
|
||||
import { useLayout } from "@/app/(presentation-generator)/context/LayoutContext";
|
||||
|
||||
const GroupLayoutPreview = () => {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
|
|
@ -15,6 +16,8 @@ const GroupLayoutPreview = () => {
|
|||
const { getFullDataByGroup, loading,refetch } = useLayout();
|
||||
const layoutGroup = getFullDataByGroup(slug);
|
||||
|
||||
const [templateMeta, setTemplateMeta] = React.useState<{ name?: string; description?: string } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const existingScript = document.querySelector(
|
||||
'script[src*="tailwindcss.com"]'
|
||||
|
|
@ -27,6 +30,21 @@ const GroupLayoutPreview = () => {
|
|||
}
|
||||
}, [slug]);
|
||||
|
||||
useEffect(() => {
|
||||
// Load template meta for custom groups
|
||||
if (slug.startsWith("custom-")) {
|
||||
const presentationId = slug.replace("custom-", "");
|
||||
fetch(`/api/v1/ppt/template-management/get-templates/${presentationId}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data?.template) {
|
||||
setTemplateMeta({ name: data.template.name, description: data.template.description });
|
||||
}
|
||||
})
|
||||
.catch(() => setTemplateMeta(null));
|
||||
}
|
||||
}, [slug]);
|
||||
|
||||
// Handle loading state
|
||||
if (loading) {
|
||||
return <LoadingStates type="loading" />;
|
||||
|
|
@ -40,7 +58,7 @@ const GroupLayoutPreview = () => {
|
|||
const presentationId = slug.replace('custom-','');
|
||||
refetch();
|
||||
router.back();
|
||||
const response = await fetch(`/api/v1/ppt/layout-management/delete-layouts/${presentationId}`, {
|
||||
const response = await fetch(`/api/v1/ppt/template-management/delete-templates/${presentationId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (response.ok) {
|
||||
|
|
@ -79,11 +97,10 @@ const GroupLayoutPreview = () => {
|
|||
|
||||
<div className="text-center">
|
||||
<h1 className="text-3xl font-bold text-gray-900 capitalize">
|
||||
{layoutGroup[0].groupName} Layouts
|
||||
{templateMeta?.name || layoutGroup[0].groupName} Layouts
|
||||
</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
{layoutGroup.length} layout{layoutGroup.length !== 1 ? "s" : ""} •{" "}
|
||||
{layoutGroup[0].groupName}
|
||||
{layoutGroup.length} layout{layoutGroup.length !== 1 ? "s" : ""} • {templateMeta?.description || layoutGroup[0].groupName}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export const useGroupLayoutLoader = (
|
|||
const presentationId = groupSlug.replace("custom-", "");
|
||||
|
||||
const customLayoutResponse = await fetch(
|
||||
`/api/v1/ppt/layout-management/get-layouts/${presentationId}`
|
||||
`/api/v1/ppt/template-management/get-templates/${presentationId}`
|
||||
);
|
||||
|
||||
if (!customLayoutResponse.ok) {
|
||||
|
|
@ -86,10 +86,11 @@ export const useGroupLayoutLoader = (
|
|||
|
||||
const customLayoutsData = await customLayoutResponse.json();
|
||||
const allLayouts = customLayoutsData.layouts;
|
||||
const templateMeta = customLayoutsData.template;
|
||||
|
||||
const groupLayouts: LayoutInfo[] = [];
|
||||
const settings: GroupSetting = {
|
||||
description: `Custom presentation layouts`,
|
||||
description: templateMeta?.description || `Custom presentation layouts`,
|
||||
ordered: false,
|
||||
default: false,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"use client";
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import LoadingStates from "./components/LoadingStates";
|
||||
import { Card } from "@/components/ui/card";
|
||||
|
|
@ -18,6 +18,8 @@ const LayoutPreview = () => {
|
|||
} = useLayout();
|
||||
const router = useRouter();
|
||||
|
||||
const [summaryMap, setSummaryMap] = useState<Record<string, { lastUpdatedAt?: number; name?: string; description?: string }>>({});
|
||||
|
||||
useEffect(() => {
|
||||
const existingScript = document.querySelector(
|
||||
'script[src*="tailwindcss.com"]'
|
||||
|
|
@ -30,6 +32,27 @@ const LayoutPreview = () => {
|
|||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch summary to map custom group slug to template meta and last updated time
|
||||
fetch("/api/v1/ppt/template-management/summary")
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
const map: Record<string, { lastUpdatedAt?: number; name?: string; description?: string }> = {};
|
||||
if (data && Array.isArray(data.presentations)) {
|
||||
for (const p of data.presentations) {
|
||||
const slug = `custom-${p.presentation_id}`;
|
||||
map[slug] = {
|
||||
lastUpdatedAt: p.last_updated_at ? new Date(p.last_updated_at).getTime() : 0,
|
||||
name: p.template?.name,
|
||||
description: p.template?.description,
|
||||
};
|
||||
}
|
||||
}
|
||||
setSummaryMap(map);
|
||||
})
|
||||
.catch(() => setSummaryMap({}));
|
||||
}, []);
|
||||
|
||||
// Transform context data to match expected format
|
||||
const layoutGroups = getAllGroups().map((groupName) => ({
|
||||
groupName,
|
||||
|
|
@ -44,6 +67,11 @@ const LayoutPreview = () => {
|
|||
g.groupName.toLowerCase().startsWith("custom-")
|
||||
);
|
||||
|
||||
// Sort custom groups by last_updated_at desc using summaryMap
|
||||
const customGroupsSorted = [...customGroups].sort(
|
||||
(a, b) => (summaryMap[b.groupName]?.lastUpdatedAt || 0) - (summaryMap[a.groupName]?.lastUpdatedAt || 0)
|
||||
);
|
||||
|
||||
// Handle loading state
|
||||
if (loading) {
|
||||
return <LoadingStates type="loading" />;
|
||||
|
|
@ -77,41 +105,47 @@ const LayoutPreview = () => {
|
|||
<div className="max-w-7xl mx-auto px-6 py-6 w-full">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">In Built Templates</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{inBuiltGroups.map((group) => (
|
||||
<Card
|
||||
key={group.groupName}
|
||||
className="cursor-pointer hover:shadow-md transition-all duration-200 group"
|
||||
onClick={() => router.push(`/template-preview/${group.groupName}`)}
|
||||
>
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-lg font-semibold text-gray-900 capitalize group-hover:text-blue-600 transition-colors">
|
||||
{group.groupName}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
|
||||
{group.layouts.length}
|
||||
{inBuiltGroups.map((group) => {
|
||||
const isCustom = group.groupName.toLowerCase().startsWith("custom-");
|
||||
const meta = summaryMap[group.groupName];
|
||||
const displayName = isCustom && meta?.name ? meta.name : group.groupName;
|
||||
const displayDescription = isCustom && meta?.description ? meta.description : group.settings.description;
|
||||
return (
|
||||
<Card
|
||||
key={group.groupName}
|
||||
className="cursor-pointer hover:shadow-md transition-all duration-200 group"
|
||||
onClick={() => router.push(`/template-preview/${group.groupName}`)}
|
||||
>
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-lg font-semibold text-gray-900 capitalize group-hover:text-blue-600 transition-colors">
|
||||
{displayName}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
|
||||
{group.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">
|
||||
{displayDescription}
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-gray-500">
|
||||
{group.layouts.length} layout
|
||||
{group.layouts.length !== 1 ? "s" : ""}
|
||||
</span>
|
||||
<ExternalLink className="w-4 h-4 text-gray-400 group-hover:text-blue-600 transition-colors" />
|
||||
{group.settings.default && (
|
||||
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
Default
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
{group.settings.description}
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-gray-500">
|
||||
{group.layouts.length} layout
|
||||
{group.layouts.length !== 1 ? "s" : ""}
|
||||
</span>
|
||||
{group.settings.default && (
|
||||
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
Default
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -123,37 +157,42 @@ const LayoutPreview = () => {
|
|||
<h2 className="text-xl font-semibold text-gray-900">Custom AI Templates</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{customGroups.length > 0 ? (
|
||||
customGroups.map((group) => (
|
||||
<Card
|
||||
key={group.groupName}
|
||||
className="cursor-pointer hover:shadow-md transition-all duration-200 group"
|
||||
onClick={() => router.push(`/template-preview/${group.groupName}`)}
|
||||
>
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-lg font-semibold text-gray-900 capitalize group-hover:text-blue-600 transition-colors">
|
||||
{group.groupName}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
|
||||
{group.layouts.length}
|
||||
{customGroupsSorted.length > 0 ? (
|
||||
customGroupsSorted.map((group) => {
|
||||
const meta = summaryMap[group.groupName];
|
||||
const displayName = meta?.name ? meta.name : group.groupName;
|
||||
const displayDescription = meta?.description ? meta.description : group.settings.description;
|
||||
return (
|
||||
<Card
|
||||
key={group.groupName}
|
||||
className="cursor-pointer hover:shadow-md transition-all duration-200 group"
|
||||
onClick={() => router.push(`/template-preview/${group.groupName}`)}
|
||||
>
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-lg font-semibold text-gray-900 capitalize group-hover:text-blue-600 transition-colors">
|
||||
{displayName}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
|
||||
{group.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">
|
||||
{displayDescription}
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-gray-500">
|
||||
{group.layouts.length} layout
|
||||
{group.layouts.length !== 1 ? "s" : ""}
|
||||
</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">
|
||||
{group.settings.description}
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-gray-500">
|
||||
{group.layouts.length} layout
|
||||
{group.layouts.length !== 1 ? "s" : ""}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))
|
||||
</Card>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Card
|
||||
className="cursor-pointer hover:shadow-md transition-all border-blue-500 duration-200 group"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue