nexus/ui/src/components/BrandKitResult.tsx

217 lines
7.1 KiB
TypeScript

import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
export type BrandKitBundle = {
type: "brand-kit-bundle";
spec: {
name: string;
tagline: string;
primaryColor: string;
secondaryColor: string;
fontStyle: string;
industry: string;
};
logoSvgBase64: string;
avatarPngs: Record<string, string>; // "512"|"256"|"128"|"64"|"32" -> base64
socialImages: Record<string, string>; // "twitter-profile" etc -> base64
signatureHtml: string;
letterheadHtml: string;
guidelinesPdfBase64: string;
zipBase64: string;
};
const AVATAR_SIZES = ["512", "256", "128", "64", "32"] as const;
const SOCIAL_PLATFORM_LABELS: Record<string, string> = {
"twitter-profile": "Twitter/X Profile",
"linkedin-profile": "LinkedIn Profile",
"linkedin-banner": "LinkedIn Banner",
"instagram-profile": "Instagram Profile",
"facebook-cover": "Facebook Cover",
};
interface BrandKitResultProps {
bundle: BrandKitBundle;
}
export function BrandKitResult({ bundle }: BrandKitResultProps) {
function downloadBlob(base64: string, mimeType: string, filename: string) {
const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
const blob = new Blob([bytes], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
function handleDownloadZip() {
downloadBlob(bundle.zipBase64, "application/zip", `${bundle.spec.name || "brand-kit"}.zip`);
}
function handleDownloadGuidelines() {
downloadBlob(
bundle.guidelinesPdfBase64,
"application/pdf",
`${bundle.spec.name || "brand"}-guidelines.pdf`,
);
}
const socialEntries = Object.entries(bundle.socialImages);
return (
<div className="flex flex-col gap-6">
{/* Brand spec summary */}
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm font-semibold">{bundle.spec.name}</span>
{bundle.spec.tagline && (
<span className="text-sm text-muted-foreground"> {bundle.spec.tagline}</span>
)}
<Badge variant="outline">{bundle.spec.industry}</Badge>
<Badge variant="outline">{bundle.spec.fontStyle}</Badge>
<span
className="inline-flex size-5 rounded-full border border-border"
style={{ backgroundColor: bundle.spec.primaryColor }}
title={`Primary: ${bundle.spec.primaryColor}`}
/>
<span
className="inline-flex size-5 rounded-full border border-border"
style={{ backgroundColor: bundle.spec.secondaryColor }}
title={`Secondary: ${bundle.spec.secondaryColor}`}
/>
</div>
{/* Logo */}
<Card>
<CardHeader>
<CardTitle className="text-base">Logo</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-center rounded-lg bg-muted p-6">
<img
src={`data:image/svg+xml;base64,${bundle.logoSvgBase64}`}
alt={`${bundle.spec.name} logo`}
className="max-h-32 max-w-full object-contain"
/>
</div>
</CardContent>
</Card>
{/* Avatars */}
<Card>
<CardHeader>
<CardTitle className="text-base">Avatars</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap items-end gap-4">
{AVATAR_SIZES.map((size) => {
const pngBase64 = bundle.avatarPngs[size];
if (!pngBase64) return null;
const px = Number(size);
const displaySize = Math.min(px, 96);
return (
<div key={size} className="flex flex-col items-center gap-1">
<img
src={`data:image/png;base64,${pngBase64}`}
alt={`${size}px avatar`}
width={displaySize}
height={displaySize}
className="rounded-full border border-border object-contain"
/>
<span className="text-xs text-muted-foreground">{size}px</span>
</div>
);
})}
</div>
</CardContent>
</Card>
{/* Social images */}
{socialEntries.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="text-base">Social Images</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3">
{socialEntries.map(([platform, pngBase64]) => (
<div key={platform} className="flex flex-col gap-1">
<div className="rounded-lg bg-muted overflow-hidden">
<img
src={`data:image/png;base64,${pngBase64}`}
alt={`${platform} image`}
className="w-full object-cover"
/>
</div>
<span className="text-xs text-muted-foreground text-center">
{SOCIAL_PLATFORM_LABELS[platform] ?? platform}
</span>
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* Templates */}
<Card>
<CardHeader>
<CardTitle className="text-base">Templates</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<span className="text-sm font-medium">Email Signature</span>
<div className="rounded-lg border border-border overflow-hidden">
<iframe
title="Email Signature Preview"
srcDoc={bundle.signatureHtml}
sandbox="allow-same-origin"
className="w-full h-32 bg-white"
/>
</div>
</div>
<div className="flex flex-col gap-2">
<span className="text-sm font-medium">Letterhead</span>
<div className="rounded-lg border border-border overflow-hidden">
<iframe
title="Letterhead Preview"
srcDoc={bundle.letterheadHtml}
sandbox="allow-same-origin"
className="w-full h-48 bg-white"
/>
</div>
</div>
</CardContent>
</Card>
{/* Guidelines */}
<Card>
<CardHeader>
<CardTitle className="text-base">Brand Guidelines</CardTitle>
</CardHeader>
<CardContent>
<Button
type="button"
variant="secondary"
className="w-full"
onClick={handleDownloadGuidelines}
>
Download Brand Guidelines PDF
</Button>
</CardContent>
</Card>
{/* Download Brand Kit (ZIP) */}
<Button
type="button"
size="lg"
className="w-full"
onClick={handleDownloadZip}
>
Download Brand Kit (ZIP)
</Button>
</div>
);
}