ppt-tool/frontend/app/presentation-templates/modern/ImageAndDescriptionLayout.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

152 lines
5.9 KiB
TypeScript

import React from "react";
import * as z from "zod";
import { ImageSchema } from "../defaultSchemes";
export const layoutId = "image-and-description";
export const layoutName = "Image And Description";
export const layoutDescription =
"A slide layout with a title, a description, and an image.";
const imageWithDescriptionSlideSchema = z.object({
title: z.string().min(3).max(30).default("Image With Description").meta({
description: "Main title of the slide",
}),
content: z
.string()
.min(25)
.max(300)
.default(
"In the presentation session, the background/introduction can be filled with information that is arranged systematically and effectively with respect to an interesting topic to be used as material for discussion at the opening of the presentation session. The introduction can provide a general overview for those who are listening to your presentation so that the key words on the topic of discussion are emphasized during this background/introductory presentation session.",
)
.meta({
description: "Main content text describing the company or topic",
}),
image: ImageSchema.default({
__image_url__:
"https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?q=80&w=1600&auto=format&fit=crop",
__image_prompt__: "Abstract business background",
}).meta({
description:
"Optional supporting image for the slide (building, office, etc.)",
}),
});
export const Schema = imageWithDescriptionSlideSchema;
export type ImageWithDescriptionSlideData = z.infer<typeof imageWithDescriptionSlideSchema>;
interface ImageWithDescriptionSlideLayoutProps {
data?: Partial<ImageWithDescriptionSlideData>;
}
const ImageWithDescriptionSlideLayout: React.FC<ImageWithDescriptionSlideLayoutProps> = ({
data: slideData,
}) => {
return (
<>
{/* Import fonts */}
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap"
rel="stylesheet"
/>
<div
className="w-full rounded-sm max-w-[1280px] shadow-lg aspect-video relative z-20 mx-auto overflow-hidden"
style={{
fontFamily: "var(--heading-font-family,Montserrat)",
backgroundColor: "var(--background-color, #FFFFFF)",
}}
>
{/* Header */}
{((slideData as any)?.__companyName__ || (slideData as any)?._logo_url__) && (
<div className="absolute top-0 left-0 right-0 px-8 sm:px-12 lg:px-20 pt-4">
<div className="flex items-center gap-4">
<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-sm sm:text-base font-semibold" style={{ color: 'var(--background-text, #111827)' }}>
{(slideData as any)?.__companyName__ || 'Company Name'}
</span>}
</div>
</div>
</div>
)}
{/* Main content area */}
<div className="flex h-full px-16 pb-16">
{/* Left side - Image */}
<div className="flex-1 pr-16 flex items-center pt-8">
<div className="w-full h-96 overflow-hidden">
{slideData?.image ? (
<img
src={slideData.image.__image_url__}
alt={slideData.image.__image_prompt__}
className="w-full h-full object-cover"
/>
) : (
/* Default building facade */
<div className="w-full h-full bg-gray-200 relative">
{/* Building structure simulation */}
<div className="absolute inset-0 bg-gray-300"></div>
{/* Horizontal lines (building floors) */}
<div className="absolute inset-0">
{[...Array(12)].map((_, i) => (
<div
key={i}
className="absolute w-full border-t border-gray-400 opacity-60"
style={{ top: `${(i + 1) * 8}%` }}
></div>
))}
</div>
{/* Vertical lines (building columns) */}
<div className="absolute inset-0">
{[...Array(6)].map((_, i) => (
<div
key={i}
className="absolute h-full border-l border-gray-400 opacity-40"
style={{ left: `${(i + 1) * 16}%` }}
></div>
))}
</div>
{/* Windows */}
<div className="absolute inset-0 grid grid-cols-4 gap-2 p-4">
{[...Array(32)].map((_, i) => (
<div
key={i}
className="bg-blue-100 opacity-60 rounded-sm border border-gray-300"
></div>
))}
</div>
{/* Building edge highlight */}
<div className="absolute right-0 top-0 w-1 h-full bg-white opacity-80"></div>
</div>
)}
</div>
</div>
{/* Right side - Content */}
<div className="flex-1 pl-16 flex flex-col justify-center">
{slideData?.title && (
<h2 className="text-5xl font-bold mb-12 leading-tight" style={{ color: 'var(--background-text, #1E4CD9)' }}>
{slideData?.title}
</h2>
)}
{slideData?.content && (
<div className="text-lg leading-relaxed font-normal max-w-lg" style={{ color: 'var(--background-text, #334155)' }}>
{slideData?.content}
</div>
)}
</div>
</div>
</div>
</>
);
};
export default ImageWithDescriptionSlideLayout;