Phase 1 (Foundation): - Project restructure (presenton-main → backend/ + frontend/) - Database schema (8 new models, Alembic config, seed script) - Auth (Azure AD SSO + dev bypass, JWT sessions, AuthMiddleware) - RBAC (access_service, rbac_middleware, admin routers) - Audit logging (fire-and-forget, AuditMiddleware, admin router) - i18n (react-i18next with 5 namespace files) Phase 2 (Admin Panel & Client Management): - Admin panel shell (sidebar layout, role guard, 12 pages) - Redux admin slice with 18 async thunks - User management (role changes, deactivation) - Client management (CRUD, brand config, team management) - Brand config editor (colors, fonts, logos, voice rules) - Master deck upload & parser (PPTX → HTML → React pipeline) - Audit log viewer with filters and CSV/JSON export Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
178 lines
5.4 KiB
TypeScript
178 lines
5.4 KiB
TypeScript
import React from "react"
|
|
import * as z from "zod"
|
|
|
|
const layoutId = "table-of-contents-layout"
|
|
const layoutName = "Table Of Contents"
|
|
const layoutDescription = "Header with brand marker, title, optional description, and a two-column table of contents list"
|
|
|
|
const ToCItemSchema = z
|
|
.object({
|
|
title: z.string().min(4).max(50).default("Introduction").meta({
|
|
description: "Section title. Max 50 characters",
|
|
}),
|
|
})
|
|
.default({
|
|
title: "Introduction",
|
|
})
|
|
|
|
const Schema = z
|
|
.object({
|
|
topBar: z
|
|
.object({
|
|
|
|
marker: z.string().min(1).max(3).default("2").meta({
|
|
description: "Numeric marker on the top bar. Up to 3 digits",
|
|
}),
|
|
})
|
|
.default({ marker: "2" }),
|
|
|
|
title: z
|
|
.string()
|
|
.min(12)
|
|
.max(68)
|
|
.default("Table Of Contents")
|
|
.meta({ description: "Main slide title. Max 10 words" }),
|
|
|
|
description: z
|
|
.string()
|
|
.min(0)
|
|
.max(200)
|
|
.default(
|
|
"Use this as a quick guide to navigate the presentation sections."
|
|
)
|
|
.meta({ description: "Lead paragraph. Optional. Max 35 words" }),
|
|
|
|
items: z
|
|
.array(ToCItemSchema)
|
|
.min(3)
|
|
.max(10)
|
|
.default([
|
|
{ title: "Introduction" },
|
|
{ title: "Problem Statement" },
|
|
{ title: "Solution" },
|
|
{ title: "Market" },
|
|
{ title: "Business Model" },
|
|
{ title: "Roadmap" },
|
|
{ title: "Team" },
|
|
{ title: "Go-To-Market" },
|
|
{ title: "Financials" },
|
|
{ title: "Ask" },
|
|
])
|
|
.meta({ description: "List of contents (3-10)" }),
|
|
})
|
|
.default({
|
|
topBar: { marker: "2" },
|
|
title: "Table Of Contents",
|
|
description:
|
|
"Use this as a quick guide to navigate the presentation sections.",
|
|
items: [
|
|
{ title: "Introduction" },
|
|
{ title: "Problem Statement" },
|
|
{ title: "Solution" },
|
|
{ title: "Market" },
|
|
{ title: "Business Model" },
|
|
{ title: "Roadmap" },
|
|
{ title: "Team" },
|
|
{ title: "Go-To-Market" },
|
|
{ title: "Financials" },
|
|
{ title: "Ask" },
|
|
],
|
|
})
|
|
|
|
type SlideData = z.infer<typeof Schema>
|
|
|
|
interface SlideLayoutProps {
|
|
data?: Partial<SlideData>
|
|
}
|
|
|
|
const dynamicSlideLayout: React.FC<SlideLayoutProps> = ({ data: slideData }) => {
|
|
const items = slideData?.items || []
|
|
|
|
return (
|
|
<>
|
|
<link
|
|
href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500;600;700&display=swap"
|
|
rel="stylesheet"
|
|
/>
|
|
|
|
<div
|
|
className=" w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video relative z-20 mx-auto overflow-hidden"
|
|
style={{
|
|
fontFamily: "var(--heading-font-family,Playfair Display)",
|
|
backgroundColor: "var(--background-color, #FFFFFF)",
|
|
}}
|
|
>
|
|
<div className="px-12 pt-6 pb-2">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-6">
|
|
<div className="flex items-center gap-1">
|
|
|
|
{(slideData as any)?._logo_url__ && <img src={(slideData as any)?._logo_url__} alt="logo" className="w-6 h-6" />}
|
|
{(slideData as any)?.__companyName__ && <span className="text-[18px] font-semibold" style={{ color: 'var(--background-text, #111827)' }}>{(slideData as any)?.__companyName__ || "Pitchdeck"}</span>}
|
|
</div>
|
|
<div
|
|
className="h-[2px] w-[220px]"
|
|
style={{ backgroundColor: "var(--background-text, #111827)" }}
|
|
></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="px-12">
|
|
<h1
|
|
className="text-[64px] leading-[1.05] tracking-tight font-semibold mt-2"
|
|
style={{ color: "var(--background-text, #111827)" }}
|
|
>
|
|
{slideData?.title}
|
|
</h1>
|
|
{slideData?.description && (
|
|
<p
|
|
className="mt-5 text-[16px] leading-[1.6] max-w-[1020px] "
|
|
style={{ color: "var(--background-text, #6B7280)" }}
|
|
>
|
|
{slideData?.description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="px-10 mt-10">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
{items.map((item, idx) => (
|
|
<div
|
|
key={idx}
|
|
className="rounded-sm border shadow-[0_8px_24px_rgba(0,0,0,0.06)] px-4 py-3 flex items-center gap-4"
|
|
style={{
|
|
backgroundColor: "var(--card-color, #FFFFFF)",
|
|
borderColor: "var(--stroke, #E5E7EB)",
|
|
}}
|
|
>
|
|
<div
|
|
className="w-8 h-8 rounded-full flex items-center justify-center text-[16px] font-semibold"
|
|
style={{
|
|
border: "2px solid var(--primary-color, #1B8C2D)",
|
|
color: "var(--background-text, #111827)",
|
|
}}
|
|
>
|
|
{idx + 1}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div
|
|
className="text-[18px] leading-tight font-semibold truncate"
|
|
style={{ color: "var(--background-text, #111827)" }}
|
|
>
|
|
{item.title}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export { Schema, layoutId, layoutName, layoutDescription }
|
|
export default dynamicSlideLayout
|
|
|
|
|