ppt-tool/frontend/app/presentation-templates/standard/VisualMetricsSlideLayout.tsx
Vadym Samoilenko cf21ba4516 Phase 1-2: Foundation + Admin Panel & Client Management
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>
2026-02-26 15:37:17 +00:00

191 lines
No EOL
9.4 KiB
TypeScript

import React from 'react'
// charts trimmed to donut only (inline SVG)
import * as z from "zod";
const ImageSchema = z.object({
__image_url__: z.string().url().default("https://images.unsplash.com/photo-1522199755839-a2bacb67c546?q=80&w=1200&auto=format&fit=crop").meta({
description: "URL to image",
}),
__image_prompt__: z.string().min(10).max(150).default("Abstract light background for slide header area").meta({
description: "Prompt used to generate the image. Max 30 words",
}),
})
const IconSchema = z.object({
__icon_url__: z.string().url().default("https://cdn.jsdelivr.net/gh/tabler/tabler-icons/icons/placeholder.svg").meta({
description: "URL to icon",
}),
__icon_query__: z.string().min(3).max(30).default("progress ring placeholder").meta({
description: "Query used to search the icon. Max 3 words",
}),
})
const layoutId = "visual-metrics"
const layoutName = "Visual Metrics"
const layoutDescription = "A slide with a header bar, numeric marker, title, description, and grid of cards with headings, circular metrics, and texts"
const CardSchema = z.object({
title: z.string().min(6).max(18).default("Research").meta({
description: "Card heading. Max 3 words",
}),
value: z.number().min(0).max(9999).default(67).meta({
description: "Numeric value to display",
}),
unit: z.string().min(0).max(2).default("K").meta({
description: "Unit for the value, e.g., %, K, M",
}),
description: z.string().min(1).max(50).default("Lorem ipsum dolor sit amet, consectetur").meta({
description: "Card supporting text. Max 50 characters",
}),
}).default({
title: "Research",
value: 67,
unit: "K",
description: "Lorem ipsum dolor sit amet, consectetur",
})
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(20).max(68).default("Our Vision And Strategy For Excellence").meta({
description: "Main slide title. Max 10 words",
}),
description: z.string().min(70).max(200).default("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation").meta({
description: "Lead paragraph. Max 35 words",
}),
cards: z.array(CardSchema).min(1).max(4).default([
{
title: "Research",
value: 6700,
unit: "K",
description: "Lorem ipsum dolor sit amet, consectetur",
},
{
title: "Development",
value: 80,
unit: "K",
description: "Lorem ipsum dolor sit amet, consectetur",
},
{
title: "Research",
value: 67,
unit: "%",
description: "Lorem ipsum dolor sit amet, consectetur",
},
{ title: "Development", value: 80, unit: "K", description: "Lorem ipsum dolor sit amet, consectetur" },
]).meta({
description: "Grid of cards with heading, circular metric, and text",
}),
chartPalette: z.array(z.string().min(4).max(20)).min(2).max(6).default(["var(--primary-color, #1B8C2D)", "var(--background-text, #E5E7EB)", "#f59e0b", "#3b82f6"]).meta({
description: "Palette for charts",
}),
}).default({
topBar: { marker: "2" },
title: "Our Vision And Strategy For Excellence",
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation",
cards: [
{ title: "Research", value: 67, unit: "K", description: "Lorem ipsum dolor sit amet, consectetur" },
{ title: "Ops", value: 42, unit: "M", description: "Lorem ipsum dolor sit amet, consectetur" },
{ title: "Efficiency", value: 67, unit: "%", description: "Lorem ipsum dolor sit amet, consectetur" },
{ title: "Development", value: 80, unit: "K", description: "Lorem ipsum dolor sit amet, consectetur" },
],
chartPalette: ["var(--primary-color, #1B8C2D)", "var(--background-text, #E5E7EB)", "#f59e0b", "#3b82f6"],
})
type SlideData = z.infer<typeof Schema>
interface SlideLayoutProps {
data?: Partial<SlideData>
}
const dynamicSlideLayout: React.FC<SlideLayoutProps> = ({ data: slideData }) => {
const cards = slideData?.cards || []
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-bold" 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-bold mt-2" style={{ color: 'var(--background-text, #111827)' }}>
{slideData?.title}
</h1>
<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-4 gap-8">
{cards.map((card, idx) => {
const radius = 80
const circumference = 2 * Math.PI * radius
const dasharray = circumference
return (
<div key={idx} className="rounded-xl border shadow-[0_24px_60px_rgba(0,0,0,0.08)]" style={{ backgroundColor: 'var(--card-color, #FFFFFF)', borderColor: 'var(--stroke, #E5E7EB)' }}>
<div className="px-8 pt-8 pb-7 flex flex-col items-center text-center">
<h3 className="text-[24px] leading-tight font-bold" style={{ color: 'var(--background-text, #111827)' }}>{card.title}</h3>
<div className="mt-6 relative w-[180px] h-[180px]">
<svg viewBox="0 0 200 200" className="w-full h-full">
<circle cx="100" cy="100" r="80" fill="none" stroke="var(--background-text, #E5E7EB)" strokeWidth="16"></circle>
<circle
cx="100"
cy="100"
r="80"
fill="none"
stroke={'var(--primary-color, #1B8C2D)'}
strokeWidth="16"
strokeLinecap="round"
strokeDasharray={dasharray}
strokeDashoffset={0}
transform="rotate(-90 100 100)"
></circle>
<circle cx="100" cy="100" r="62" fill="none" stroke="var(--stroke, #E5E7EB)" strokeWidth="10"></circle>
</svg>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-[32px] font-extrabold " style={{ color: 'var(--background-text, #111827)' }}>{card.value}{card.unit}</span>
</div>
</div>
<p className="mt-6 text-[16px] leading-[1.6] " style={{ color: 'var(--background-text, #6B7280)' }}>
{card.description}
</p>
</div>
</div>
)
})}
</div>
</div>
</div>
</>
)
}
export { Schema, layoutId, layoutName, layoutDescription }
export default dynamicSlideLayout