Skip to content

Commit 3004554

Browse files
Merge pull request #32 from developerfred/brand-kit
feat(community): add brand kit section
2 parents 7da5b00 + 07a918e commit 3004554

File tree

2 files changed

+225
-1
lines changed

2 files changed

+225
-1
lines changed

.github/workflows/version-bump.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
- name: Checkout repository
1717
uses: actions/checkout@v4
1818
with:
19-
fetch-depth: 0 # Required to get all tags
19+
fetch-depth: 0
2020

2121
- name: Setup Node.js
2222
uses: actions/setup-node@v4

src/app/brand-kit/page.tsx

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/* eslint-disable @typescript-eslint/ban-ts-comment, react/no-unescaped-entities */
2+
// @ts-nocheck
3+
'use client';
4+
5+
import { useState, useRef, type ComponentType } from 'react';
6+
import { Download, Copy, Check, XCircle, ShieldCheck } from 'lucide-react';
7+
8+
9+
const generateSvgString = (type: 'complete' | 'symbol', theme: 'light' | 'dark'): string => {
10+
const isDark = theme === 'dark';
11+
const primaryColor = isDark ? '#FFFFFF' : '#1e293b';
12+
const secondaryColor = isDark ? '#cbd5e1' : '#64748b';
13+
14+
if (type === 'symbol') {
15+
return `<svg viewBox="0 0 130 100" xmlns="http://www.w3.org/2000/svg"><g><path d="M 5 20 L 40 50 L 5 80 Z" fill="#E6007A"/><text x="60" y="80" font-family="Inter, sans-serif" font-size="80" font-weight="800" fill="${primaryColor}">P</text><circle cx="88" cy="50" r="10" fill="#E6007A" /></g></svg>`;
16+
}
17+
18+
return `<svg viewBox="0 0 400 100" xmlns="http://www.w3.org/2000/svg"><g id="logomark"><path d="M 5 20 L 40 50 L 5 80 Z" fill="#E6007A"/><text x="60" y="80" font-family="Inter, sans-serif" font-size="80" font-weight="800" fill="${primaryColor}">P</text><circle cx="88" cy="50" r="10" fill="#E6007A" /></g><g id="logotype" transform="translate(170, 0)"><text x="0" y="55" font-family="Inter, sans-serif" font-size="40" font-weight="800" fill="${primaryColor}">PAPI</text><text x="0" y="85" font-family="Inter, sans-serif" font-size="18" font-weight="500" fill="${secondaryColor}" letter-spacing="0.2em">SIMULATOR</text></g></svg>`;
19+
};
20+
21+
22+
23+
type LogoProps = { theme?: 'light' | 'dark' };
24+
25+
const LogoCompleto: React.FC<LogoProps> = ({ theme = 'light' }) => {
26+
const isDark = theme === 'dark';
27+
return (
28+
<svg viewBox="0 0 400 100" xmlns="http://www.w3.org/2000/svg" aria-label={`PAPI Simulator Primary Logo ${isDark ? 'White Version' : ''}`}>
29+
<g id="logomark"><path d="M 5 20 L 40 50 L 5 80 Z" fill="#E6007A" /><text x="60" y="80" fontFamily="Inter, sans-serif" fontSize="80" fontWeight="800" fill={isDark ? "#FFFFFF" : "#1e293b"}>P</text><circle cx="88" cy="50" r="10" fill="#E6007A" /></g>
30+
<g id="logotype" transform="translate(170, 0)"><text x="0" y="55" fontFamily="Inter, sans-serif" fontSize="40" fontWeight="800" fill={isDark ? "#FFFFFF" : "#1e293b"}>PAPI</text><text x="0" y="85" fontFamily="Inter, sans-serif" fontSize="18" fontWeight="500" fill={isDark ? "#cbd5e1" : "#64748b"} letterSpacing="0.2em">SIMULATOR</text></g>
31+
</svg>
32+
);
33+
};
34+
35+
const Simbolo: React.FC<LogoProps> = ({ theme = 'light' }) => {
36+
const isDark = theme === 'dark';
37+
return (
38+
<svg viewBox="0 0 130 100" xmlns="http://www.w3.org/2000/svg" aria-label={`PAPI Simulator Symbol ${isDark ? 'White Version' : ''}`}>
39+
<g><path d="M 5 20 L 40 50 L 5 80 Z" fill="#E6007A" /><text x="60" y="80" fontFamily="Inter, sans-serif" fontSize="80" fontWeight="800" fill={isDark ? "#FFFFFF" : "#1e293b"}>P</text><circle cx="88" cy="50" r="10" fill="#E6007A" /></g>
40+
</svg>
41+
);
42+
};
43+
44+
45+
interface LogoCardProps {
46+
title: string;
47+
LogoComponent: ComponentType<LogoProps>;
48+
svgContent: string;
49+
fileName: string;
50+
isDark?: boolean;
51+
}
52+
53+
const LogoCard: React.FC<LogoCardProps> = ({ title, LogoComponent, svgContent, fileName, isDark = false }) => {
54+
const [copied, setCopied] = useState(false);
55+
const canvasRef = useRef<HTMLCanvasElement>(null);
56+
const theme = isDark ? 'dark' : 'light';
57+
58+
const handleCopy = () => {
59+
navigator.clipboard.writeText(svgContent.trim());
60+
setCopied(true);
61+
setTimeout(() => setCopied(false), 2000);
62+
};
63+
64+
const handleDownload = (format: 'svg' | 'png', size?: number) => {
65+
let url: string | null = null;
66+
try {
67+
if (format === 'svg') {
68+
const blob = new Blob([svgContent], { type: 'image/svg+xml' });
69+
url = URL.createObjectURL(blob);
70+
const a = document.createElement('a');
71+
a.href = url;
72+
a.download = `${fileName}.svg`;
73+
document.body.appendChild(a);
74+
a.click();
75+
document.body.removeChild(a);
76+
} else if (format === 'png' && size) {
77+
const canvas = canvasRef.current;
78+
const ctx = canvas?.getContext('2d');
79+
if (!ctx || !canvas) {
80+
console.error("Canvas context is not available.");
81+
return;
82+
}
83+
const img = new Image();
84+
const svgBlob = new Blob([svgContent], { type: 'image/svg+xml' });
85+
url = URL.createObjectURL(svgBlob);
86+
87+
img.onload = () => {
88+
// Validate image dimensions before calculations
89+
if (img.width > 0 && img.height > 0) {
90+
const aspectRatio = img.width / img.height;
91+
canvas.width = size;
92+
canvas.height = size / aspectRatio;
93+
ctx.clearRect(0, 0, canvas.width, canvas.height);
94+
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
95+
const pngUrl = canvas.toDataURL('image/png');
96+
const a = document.createElement('a');
97+
a.href = pngUrl;
98+
a.download = `${fileName}_${size}px.png`;
99+
document.body.appendChild(a);
100+
a.click();
101+
document.body.removeChild(a);
102+
} else {
103+
console.error("Failed to process SVG: Invalid image dimensions (width or height is zero).");
104+
}
105+
};
106+
107+
img.onerror = () => {
108+
console.error("Failed to load SVG image for canvas conversion.");
109+
};
110+
111+
img.src = url;
112+
}
113+
} catch (error) {
114+
console.error("An error occurred during the download process:", error);
115+
} finally {
116+
if (url) {
117+
URL.revokeObjectURL(url);
118+
}
119+
}
120+
};
121+
122+
return (
123+
<div className="border border-slate-200 dark:border-slate-700 rounded-2xl overflow-hidden shadow-sm bg-white dark:bg-slate-800">
124+
<div className={`p-8 sm:p-12 flex items-center justify-center ${isDark ? 'bg-slate-900' : 'bg-slate-50'}`}>
125+
<div className="w-48 h-24"> <LogoComponent theme={theme} /> </div>
126+
</div>
127+
<div className="p-4">
128+
<h3 className="font-semibold text-slate-800 dark:text-slate-200">{title}</h3>
129+
<div className="mt-4 flex flex-wrap gap-2">
130+
<button onClick={() => handleDownload('svg')} className="flex items-center gap-2 text-sm bg-slate-100 dark:bg-slate-700 hover:bg-slate-200 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-300 font-medium py-2 px-3 rounded-lg transition-colors"> <Download size={16} /> SVG </button>
131+
<button onClick={() => handleDownload('png', 512)} className="flex items-center gap-2 text-sm bg-slate-100 dark:bg-slate-700 hover:bg-slate-200 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-300 font-medium py-2 px-3 rounded-lg transition-colors"> <Download size={16} /> PNG (512px) </button>
132+
<button onClick={() => handleDownload('png', 1024)} className="flex items-center gap-2 text-sm bg-slate-100 dark:bg-slate-700 hover:bg-slate-200 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-300 font-medium py-2 px-3 rounded-lg transition-colors"> <Download size={16} /> PNG (1024px) </button>
133+
<button onClick={handleCopy} className={`flex items-center gap-2 text-sm ${copied ? 'bg-green-100 text-green-700' : 'bg-slate-100 dark:bg-slate-700 hover:bg-slate-200 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-300'} font-medium py-2 px-3 rounded-lg transition-colors`}> {copied ? <Check size={16} /> : <Copy size={16} />} {copied ? 'Copied!' : 'Copy SVG'} </button>
134+
</div>
135+
</div>
136+
<canvas ref={canvasRef} style={{ display: 'none' }} />
137+
</div>
138+
);
139+
};
140+
141+
142+
const ColorPalette = () => {
143+
const colors = [
144+
{ name: 'Polkadot Pink', hex: '#E6007A', text: 'white' }, { name: 'Slate Dark', hex: '#1e293b', text: 'white' },
145+
{ name: 'Slate Medium', hex: '#64748b', text: 'white' }, { name: 'Slate Light', hex: '#f8fafc', text: 'black' },
146+
];
147+
const [copiedHex, setCopiedHex] = useState('');
148+
const handleCopy = (hex: string) => { navigator.clipboard.writeText(hex); setCopiedHex(hex); setTimeout(() => setCopiedHex(''), 2000); };
149+
return (
150+
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-6">
151+
{colors.map(color => (
152+
<div key={color.name} className="rounded-2xl shadow-sm overflow-hidden border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800">
153+
<div style={{ backgroundColor: color.hex, color: `var(--color-${color.text})` }} className="h-24 sm:h-32 p-4 flex flex-col justify-end"> <h3 className="font-bold text-lg">{color.name}</h3> </div>
154+
<div className="p-4 flex justify-between items-center"> <span className="font-mono text-slate-600 dark:text-slate-400">{color.hex}</span> <button onClick={() => handleCopy(color.hex)} title={`Copy ${color.hex}`}>{copiedHex === color.hex ? <Check size={18} className="text-green-600" /> : <Copy size={18} className="text-slate-400 hover:text-slate-600" />}</button> </div>
155+
</div>
156+
))}
157+
</div>
158+
);
159+
};
160+
161+
162+
export default function BrandKitPage() {
163+
const logoAssets: Omit<LogoCardProps, 'svgContent'>[] = [
164+
{ title: 'Primary Logo', LogoComponent: LogoCompleto, fileName: 'papi_simulator_logo_primary' },
165+
{ title: 'Primary Logo (White Version)', LogoComponent: LogoCompleto, fileName: 'papi_simulator_logo_primary_white', isDark: true },
166+
{ title: 'Symbol (Icon)', LogoComponent: Simbolo, fileName: 'papi_simulator_symbol' },
167+
{ title: 'Symbol (White Version)', LogoComponent: Simbolo, fileName: 'papi_simulator_symbol_white', isDark: true },
168+
];
169+
170+
return (
171+
<main className="bg-slate-50 dark:bg-slate-900 min-h-screen text-slate-800 dark:text-slate-200 font-sans">
172+
<div className="container mx-auto px-4 py-12 sm:py-16 md:py-24">
173+
<header className="text-center max-w-3xl mx-auto mb-16">
174+
<div className="w-48 mx-auto mb-6 text-slate-900 dark:text-white"> <Simbolo theme="light" /> </div>
175+
<h1 className="text-4xl sm:text-5xl font-extrabold text-slate-900 dark:text-slate-50 tracking-tight">PAPI Simulator Brand Kit</h1>
176+
<p className="mt-4 text-lg text-slate-600 dark:text-slate-400"> Welcome to the PAPI Simulator brand resource hub. Here you'll find our logos, colors, and guidelines to ensure our brand is represented consistently. </p>
177+
</header>
178+
179+
<section className="mb-16">
180+
<h2 className="text-3xl font-bold text-slate-900 dark:text-slate-100 mb-8 border-b border-slate-200 dark:border-slate-700 pb-4">Logos</h2>
181+
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
182+
{logoAssets.map(asset => {
183+
const theme = asset.isDark ? 'dark' : 'light';
184+
const type = asset.title.includes('Symbol') ? 'symbol' : 'complete';
185+
return <LogoCard key={asset.fileName} {...asset} svgContent={generateSvgString(type, theme)} />;
186+
})}
187+
</div>
188+
</section>
189+
190+
<section className="mb-16">
191+
<h2 className="text-3xl font-bold text-slate-900 dark:text-slate-100 mb-8 border-b border-slate-200 dark:border-slate-700 pb-4">Color Palette</h2>
192+
<ColorPalette />
193+
</section>
194+
195+
<section className="mb-16">
196+
<h2 className="text-3xl font-bold text-slate-900 dark:text-slate-100 mb-8 border-b border-slate-200 dark:border-slate-700 pb-4">Typography</h2>
197+
<div className="bg-white dark:bg-slate-800 p-8 rounded-2xl border border-slate-200 dark:border-slate-700 shadow-sm">
198+
<h3 className="text-2xl font-bold text-slate-800 dark:text-slate-200">Inter</h3>
199+
<p className="mt-2 text-slate-600 dark:text-slate-400 mb-6">Our visual identity uses the Inter font family, known for its excellent legibility in user interfaces. It's an open-source font and can be obtained for free from Google Fonts.</p>
200+
<div className="flex items-baseline gap-x-8 gap-y-4 flex-wrap text-slate-700 dark:text-slate-300"> <div className="font-medium">Regular</div> <div className="font-semibold">SemiBold</div> <div className="font-bold">Bold</div> <div className="font-extrabold">ExtraBold</div> </div>
201+
<a href="https://fonts.google.com/specimen/Inter" target="_blank" rel="noopener noreferrer" className="inline-block mt-6 bg-slate-900 dark:bg-slate-100 text-white dark:text-slate-900 font-semibold py-2 px-4 rounded-lg hover:bg-slate-700 dark:hover:bg-slate-300 transition-colors"> Get Inter on Google Fonts </a>
202+
</div>
203+
</section>
204+
205+
<section>
206+
<h2 className="text-3xl font-bold text-slate-900 dark:text-slate-100 mb-8 border-b border-slate-200 dark:border-slate-700 pb-4">Usage Guidelines</h2>
207+
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
208+
<div className="bg-white dark:bg-slate-800 p-8 rounded-2xl border border-slate-200 dark:border-slate-700 shadow-sm">
209+
<div className="flex items-center gap-3 mb-4"> <div className="bg-green-100 dark:bg-green-900/30 p-2 rounded-full"> <ShieldCheck className="text-green-600 dark:text-green-400" size={24} /> </div> <h3 className="text-2xl font-bold text-slate-800 dark:text-slate-200">Do's</h3> </div>
210+
<ul className="space-y-3 text-slate-600 dark:text-slate-400 list-disc list-inside"> <li>Use the primary logo whenever space permits.</li> <li>Maintain a clear space around the logo (at least 50% of the logo's height).</li> <li>Use the white version on dark backgrounds to ensure contrast.</li> <li>Always maintain the original proportions of the logo.</li> </ul>
211+
</div>
212+
<div className="bg-white dark:bg-slate-800 p-8 rounded-2xl border border-slate-200 dark:border-slate-700 shadow-sm">
213+
<div className="flex items-center gap-3 mb-4"> <div className="bg-red-100 dark:bg-red-900/30 p-2 rounded-full"> <XCircle className="text-red-600 dark:text-red-400" size={24} /> </div> <h3 className="text-2xl font-bold text-slate-800 dark:text-slate-200">Don'ts</h3> </div>
214+
<ul className="space-y-3 text-slate-600 dark:text-slate-400 list-disc list-inside"> <li>Do not distort or alter the logo's proportions.</li> <li>Do not change the logo's colors.</li> <li>Do not add shadows, gradients, or other visual effects.</li> <li>Do not use the logo on backgrounds that compromise its legibility.</li> </ul>
215+
</div>
216+
</div>
217+
</section>
218+
</div>
219+
<footer className="text-center py-8 border-t border-slate-200 dark:border-slate-700">
220+
<p className="text-slate-500 dark:text-slate-400">© {new Date().getFullYear()} PAPI Simulator. All rights reserved.</p>
221+
</footer>
222+
</main>
223+
);
224+
}

0 commit comments

Comments
 (0)