Add WhyRating Templates - brand identity system
Next.js app showcasing WhyRating brand guidelines with interactive tabs for colors, typography, proportions, logos, voice, downloads, and AI context. Includes email templates (headers, signatures, CTAs) and presentation component. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
118
whyrating-templates/src/app/api/generate-png/route.ts
Normal file
118
whyrating-templates/src/app/api/generate-png/route.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import puppeteer from 'puppeteer';
|
||||
import sharp from 'sharp';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
|
||||
const variant = searchParams.get('variant') || 'icon';
|
||||
const colorScheme = searchParams.get('colorScheme') || 'light';
|
||||
const size = parseInt(searchParams.get('size') || '256', 10);
|
||||
const background = searchParams.get('background') || 'transparent';
|
||||
const scale = parseInt(searchParams.get('scale') || '2', 10); // 1x, 2x, or 3x
|
||||
|
||||
// Validate inputs
|
||||
const validVariants = ['icon', 'primary', 'full', 'horizontal', 'horizontal-full', 'horizontal-v2', 'horizontal-full-v2'];
|
||||
const validColorSchemes = ['light', 'dark', 'mono-dark', 'mono-light'];
|
||||
const validBackgrounds = ['transparent', 'white', 'dark'];
|
||||
const validScales = [1, 2, 3];
|
||||
|
||||
if (!validVariants.includes(variant)) {
|
||||
return NextResponse.json({ error: 'Invalid variant' }, { status: 400 });
|
||||
}
|
||||
if (!validColorSchemes.includes(colorScheme)) {
|
||||
return NextResponse.json({ error: 'Invalid colorScheme' }, { status: 400 });
|
||||
}
|
||||
if (!validBackgrounds.includes(background)) {
|
||||
return NextResponse.json({ error: 'Invalid background' }, { status: 400 });
|
||||
}
|
||||
if (size < 32 || size > 2048) {
|
||||
return NextResponse.json({ error: 'Size must be between 32 and 2048' }, { status: 400 });
|
||||
}
|
||||
if (!validScales.includes(scale)) {
|
||||
return NextResponse.json({ error: 'Scale must be 1, 2, or 3' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-lcd-text',
|
||||
'--font-render-hinting=none',
|
||||
],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
// Set viewport with requested scale factor for crisp rendering
|
||||
await page.setViewport({
|
||||
width: 1200,
|
||||
height: 1200,
|
||||
deviceScaleFactor: scale,
|
||||
});
|
||||
|
||||
// Navigate to the render page with the actual React component
|
||||
const baseUrl = request.nextUrl.origin;
|
||||
const renderUrl = `${baseUrl}/render/${variant}/${colorScheme}/${size}?background=${background}`;
|
||||
|
||||
await page.goto(renderUrl, { waitUntil: 'networkidle0' });
|
||||
|
||||
// Wait for fonts to load
|
||||
await page.evaluateHandle('document.fonts.ready');
|
||||
|
||||
// Small delay to ensure rendering is complete
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Get the bounding box of the logo container
|
||||
const element = await page.$('#logo-container');
|
||||
if (!element) {
|
||||
throw new Error('Logo container not found');
|
||||
}
|
||||
|
||||
const boundingBox = await element.boundingBox();
|
||||
if (!boundingBox) {
|
||||
throw new Error('Could not get bounding box');
|
||||
}
|
||||
|
||||
// Take screenshot at higher resolution
|
||||
const screenshot = await page.screenshot({
|
||||
type: 'png',
|
||||
omitBackground: background === 'transparent',
|
||||
clip: {
|
||||
x: boundingBox.x,
|
||||
y: boundingBox.y,
|
||||
width: Math.ceil(boundingBox.width),
|
||||
height: Math.ceil(boundingBox.height),
|
||||
},
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
|
||||
// The screenshot is at the requested scale due to deviceScaleFactor
|
||||
// Keep the high-resolution output for crisp images
|
||||
const processedImage = await sharp(screenshot)
|
||||
.png({
|
||||
compressionLevel: 9,
|
||||
adaptiveFiltering: true,
|
||||
})
|
||||
.toBuffer();
|
||||
|
||||
// Return the image with scale suffix in filename
|
||||
const scaleLabel = scale > 1 ? `@${scale}x` : '';
|
||||
return new NextResponse(new Uint8Array(processedImage), {
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
'Content-Disposition': `attachment; filename="whyrating-${variant}-${size}px${scaleLabel}.png"`,
|
||||
'Cache-Control': 'public, max-age=31536000, immutable',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error generating PNG:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to generate PNG', details: String(error) },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
61
whyrating-templates/src/app/api/screenshot/route.ts
Normal file
61
whyrating-templates/src/app/api/screenshot/route.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { template, params } = await request.json();
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
// Build URL with query params
|
||||
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
|
||||
const url = new URL(`${baseUrl}/render/${template}`);
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
url.searchParams.set(key, value as string);
|
||||
});
|
||||
|
||||
// Set viewport for high-res output
|
||||
await page.setViewport({
|
||||
width: 1920,
|
||||
height: 1920,
|
||||
deviceScaleFactor: 1,
|
||||
});
|
||||
|
||||
await page.goto(url.toString(), { waitUntil: 'networkidle0' });
|
||||
|
||||
// Wait for template element
|
||||
await page.waitForSelector('#template');
|
||||
|
||||
// Screenshot just the template element
|
||||
const element = await page.$('#template');
|
||||
if (!element) {
|
||||
await browser.close();
|
||||
return NextResponse.json({ error: 'Template element not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const screenshot = await element.screenshot({
|
||||
type: 'png',
|
||||
omitBackground: true,
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
|
||||
return new NextResponse(Buffer.from(screenshot), {
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
'Content-Disposition': `attachment; filename="whyrating-${template}-${Date.now()}.png"`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Screenshot error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to generate screenshot', details: String(error) },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
BIN
whyrating-templates/src/app/favicon.ico
Normal file
BIN
whyrating-templates/src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
289
whyrating-templates/src/app/globals.css
Normal file
289
whyrating-templates/src/app/globals.css
Normal file
@@ -0,0 +1,289 @@
|
||||
/* Google Fonts - must be first */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Nunito:wght@700&display=swap');
|
||||
|
||||
@import "tailwindcss";
|
||||
|
||||
/* Configure Tailwind v4 dark mode to use .dark class instead of prefers-color-scheme */
|
||||
@custom-variant dark (&:is(.dark, .dark *));
|
||||
|
||||
:root {
|
||||
--background: #F8FAFC;
|
||||
--foreground: #1E293B;
|
||||
|
||||
/* Brand Logo Colors */
|
||||
--brand-star: #FBBC05;
|
||||
--brand-magnifier: #1E293B;
|
||||
--brand-lens: #FEF3C7;
|
||||
--brand-bar-light: #86EFAC;
|
||||
--brand-bar-mid: #22C55E;
|
||||
--brand-bar-dark: #15803D;
|
||||
--brand-accent: #F59E0B;
|
||||
|
||||
/* UI Colors */
|
||||
--ui-primary: #4285F4;
|
||||
--ui-primary-hover: #1E40AF;
|
||||
--ui-accent: #F59E0B;
|
||||
--ui-success: #34A853;
|
||||
--ui-error: #EA4335;
|
||||
|
||||
/* Surface Colors */
|
||||
--surface-page: #F8FAFC;
|
||||
--surface-card: #FFFFFF;
|
||||
--surface-muted: #F1F5F9;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
|
||||
/* Brand colors */
|
||||
--color-brand-star: var(--brand-star);
|
||||
--color-brand-magnifier: var(--brand-magnifier);
|
||||
--color-brand-lens: var(--brand-lens);
|
||||
--color-brand-bar-light: var(--brand-bar-light);
|
||||
--color-brand-bar-mid: var(--brand-bar-mid);
|
||||
--color-brand-bar-dark: var(--brand-bar-dark);
|
||||
--color-brand-accent: var(--brand-accent);
|
||||
|
||||
/* UI colors */
|
||||
--color-ui-primary: var(--ui-primary);
|
||||
--color-ui-primary-hover: var(--ui-primary-hover);
|
||||
--color-ui-success: var(--ui-success);
|
||||
--color-ui-error: var(--ui-error);
|
||||
|
||||
/* Surface colors */
|
||||
--color-surface-page: var(--surface-page);
|
||||
--color-surface-card: var(--surface-card);
|
||||
--color-surface-muted: var(--surface-muted);
|
||||
|
||||
/* Fonts */
|
||||
--font-sans: 'Inter', sans-serif;
|
||||
--font-wordmark: 'Nunito', sans-serif;
|
||||
|
||||
/* Border radius */
|
||||
--radius-brand: 10px;
|
||||
}
|
||||
|
||||
/* Dark mode */
|
||||
.dark {
|
||||
--background: #1C1917;
|
||||
--foreground: #FAFAF9;
|
||||
--surface-page: #1C1917;
|
||||
--surface-card: #292524;
|
||||
--surface-muted: #44403C;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: 'Inter', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Utility classes for fonts */
|
||||
.font-wordmark {
|
||||
font-family: 'Nunito', sans-serif;
|
||||
}
|
||||
|
||||
.font-body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/* Code block styles */
|
||||
.code-block {
|
||||
background-color: #1E293B;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
font-family: ui-monospace, 'SF Mono', monospace;
|
||||
font-size: 0.8125rem;
|
||||
color: #E2E8F0;
|
||||
overflow-x: auto;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.dark .code-block {
|
||||
background-color: #1C1917;
|
||||
border: 1px solid #57534E;
|
||||
}
|
||||
|
||||
/* Markdown content styles */
|
||||
.markdown-content h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1rem;
|
||||
margin-top: 2rem;
|
||||
color: #1E293B;
|
||||
}
|
||||
|
||||
.markdown-content h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
margin-top: 1.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid #E2E8F0;
|
||||
color: #1E293B;
|
||||
}
|
||||
|
||||
.markdown-content h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-top: 1.25rem;
|
||||
color: #1E293B;
|
||||
}
|
||||
|
||||
.markdown-content h4 {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
color: #1E293B;
|
||||
}
|
||||
|
||||
.markdown-content p {
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.7;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.markdown-content ul,
|
||||
.markdown-content ol {
|
||||
margin-bottom: 1rem;
|
||||
padding-left: 1.5rem;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.markdown-content li {
|
||||
margin-bottom: 0.25rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.markdown-content strong {
|
||||
font-weight: 600;
|
||||
color: #1E293B;
|
||||
}
|
||||
|
||||
.markdown-content blockquote {
|
||||
border-left: 4px solid #FBBC05;
|
||||
padding-left: 1rem;
|
||||
margin: 1rem 0;
|
||||
font-style: italic;
|
||||
color: #64748B;
|
||||
background: #FEF3C7;
|
||||
padding: 1rem;
|
||||
border-radius: 0 0.5rem 0.5rem 0;
|
||||
}
|
||||
|
||||
.markdown-content code {
|
||||
background: #F1F5F9;
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.875em;
|
||||
color: #1E293B;
|
||||
}
|
||||
|
||||
.markdown-content pre {
|
||||
background: #1E293B;
|
||||
padding: 1rem;
|
||||
border-radius: 0.75rem;
|
||||
overflow-x: auto;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.markdown-content pre code {
|
||||
background: transparent;
|
||||
color: #E2E8F0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.markdown-content table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1rem 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.markdown-content th {
|
||||
background: #F1F5F9;
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
border: 1px solid #E2E8F0;
|
||||
color: #1E293B;
|
||||
}
|
||||
|
||||
.markdown-content td {
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #E2E8F0;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.markdown-content hr {
|
||||
border: none;
|
||||
border-top: 2px solid #E2E8F0;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.markdown-content a {
|
||||
color: #4285F4;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markdown-content a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Dark mode markdown styles */
|
||||
.dark .markdown-content h1,
|
||||
.dark .markdown-content h2,
|
||||
.dark .markdown-content h3,
|
||||
.dark .markdown-content h4 {
|
||||
color: #FAFAF9;
|
||||
}
|
||||
|
||||
.dark .markdown-content h2 {
|
||||
border-bottom-color: #44403C;
|
||||
}
|
||||
|
||||
.dark .markdown-content p,
|
||||
.dark .markdown-content li,
|
||||
.dark .markdown-content td {
|
||||
color: #A8A29E;
|
||||
}
|
||||
|
||||
.dark .markdown-content strong {
|
||||
color: #FAFAF9;
|
||||
}
|
||||
|
||||
.dark .markdown-content blockquote {
|
||||
background: #292524;
|
||||
color: #A8A29E;
|
||||
border-left-color: #FBBC05;
|
||||
}
|
||||
|
||||
.dark .markdown-content code {
|
||||
background: #292524;
|
||||
color: #FAFAF9;
|
||||
}
|
||||
|
||||
.dark .markdown-content pre {
|
||||
background: #1C1917;
|
||||
border: 1px solid #57534E;
|
||||
}
|
||||
|
||||
.dark .markdown-content th {
|
||||
background: #292524;
|
||||
border-color: #44403C;
|
||||
color: #FAFAF9;
|
||||
}
|
||||
|
||||
.dark .markdown-content td {
|
||||
border-color: #44403C;
|
||||
}
|
||||
|
||||
.dark .markdown-content hr {
|
||||
border-top-color: #44403C;
|
||||
}
|
||||
24
whyrating-templates/src/app/layout.tsx
Normal file
24
whyrating-templates/src/app/layout.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
import { Providers } from "./providers";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Brand Guidelines & Templates",
|
||||
description: "Brand guidelines and asset downloads",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className="antialiased">
|
||||
<Providers>
|
||||
{children}
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
146
whyrating-templates/src/app/page.tsx
Normal file
146
whyrating-templates/src/app/page.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { WhyMyRatingLogo } from '@/components';
|
||||
import { useBrand } from '@/lib/BrandContext';
|
||||
import {
|
||||
StatTemplateSquare,
|
||||
StatTemplateLandscape,
|
||||
TipTemplateSquare,
|
||||
TipTemplateLandscape,
|
||||
CTATemplateSquare,
|
||||
CTATemplateLandscape,
|
||||
EmailSignature,
|
||||
EmailHeader,
|
||||
EmailHeaderMinimal,
|
||||
} from '@/components/templates';
|
||||
import { SettingsTab } from '@/components/tabs/SettingsTab';
|
||||
|
||||
type TabId = 'stat' | 'tip' | 'cta' | 'email' | 'settings';
|
||||
|
||||
const tabs: { id: TabId; label: string; description: string }[] = [
|
||||
{ id: 'stat', label: 'Stat Posts', description: 'Statistics & data posts' },
|
||||
{ id: 'tip', label: 'Tip Posts', description: 'Tips & advice posts' },
|
||||
{ id: 'cta', label: 'CTA Posts', description: 'Call-to-action posts' },
|
||||
{ id: 'email', label: 'Email', description: 'Signatures & headers' },
|
||||
{ id: 'settings', label: 'Settings', description: 'Configure brand settings' },
|
||||
];
|
||||
|
||||
export default function Home() {
|
||||
const [activeTab, setActiveTab] = useState<TabId>('stat');
|
||||
const [darkMode, setDarkMode] = useState(false);
|
||||
const { config } = useBrand();
|
||||
|
||||
// Toggle dark mode class on html element
|
||||
useEffect(() => {
|
||||
if (darkMode) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
}, [darkMode]);
|
||||
|
||||
const renderTab = () => {
|
||||
switch (activeTab) {
|
||||
case 'stat':
|
||||
return (
|
||||
<div className="space-y-12">
|
||||
<StatTemplateSquare />
|
||||
<StatTemplateLandscape />
|
||||
</div>
|
||||
);
|
||||
case 'tip':
|
||||
return (
|
||||
<div className="space-y-12">
|
||||
<TipTemplateSquare />
|
||||
<TipTemplateLandscape />
|
||||
</div>
|
||||
);
|
||||
case 'cta':
|
||||
return (
|
||||
<div className="space-y-12">
|
||||
<CTATemplateSquare />
|
||||
<CTATemplateLandscape />
|
||||
</div>
|
||||
);
|
||||
case 'email':
|
||||
return (
|
||||
<div className="space-y-12">
|
||||
<EmailSignature />
|
||||
<EmailHeader />
|
||||
<EmailHeaderMinimal />
|
||||
</div>
|
||||
);
|
||||
case 'settings':
|
||||
return <SettingsTab />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--surface-page)]">
|
||||
{/* Header */}
|
||||
<header className="bg-white dark:bg-stone-900 border-b border-slate-100 dark:border-stone-700/50 sticky top-0 z-50">
|
||||
<div className="max-w-6xl mx-auto px-6 py-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<WhyMyRatingLogo size={80} variant="horizontal-v2" colorScheme={darkMode ? 'dark' : 'light'} />
|
||||
<div className="hidden sm:block">
|
||||
<div className="text-lg font-semibold text-slate-900 dark:text-stone-50">Templates</div>
|
||||
<div className="text-sm text-slate-500 dark:text-stone-500">Create branded content</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => setDarkMode(!darkMode)}
|
||||
className="px-3 py-2 bg-slate-100 dark:bg-stone-800 border border-slate-100 dark:border-stone-700 rounded-lg cursor-pointer text-sm text-slate-900 dark:text-stone-50 hover:bg-slate-100 dark:hover:bg-stone-700/70 transition-colors"
|
||||
>
|
||||
{darkMode ? '☀️ Light' : '🌙 Dark'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="max-w-6xl mx-auto px-6">
|
||||
<nav className="flex gap-1 items-center">
|
||||
{tabs.map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`px-5 py-3 text-sm font-medium border-none cursor-pointer rounded-t-lg transition-all whitespace-nowrap
|
||||
${activeTab === tab.id
|
||||
? 'bg-slate-100 dark:bg-stone-800 text-slate-900 dark:text-stone-50'
|
||||
: 'bg-transparent text-slate-500 dark:text-stone-500 hover:text-slate-700 dark:hover:text-stone-300'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Content */}
|
||||
<main className="max-w-6xl mx-auto px-6 py-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold text-slate-900 dark:text-stone-50 mb-2">
|
||||
{tabs.find(t => t.id === activeTab)?.label}
|
||||
</h1>
|
||||
<p className="text-slate-500 dark:text-stone-500">
|
||||
{tabs.find(t => t.id === activeTab)?.description}
|
||||
{activeTab !== 'settings' && '. Edit the content below and download as PNG.'}
|
||||
</p>
|
||||
</div>
|
||||
{renderTab()}
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="text-center py-12 text-slate-400 dark:text-stone-600 text-sm">
|
||||
<span className="font-wordmark font-bold text-slate-500 dark:text-stone-500">
|
||||
{config.domain}<span className="text-[var(--brand-accent)]">{config.domainTLD}</span>
|
||||
</span>
|
||||
<span> • Templates • Version 1.0</span>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
7
whyrating-templates/src/app/providers.tsx
Normal file
7
whyrating-templates/src/app/providers.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { BrandProvider } from '@/lib/BrandContext';
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
return <BrandProvider>{children}</BrandProvider>;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import { WhyMyRatingLogo, LogoVariant, ColorScheme } from '@/components';
|
||||
import { use } from 'react';
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{
|
||||
variant: string;
|
||||
colorScheme: string;
|
||||
size: string;
|
||||
}>;
|
||||
searchParams: Promise<{
|
||||
background?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default function RenderPage({ params, searchParams }: PageProps) {
|
||||
const { variant, colorScheme, size } = use(params);
|
||||
const { background } = use(searchParams);
|
||||
|
||||
const sizeNum = parseInt(size, 10) || 256;
|
||||
const bg = background || 'transparent';
|
||||
|
||||
const bgColor = bg === 'white' ? '#FFFFFF' :
|
||||
bg === 'dark' ? '#1E293B' : 'transparent';
|
||||
|
||||
return (
|
||||
<div
|
||||
id="logo-container"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
background: bgColor,
|
||||
}}
|
||||
>
|
||||
<WhyMyRatingLogo
|
||||
size={sizeNum}
|
||||
variant={variant as LogoVariant}
|
||||
colorScheme={colorScheme as ColorScheme}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
63
whyrating-templates/src/app/render/cta-landscape/page.tsx
Normal file
63
whyrating-templates/src/app/render/cta-landscape/page.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { Suspense } from 'react';
|
||||
import { WhyMyRatingLogo } from '@/components';
|
||||
|
||||
function CTALandscapeTemplate() {
|
||||
const searchParams = useSearchParams();
|
||||
const hook = searchParams.get('hook') || 'Still reading reviews one by one?';
|
||||
const valueProp = searchParams.get('valueProp') || 'Get AI-powered insights in 45 seconds';
|
||||
const ctaText = searchParams.get('ctaText') || 'Try it free at whyrating.com';
|
||||
|
||||
return (
|
||||
<div
|
||||
id="template"
|
||||
className="rounded-xl p-20 flex flex-col justify-between relative overflow-hidden bg-gradient-to-br from-slate-800 to-slate-900"
|
||||
style={{ width: '1200px', height: '675px' }}
|
||||
>
|
||||
{/* Decorative stars */}
|
||||
<div className="absolute top-12 right-16 text-[#FBBC05] text-6xl opacity-20">★</div>
|
||||
<div className="absolute bottom-32 right-24 text-[#FBBC05] text-4xl opacity-15">★</div>
|
||||
<div className="absolute top-1/4 right-12 text-[#FBBC05] text-3xl opacity-10">★</div>
|
||||
<div className="absolute bottom-1/3 left-12 text-[#FBBC05] text-4xl opacity-10">★</div>
|
||||
|
||||
{/* Main Content - Horizontal Layout */}
|
||||
<div className="flex-1 flex items-center">
|
||||
<div className="flex flex-col gap-8 max-w-[85%]">
|
||||
{/* Hook */}
|
||||
<div className="text-slate-300 text-4xl font-medium leading-tight">
|
||||
{hook}
|
||||
</div>
|
||||
|
||||
{/* Value Proposition */}
|
||||
<div className="text-white text-6xl font-bold leading-tight">
|
||||
{valueProp}
|
||||
</div>
|
||||
|
||||
{/* CTA with amber accent */}
|
||||
<div className="mt-4">
|
||||
<span className="inline-block bg-[#F59E0B] text-slate-900 px-10 py-5 rounded-xl text-3xl font-semibold">
|
||||
{ctaText}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer with Logo */}
|
||||
<div className="flex items-end justify-end">
|
||||
<WhyMyRatingLogo size={140} variant="horizontal-v2" colorScheme="dark" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RenderCTALandscapePage() {
|
||||
return (
|
||||
<div className="bg-transparent">
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<CTALandscapeTemplate />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
61
whyrating-templates/src/app/render/cta-square/page.tsx
Normal file
61
whyrating-templates/src/app/render/cta-square/page.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { Suspense } from 'react';
|
||||
import { WhyMyRatingLogo } from '@/components';
|
||||
|
||||
function CTASquareTemplate() {
|
||||
const searchParams = useSearchParams();
|
||||
const hook = searchParams.get('hook') || 'Still reading reviews one by one?';
|
||||
const valueProp = searchParams.get('valueProp') || 'Get AI-powered insights in 45 seconds';
|
||||
const ctaText = searchParams.get('ctaText') || 'Try it free at whyrating.com';
|
||||
|
||||
return (
|
||||
<div
|
||||
id="template"
|
||||
className="rounded-xl p-24 flex flex-col justify-between relative overflow-hidden bg-gradient-to-br from-slate-800 to-slate-900"
|
||||
style={{ width: '1080px', height: '1080px' }}
|
||||
>
|
||||
{/* Decorative stars */}
|
||||
<div className="absolute top-16 right-16 text-[#FBBC05] text-7xl opacity-20">★</div>
|
||||
<div className="absolute bottom-64 left-20 text-[#FBBC05] text-5xl opacity-15">★</div>
|
||||
<div className="absolute top-1/4 right-32 text-[#FBBC05] text-4xl opacity-10">★</div>
|
||||
|
||||
{/* Hook Question */}
|
||||
<div className="pt-8">
|
||||
<div className="text-white text-6xl font-bold leading-tight max-w-4xl">
|
||||
{hook}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Value Proposition - Center */}
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="text-white text-5xl font-medium leading-snug text-center max-w-4xl">
|
||||
{valueProp}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA Section */}
|
||||
<div className="pb-8">
|
||||
<div className="bg-[#F59E0B] text-slate-900 text-4xl font-bold px-12 py-8 rounded-2xl text-center mb-12">
|
||||
{ctaText}
|
||||
</div>
|
||||
|
||||
{/* Logo */}
|
||||
<div className="flex justify-center">
|
||||
<WhyMyRatingLogo size={180} variant="horizontal-v2" colorScheme="dark" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RenderCTASquarePage() {
|
||||
return (
|
||||
<div className="bg-transparent">
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<CTASquareTemplate />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
'use client';
|
||||
|
||||
import { Suspense } from 'react';
|
||||
import { WhyMyRatingLogo } from '@/components';
|
||||
|
||||
function EmailHeaderMinimalTemplate() {
|
||||
return (
|
||||
<div
|
||||
id="template"
|
||||
className="rounded-lg flex items-center justify-center bg-gradient-to-r from-slate-800 to-slate-900"
|
||||
style={{ width: '600px', height: '100px' }}
|
||||
>
|
||||
<WhyMyRatingLogo size={70} variant="horizontal-v2" colorScheme="dark" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RenderEmailHeaderMinimalPage() {
|
||||
return (
|
||||
<div className="bg-transparent">
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<EmailHeaderMinimalTemplate />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
29
whyrating-templates/src/app/render/email-header/page.tsx
Normal file
29
whyrating-templates/src/app/render/email-header/page.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
'use client';
|
||||
|
||||
import { Suspense } from 'react';
|
||||
import { WhyMyRatingLogo } from '@/components';
|
||||
|
||||
function EmailHeaderTemplate() {
|
||||
return (
|
||||
<div
|
||||
id="template"
|
||||
className="rounded-lg flex flex-col items-center justify-center gap-4 bg-gradient-to-br from-slate-800 to-slate-900"
|
||||
style={{ width: '600px', height: '150px' }}
|
||||
>
|
||||
<WhyMyRatingLogo size={80} variant="horizontal-v2" colorScheme="dark" />
|
||||
<span className="text-[#A8A29E] text-sm tracking-wide">
|
||||
The story behind your stars
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RenderEmailHeaderPage() {
|
||||
return (
|
||||
<div className="bg-transparent">
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<EmailHeaderTemplate />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
50
whyrating-templates/src/app/render/stat-landscape/page.tsx
Normal file
50
whyrating-templates/src/app/render/stat-landscape/page.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { Suspense } from 'react';
|
||||
import { WhyMyRatingLogo } from '@/components';
|
||||
|
||||
function StatLandscapeTemplate() {
|
||||
const searchParams = useSearchParams();
|
||||
const stat = searchParams.get('stat') || '89%';
|
||||
const headline = searchParams.get('headline') || 'of consumers read business responses to reviews';
|
||||
const source = searchParams.get('source') || 'BrightLocal 2024';
|
||||
|
||||
return (
|
||||
<div
|
||||
id="template"
|
||||
className="rounded-xl p-16 flex flex-col justify-between relative overflow-hidden bg-gradient-to-br from-slate-800 to-slate-900"
|
||||
style={{ width: '1200px', height: '675px' }}
|
||||
>
|
||||
{/* Decorative stars */}
|
||||
<div className="absolute top-12 right-12 text-[#FBBC05] text-6xl opacity-20">★</div>
|
||||
<div className="absolute bottom-32 right-24 text-[#FBBC05] text-4xl opacity-15">★</div>
|
||||
<div className="absolute top-1/3 right-16 text-[#FBBC05] text-3xl opacity-10">★</div>
|
||||
<div className="absolute top-1/2 left-1/4 text-[#FBBC05] text-2xl opacity-10">★</div>
|
||||
|
||||
{/* Content - horizontal layout for landscape */}
|
||||
<div className="flex-1 flex items-center">
|
||||
<div className="flex items-center gap-12">
|
||||
<div className="text-[#FBBC05] text-[120px] font-bold shrink-0">{stat}</div>
|
||||
<div className="text-white text-4xl font-medium leading-tight max-w-2xl">{headline}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-end justify-between">
|
||||
<div className="text-slate-400 text-xl">Source: {source}</div>
|
||||
<WhyMyRatingLogo size={120} variant="horizontal-v2" colorScheme="dark" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RenderStatLandscapePage() {
|
||||
return (
|
||||
<div className="bg-transparent">
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<StatLandscapeTemplate />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
51
whyrating-templates/src/app/render/stat-square/page.tsx
Normal file
51
whyrating-templates/src/app/render/stat-square/page.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { Suspense } from 'react';
|
||||
import { WhyMyRatingLogo } from '@/components';
|
||||
|
||||
function StatSquareTemplate() {
|
||||
const searchParams = useSearchParams();
|
||||
const stat = searchParams.get('stat') || '89%';
|
||||
const headline = searchParams.get('headline') || 'of consumers read business responses to reviews';
|
||||
const source = searchParams.get('source') || 'BrightLocal 2024';
|
||||
|
||||
return (
|
||||
<div
|
||||
id="template"
|
||||
className="rounded-xl p-12 flex flex-col justify-between relative overflow-hidden"
|
||||
style={{
|
||||
width: '1080px',
|
||||
height: '1080px',
|
||||
background: 'linear-gradient(to bottom right, #1e293b, #0f172a)'
|
||||
}}
|
||||
>
|
||||
{/* Decorative stars */}
|
||||
<div className="absolute top-16 right-16 text-8xl opacity-20 text-[#FBBC05]">★</div>
|
||||
<div className="absolute bottom-40 right-32 text-5xl opacity-15 text-[#FBBC05]">★</div>
|
||||
<div className="absolute top-1/3 right-24 text-4xl opacity-10 text-[#FBBC05]">★</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 flex flex-col justify-center">
|
||||
<div className="text-[160px] font-bold mb-8 text-[#FBBC05]">{stat}</div>
|
||||
<div className="text-5xl font-medium leading-tight max-w-4xl text-white">{headline}</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-end justify-between">
|
||||
<div className="text-2xl text-[#94a3b8]">Source: {source}</div>
|
||||
<WhyMyRatingLogo size={160} variant="horizontal-v2" colorScheme="dark" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RenderStatSquarePage() {
|
||||
return (
|
||||
<div className="bg-transparent">
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<StatSquareTemplate />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
55
whyrating-templates/src/app/render/tip-landscape/page.tsx
Normal file
55
whyrating-templates/src/app/render/tip-landscape/page.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { Suspense } from 'react';
|
||||
import { WhyMyRatingLogo } from '@/components';
|
||||
|
||||
function TipLandscapeTemplate() {
|
||||
const searchParams = useSearchParams();
|
||||
const tipNumber = searchParams.get('tipNumber') || '1';
|
||||
const tipTitle = searchParams.get('tipTitle') || 'Respond to negative reviews within 24 hours';
|
||||
const tipBody = searchParams.get('tipBody') || 'Quick responses show customers you care and can turn a negative experience into a positive one.';
|
||||
|
||||
return (
|
||||
<div
|
||||
id="template"
|
||||
className="rounded-xl p-20 flex relative overflow-hidden bg-gradient-to-br from-slate-800 to-slate-900"
|
||||
style={{ width: '1200px', height: '675px' }}
|
||||
>
|
||||
{/* Decorative stars */}
|
||||
<div className="absolute top-12 right-12 text-[#FBBC05] text-7xl opacity-20">★</div>
|
||||
<div className="absolute bottom-32 right-48 text-[#FBBC05] text-5xl opacity-15">★</div>
|
||||
<div className="absolute top-1/4 right-20 text-[#FBBC05] text-4xl opacity-10">★</div>
|
||||
|
||||
{/* Left section - TIP number */}
|
||||
<div className="flex flex-col justify-center pr-16 border-r border-slate-600">
|
||||
<div className="text-[#FBBC05] text-3xl font-bold tracking-widest uppercase">TIP</div>
|
||||
<div className="text-[#FBBC05] text-[100px] font-bold leading-none">#{tipNumber}</div>
|
||||
</div>
|
||||
|
||||
{/* Right section - Content */}
|
||||
<div className="flex-1 flex flex-col justify-between pl-16">
|
||||
{/* Tip content */}
|
||||
<div className="flex-1 flex flex-col justify-center">
|
||||
<h2 className="text-white text-4xl font-bold leading-tight mb-6">{tipTitle}</h2>
|
||||
<p className="text-slate-300 text-2xl leading-relaxed max-w-2xl">{tipBody}</p>
|
||||
</div>
|
||||
|
||||
{/* Logo */}
|
||||
<div className="flex justify-end">
|
||||
<WhyMyRatingLogo size={140} variant="horizontal-v2" colorScheme="dark" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RenderTipLandscapePage() {
|
||||
return (
|
||||
<div className="bg-transparent">
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<TipLandscapeTemplate />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
62
whyrating-templates/src/app/render/tip-square/page.tsx
Normal file
62
whyrating-templates/src/app/render/tip-square/page.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { Suspense } from 'react';
|
||||
import { WhyMyRatingLogo } from '@/components';
|
||||
|
||||
function TipSquareTemplate() {
|
||||
const searchParams = useSearchParams();
|
||||
const tipNumber = searchParams.get('tipNumber') || '1';
|
||||
const tipTitle = searchParams.get('tipTitle') || 'Respond to negative reviews within 24 hours';
|
||||
const tipBody = searchParams.get('tipBody') || 'Quick responses show customers you care and can turn a negative experience into a positive one.';
|
||||
|
||||
return (
|
||||
<div
|
||||
id="template"
|
||||
className="rounded-xl p-24 flex flex-col justify-between relative overflow-hidden bg-gradient-to-br from-slate-800 to-slate-900"
|
||||
style={{ width: '1080px', height: '1080px' }}
|
||||
>
|
||||
{/* Decorative elements */}
|
||||
<div className="absolute top-12 left-12 w-48 h-48 rounded-full bg-[#FBBC05] opacity-5"></div>
|
||||
<div className="absolute bottom-64 right-16 w-32 h-32 rounded-full bg-[#FBBC05] opacity-5"></div>
|
||||
<div className="absolute top-1/2 right-12 text-[#FBBC05] text-6xl opacity-15">★</div>
|
||||
<div className="absolute bottom-32 left-24 text-[#FBBC05] text-4xl opacity-10">★</div>
|
||||
|
||||
{/* Decorative line accent */}
|
||||
<div className="absolute top-0 left-32 w-2 h-40 bg-gradient-to-b from-[#FBBC05] to-transparent opacity-30"></div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 flex flex-col justify-center z-10">
|
||||
{/* Tip Header */}
|
||||
<div className="text-[#FBBC05] text-4xl font-bold tracking-widest uppercase mb-8">
|
||||
TIP #{tipNumber}
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<div className="text-white text-6xl font-bold leading-tight mb-12 max-w-4xl">
|
||||
{tipTitle}
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="text-slate-300 text-3xl leading-relaxed max-w-3xl">
|
||||
{tipBody}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer with Logo */}
|
||||
<div className="flex items-end justify-end">
|
||||
<WhyMyRatingLogo size={160} variant="horizontal-v2" colorScheme="dark" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RenderTipSquarePage() {
|
||||
return (
|
||||
<div className="bg-transparent">
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<TipSquareTemplate />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
751
whyrating-templates/src/components/Presentation.tsx
Normal file
751
whyrating-templates/src/components/Presentation.tsx
Normal file
@@ -0,0 +1,751 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { WhyMyRatingLogo } from './WhyMyRatingLogo';
|
||||
|
||||
interface Slide {
|
||||
id: string;
|
||||
content: React.ReactNode;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export function Presentation({ onClose }: { onClose: () => void }) {
|
||||
const [currentSlide, setCurrentSlide] = useState(0);
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
|
||||
const slides: Slide[] = [
|
||||
// ===== ACT 1: THE HOOK =====
|
||||
{
|
||||
id: 'title',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<div className="text-3xl font-bold mb-8" style={{ fontFamily: 'Nunito, sans-serif' }}>
|
||||
<span className="text-white">whyrating</span>
|
||||
<span className="text-amber-400">.com</span>
|
||||
</div>
|
||||
<h1 className="text-7xl font-bold text-white mb-6 tracking-tight">Brand Guidelines</h1>
|
||||
<p className="text-2xl text-slate-400">A Strategic Identity System</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'hook',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full px-12">
|
||||
<h1 className="text-6xl font-semibold text-white text-center leading-tight max-w-4xl">
|
||||
What if you could see<br />
|
||||
<span className="text-amber-400">what Google doesn't show?</span>
|
||||
</h1>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'problem',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full px-12">
|
||||
<p className="text-slate-500 text-lg tracking-[0.2em] uppercase mb-8">The Problem</p>
|
||||
<h1 className="text-5xl font-semibold text-white text-center leading-tight max-w-4xl mb-8">
|
||||
Small business owners are drowning in reviews.
|
||||
</h1>
|
||||
<p className="text-2xl text-slate-400 text-center max-w-2xl">
|
||||
They know their rating. But they don't know <em className="text-white">why</em> it's dropping.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'audience',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full px-12">
|
||||
<p className="text-slate-500 text-lg tracking-[0.2em] uppercase mb-8">Who We Serve</p>
|
||||
<h1 className="text-5xl font-semibold text-white text-center leading-tight max-w-4xl mb-12">
|
||||
The bakery owner. The dentist.<br />The auto shop manager.
|
||||
</h1>
|
||||
<p className="text-2xl text-slate-400 text-center max-w-3xl">
|
||||
People who want <span className="text-white">actionable insights</span>, not enterprise complexity.<br />
|
||||
Who don't have time for complex tools or $300/month software.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'mission',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full px-12">
|
||||
<p className="text-slate-500 text-lg tracking-[0.2em] uppercase mb-8">Our Mission</p>
|
||||
<h1 className="text-5xl font-semibold text-white text-center leading-tight max-w-5xl">
|
||||
Enterprise-grade insights.<br />
|
||||
<span className="text-green-400">Small business price.</span>
|
||||
</h1>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
// ===== ACT 2: THE STRATEGIC CHALLENGE =====
|
||||
{
|
||||
id: 'challenge-intro',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full px-12">
|
||||
<p className="text-amber-400 text-lg tracking-[0.2em] uppercase mb-8">The Strategic Challenge</p>
|
||||
<h1 className="text-6xl font-semibold text-white text-center leading-tight max-w-4xl">
|
||||
We analyze <span className="text-blue-400">Google</span>.<br />
|
||||
But we're <span className="text-slate-500">not</span> Google.
|
||||
</h1>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'four-requirements',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full px-12">
|
||||
<p className="text-slate-500 text-lg tracking-[0.2em] uppercase mb-12">Four Requirements</p>
|
||||
<div className="grid grid-cols-2 gap-8 max-w-4xl">
|
||||
{[
|
||||
{ num: '01', text: 'Instant recognition of what we do' },
|
||||
{ num: '02', text: 'Avoid looking like a Google clone' },
|
||||
{ num: '03', text: 'Feel approachable to SMB owners' },
|
||||
{ num: '04', text: 'Convey analytical depth and trust' },
|
||||
].map((item) => (
|
||||
<div key={item.num} className="flex gap-4 items-start">
|
||||
<span className="text-amber-400 text-2xl font-mono">{item.num}</span>
|
||||
<p className="text-2xl text-white">{item.text}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'tradeoff',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full px-12">
|
||||
<p className="text-slate-500 text-lg tracking-[0.2em] uppercase mb-12">The Trade-off</p>
|
||||
<div className="flex flex-col gap-6 max-w-3xl">
|
||||
<div className="flex items-center gap-6 opacity-50">
|
||||
<span className="text-red-400 text-2xl">✗</span>
|
||||
<div>
|
||||
<p className="text-2xl text-white">Full Google palette</p>
|
||||
<p className="text-lg text-slate-500">Looks like a clone. No differentiation.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-6 opacity-50">
|
||||
<span className="text-red-400 text-2xl">✗</span>
|
||||
<div>
|
||||
<p className="text-2xl text-white">Completely unique</p>
|
||||
<p className="text-lg text-slate-500">Disconnected from what we do.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<span className="text-green-400 text-2xl">✓</span>
|
||||
<div>
|
||||
<p className="text-2xl text-white font-semibold">Hybrid approach</p>
|
||||
<p className="text-lg text-slate-400">Recognition + Differentiation.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
// ===== ACT 3: THE LOGO UNVEIL =====
|
||||
{
|
||||
id: 'logo-intro',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<p className="text-slate-500 text-lg tracking-[0.3em] uppercase mb-8">The Logo</p>
|
||||
<h1 className="text-6xl font-semibold text-white text-center">
|
||||
Three elements.<br />
|
||||
<span className="text-slate-400">One story.</span>
|
||||
</h1>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'logo-star',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full px-12">
|
||||
<div className="w-32 h-32 mb-12">
|
||||
<svg viewBox="0 0 120 120" className="w-full h-full">
|
||||
<polygon
|
||||
points="60,15 71.5,42 101,46 79.5,66 85,95 60,82 35,95 40.5,66 19,46 48.5,42"
|
||||
fill="#FBBC05"
|
||||
stroke="#FBBC05"
|
||||
strokeWidth="6"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-amber-400 text-lg tracking-[0.2em] uppercase mb-4">Google Yellow</p>
|
||||
<h1 className="text-5xl font-semibold text-white text-center mb-6">
|
||||
The Star
|
||||
</h1>
|
||||
<p className="text-2xl text-slate-400 text-center max-w-2xl">
|
||||
“This is about your Google rating.”
|
||||
</p>
|
||||
<p className="text-lg text-slate-500 mt-4">Instant recognition.</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'logo-magnifier',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full px-12">
|
||||
<div className="w-32 h-32 mb-12 relative">
|
||||
<svg viewBox="0 0 120 120" className="w-full h-full">
|
||||
<circle cx="60" cy="55" r="30" fill="#1E293B" />
|
||||
<circle cx="60" cy="55" r="22" fill="#FEF3C7" />
|
||||
<line x1="83" y1="75" x2="100" y2="92" stroke="#1E293B" strokeWidth="12" strokeLinecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-slate-400 text-lg tracking-[0.2em] uppercase mb-4">Dark Slate</p>
|
||||
<h1 className="text-5xl font-semibold text-white text-center mb-6">
|
||||
The Magnifier
|
||||
</h1>
|
||||
<p className="text-2xl text-slate-400 text-center max-w-2xl">
|
||||
“We see what Google doesn't show.”
|
||||
</p>
|
||||
<p className="text-lg text-slate-500 mt-4">Differentiation. Depth. Analysis.</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'magnifier-why',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full px-12">
|
||||
<p className="text-slate-500 text-lg tracking-[0.2em] uppercase mb-12">Creative Decision</p>
|
||||
<h1 className="text-4xl font-semibold text-white text-center mb-12 max-w-3xl">
|
||||
Why dark instead of Google Blue?
|
||||
</h1>
|
||||
<div className="grid grid-cols-2 gap-12 max-w-3xl">
|
||||
<div className="text-center opacity-50">
|
||||
<div className="w-16 h-16 rounded-full bg-blue-500 mx-auto mb-4"></div>
|
||||
<p className="text-xl text-white mb-2">Google Blue</p>
|
||||
<p className="text-slate-500">“We're part of Google”</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 rounded-full bg-slate-800 border-2 border-slate-600 mx-auto mb-4"></div>
|
||||
<p className="text-xl text-white mb-2 font-semibold">Dark Slate</p>
|
||||
<p className="text-slate-400">“We look deeper”</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'logo-bars',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full px-12">
|
||||
<div className="flex items-end gap-3 mb-12 h-32">
|
||||
<div className="w-8 h-16 rounded-sm bg-green-300"></div>
|
||||
<div className="w-8 h-24 rounded-sm bg-green-500"></div>
|
||||
<div className="w-8 h-32 rounded-sm bg-green-700"></div>
|
||||
</div>
|
||||
<p className="text-green-400 text-lg tracking-[0.2em] uppercase mb-4">Custom Green Gradient</p>
|
||||
<h1 className="text-5xl font-semibold text-white text-center mb-6">
|
||||
The Growth Bars
|
||||
</h1>
|
||||
<p className="text-2xl text-slate-400 text-center max-w-2xl">
|
||||
“You'll see improvement.”
|
||||
</p>
|
||||
<p className="text-lg text-slate-500 mt-4">Progress. Journey. The outcome they want.</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'bars-why',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full px-12">
|
||||
<p className="text-slate-500 text-lg tracking-[0.2em] uppercase mb-12">Creative Decision</p>
|
||||
<h1 className="text-4xl font-semibold text-white text-center mb-12 max-w-3xl">
|
||||
Why gradient instead of single green?
|
||||
</h1>
|
||||
<div className="grid grid-cols-2 gap-12 max-w-3xl">
|
||||
<div className="text-center opacity-50">
|
||||
<div className="flex justify-center items-end gap-2 mb-4 h-20">
|
||||
<div className="w-6 h-12 bg-green-500 rounded"></div>
|
||||
<div className="w-6 h-16 bg-green-500 rounded"></div>
|
||||
<div className="w-6 h-20 bg-green-500 rounded"></div>
|
||||
</div>
|
||||
<p className="text-xl text-white mb-2">Single Green</p>
|
||||
<p className="text-slate-500">Static. Binary success.</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="flex justify-center items-end gap-2 mb-4 h-20">
|
||||
<div className="w-6 h-12 bg-green-300 rounded"></div>
|
||||
<div className="w-6 h-16 bg-green-500 rounded"></div>
|
||||
<div className="w-6 h-20 bg-green-700 rounded"></div>
|
||||
</div>
|
||||
<p className="text-xl text-white mb-2 font-semibold">Gradient</p>
|
||||
<p className="text-slate-400">Growth journey. Things get better.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'logo-assembly',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<div className="mb-8">
|
||||
<WhyMyRatingLogo size={280} variant="icon" colorScheme="dark" />
|
||||
</div>
|
||||
<p className="text-2xl text-slate-400 text-center max-w-2xl mt-4">
|
||||
Your Google rating hides insights that reveal a path to growth.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'logo-light-dark',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full px-12">
|
||||
<p className="text-slate-500 text-lg tracking-[0.2em] uppercase mb-12">Adaptable</p>
|
||||
<div className="grid grid-cols-2 gap-16">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="bg-white rounded-2xl p-12 mb-6">
|
||||
<WhyMyRatingLogo size={160} variant="icon" colorScheme="light" />
|
||||
</div>
|
||||
<p className="text-slate-400">Light backgrounds</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="bg-slate-900 rounded-2xl p-12 mb-6 border border-slate-700">
|
||||
<WhyMyRatingLogo size={160} variant="icon" colorScheme="dark" />
|
||||
</div>
|
||||
<p className="text-slate-400">Dark backgrounds</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'logo-story',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full px-12">
|
||||
<p className="text-slate-500 text-lg tracking-[0.2em] uppercase mb-12">The Logo Story</p>
|
||||
<h1 className="text-4xl font-semibold text-white text-center leading-relaxed max-w-4xl">
|
||||
Your <span className="text-amber-400">Google rating</span> (star)<br />
|
||||
hides <span className="text-slate-400">insights</span> (magnifier)<br />
|
||||
that reveal a path to <span className="text-green-400">growth</span> (bars).
|
||||
</h1>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'proportions',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full px-12">
|
||||
<p className="text-slate-500 text-lg tracking-[0.2em] uppercase mb-6">Proportions System</p>
|
||||
<h1 className="text-3xl font-semibold text-white text-center mb-8">
|
||||
Tight-fit bounding boxes.
|
||||
</h1>
|
||||
<svg viewBox="0 0 650 200" className="w-full max-w-4xl h-auto">
|
||||
{/* Background */}
|
||||
<rect x="40" y="20" width="570" height="130" fill="#1E293B" rx="8"/>
|
||||
|
||||
{/* Star with tight bbox - height 70 */}
|
||||
<rect x="70" y="35" width="73" height="70" fill="none" stroke="#EF4444" strokeWidth="2.5"/>
|
||||
<polygon points="106,35 117,58 143,58 123,73 131,105 106,89 81,105 89,73 69,58 95,58" fill="#FBBC05"/>
|
||||
|
||||
{/* Gap zone - 0.30H = 21px */}
|
||||
<rect x="143" y="50" width="21" height="40" fill="rgba(251,188,5,0.25)" stroke="#FBBC05" strokeWidth="2" strokeDasharray="6 3"/>
|
||||
|
||||
{/* Text with tight bbox - centered with star (star center y=70) */}
|
||||
<text x="164" y="82" fontSize="32" fontFamily="Nunito, Arial" fontWeight="700">
|
||||
<tspan fill="#FAFAF9">whyrating</tspan>
|
||||
<tspan fill="#F59E0B">.com</tspan>
|
||||
</text>
|
||||
<rect x="164" y="51" width="262" height="38" fill="none" stroke="#22C55E" strokeWidth="2.5"/>
|
||||
|
||||
{/* Dimension labels */}
|
||||
<text x="106" y="130" fontSize="11" fill="#EF4444" textAnchor="middle">H</text>
|
||||
<text x="153" y="130" fontSize="11" fill="#FBBC05" textAnchor="middle">0.3H</text>
|
||||
<text x="295" y="130" fontSize="11" fill="#22C55E" textAnchor="middle">0.85H</text>
|
||||
|
||||
{/* Ratio bar */}
|
||||
<rect x="70" y="150" width="73" height="8" fill="#EF4444" rx="2"/>
|
||||
<rect x="143" y="150" width="21" height="8" fill="#FBBC05" rx="2"/>
|
||||
<rect x="164" y="150" width="262" height="8" fill="#22C55E" rx="2"/>
|
||||
|
||||
{/* Ratio text */}
|
||||
<text x="480" y="75" fontSize="14" fill="#94A3B8">Ratio</text>
|
||||
<text x="480" y="100" fontSize="20" fill="#FAFAF9" fontWeight="600">1 : 0.30 : 3.4</text>
|
||||
<text x="480" y="120" fontSize="11" fill="#64748B">Symbol : Gap : Text</text>
|
||||
|
||||
{/* Why callout */}
|
||||
<text x="480" y="155" fontSize="10" fill="#FBBC05">Why 0.30?</text>
|
||||
<text x="480" y="170" fontSize="9" fill="#64748B">Tight coupling</text>
|
||||
<text x="480" y="182" fontSize="9" fill="#64748B">creates unity.</text>
|
||||
</svg>
|
||||
<p className="text-slate-600 mt-6 text-center">
|
||||
Tight gap creates a unified lockup. <span className="text-amber-400">Text nearly matches star height.</span>
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'text-size-calc',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full px-12">
|
||||
<p className="text-slate-500 text-lg tracking-[0.2em] uppercase mb-8">Proportions System</p>
|
||||
<h1 className="text-4xl font-semibold text-white text-center mb-12">
|
||||
Text height = <span className="text-amber-400">85%</span> of star height.
|
||||
</h1>
|
||||
<svg viewBox="0 0 520 130" className="w-full max-w-3xl h-auto">
|
||||
{/* Star with bbox - height 50px */}
|
||||
<rect x="68" y="30" width="52" height="50" fill="none" stroke="#EF4444" strokeWidth="2.5" strokeDasharray="6 3"/>
|
||||
<polygon points="94,30 102,47 120,47 106,58 112,80 94,69 76,80 82,58 68,47 86,47" fill="#FBBC05"/>
|
||||
|
||||
{/* Star height dimension */}
|
||||
<line x1="50" y1="30" x2="50" y2="80" stroke="#EF4444" strokeWidth="2"/>
|
||||
<line x1="45" y1="30" x2="55" y2="30" stroke="#EF4444" strokeWidth="2"/>
|
||||
<line x1="45" y1="80" x2="55" y2="80" stroke="#EF4444" strokeWidth="2"/>
|
||||
<text x="37" y="58" fontSize="14" fill="#EF4444" fontWeight="600">H</text>
|
||||
|
||||
{/* Gap zone - 0.30H = 15px, centered with star */}
|
||||
<rect x="120" y="38" width="15" height="34" fill="rgba(251,188,5,0.25)" stroke="#FBBC05" strokeWidth="2" strokeDasharray="4 2"/>
|
||||
|
||||
{/* Text with bbox - centered with star (star center y=55) */}
|
||||
<text x="135" y="70" fontSize="36" fontFamily="Nunito, Arial" fontWeight="700">
|
||||
<tspan fill="#FAFAF9">whyrating</tspan>
|
||||
<tspan fill="#F59E0B">.com</tspan>
|
||||
</text>
|
||||
<rect x="135" y="33" width="295" height="43" fill="none" stroke="#22C55E" strokeWidth="2.5" strokeDasharray="6 3"/>
|
||||
|
||||
{/* Text height dimension */}
|
||||
<line x1="440" y1="33" x2="440" y2="76" stroke="#22C55E" strokeWidth="2"/>
|
||||
<line x1="435" y1="33" x2="445" y2="33" stroke="#22C55E" strokeWidth="2"/>
|
||||
<line x1="435" y1="76" x2="445" y2="76" stroke="#22C55E" strokeWidth="2"/>
|
||||
<text x="455" y="58" fontSize="13" fill="#22C55E" fontWeight="600">0.85H</text>
|
||||
|
||||
{/* Labels */}
|
||||
<text x="94" y="105" fontSize="11" fill="#EF4444" textAnchor="middle">H</text>
|
||||
<text x="127" y="105" fontSize="11" fill="#FBBC05" textAnchor="middle">0.3H</text>
|
||||
<text x="282" y="105" fontSize="11" fill="#22C55E" textAnchor="middle">0.85H</text>
|
||||
</svg>
|
||||
<p className="text-slate-500 mt-10 text-center max-w-2xl">
|
||||
<span className="text-amber-400">Why 85%?</span> Text nearly matches star height —
|
||||
bold presence, tight unity, reads as one mark.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'proportions-result',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<WhyMyRatingLogo size={300} variant="horizontal-v2" colorScheme="dark" />
|
||||
<p className="text-slate-500 mt-12 text-lg">
|
||||
Tight gap. Bold text. <span className="text-amber-400">One unified mark.</span>
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
// ===== ACT 4: THE WORDMARK =====
|
||||
{
|
||||
id: 'wordmark',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full px-12">
|
||||
<p className="text-slate-500 text-lg tracking-[0.2em] uppercase mb-12">The Wordmark</p>
|
||||
<h1 className="text-6xl font-bold text-white mb-4" style={{ fontFamily: 'Nunito, sans-serif' }}>
|
||||
whyrating<span className="text-amber-400">.com</span>
|
||||
</h1>
|
||||
<p className="text-xl text-slate-500 mt-8">Nunito Bold — Warm, approachable, human.</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'wordmark-why',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full px-12">
|
||||
<p className="text-slate-500 text-lg tracking-[0.2em] uppercase mb-12">Font Choice</p>
|
||||
<div className="grid grid-cols-2 gap-16 max-w-4xl">
|
||||
<div className="text-center opacity-50">
|
||||
<p className="text-4xl text-white mb-4" style={{ fontFamily: 'Inter, sans-serif' }}>whyrating</p>
|
||||
<p className="text-xl text-white mb-2">Inter</p>
|
||||
<p className="text-slate-500">Clean. Professional.<br />Tech/enterprise. “Platform”</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-4xl text-white mb-4 font-bold" style={{ fontFamily: 'Nunito, sans-serif' }}>whyrating</p>
|
||||
<p className="text-xl text-white mb-2 font-semibold">Nunito</p>
|
||||
<p className="text-slate-400">Warm. Approachable.<br />Small business friendly. “Helper”</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
// ===== ACT 5: TWO COLOR SYSTEMS =====
|
||||
{
|
||||
id: 'two-systems',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full px-12">
|
||||
<p className="text-slate-500 text-lg tracking-[0.2em] uppercase mb-12">Key Insight</p>
|
||||
<h1 className="text-5xl font-semibold text-white text-center leading-tight max-w-4xl mb-8">
|
||||
Two color systems.<br />
|
||||
<span className="text-slate-400">By design.</span>
|
||||
</h1>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'systems-explained',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full px-12">
|
||||
<div className="grid grid-cols-2 gap-16 max-w-5xl">
|
||||
<div className="text-center">
|
||||
<div className="flex justify-center gap-2 mb-6">
|
||||
<div className="w-10 h-10 rounded-full bg-amber-400"></div>
|
||||
<div className="w-10 h-10 rounded-full bg-slate-800 border border-slate-600"></div>
|
||||
<div className="w-10 h-10 rounded-full bg-green-400"></div>
|
||||
</div>
|
||||
<p className="text-2xl text-white mb-2 font-semibold">Logo Colors</p>
|
||||
<p className="text-slate-400">Distinctive. Memorable.<br />Creates differentiation.</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="flex justify-center gap-2 mb-6">
|
||||
<div className="w-10 h-10 rounded-full bg-blue-500"></div>
|
||||
<div className="w-10 h-10 rounded-full bg-amber-500"></div>
|
||||
<div className="w-10 h-10 rounded-full bg-slate-700"></div>
|
||||
</div>
|
||||
<p className="text-2xl text-white mb-2 font-semibold">UI Colors</p>
|
||||
<p className="text-slate-400">Familiar. Google-inspired.<br />Reduces cognitive load.</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xl text-slate-500 mt-12 text-center max-w-2xl">
|
||||
Users spend <span className="text-white">seconds</span> on the logo, but <span className="text-white">hours</span> in the interface.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'design-principle',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full px-12">
|
||||
<p className="text-slate-500 text-lg tracking-[0.2em] uppercase mb-12">Core Principle</p>
|
||||
<div className="flex flex-col gap-6 text-center">
|
||||
<div>
|
||||
<p className="text-4xl text-white font-semibold mb-2">Logo = Distinctive</p>
|
||||
<p className="text-xl text-slate-400">Creates recognition and differentiation.</p>
|
||||
</div>
|
||||
<div className="text-4xl text-slate-600">+</div>
|
||||
<div>
|
||||
<p className="text-4xl text-white font-semibold mb-2">UI = Familiar</p>
|
||||
<p className="text-xl text-slate-400">Feels like tools users already know.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
// ===== ACT 6: VOICE =====
|
||||
{
|
||||
id: 'voice-intro',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<p className="text-slate-500 text-lg tracking-[0.2em] uppercase mb-8">Brand Voice</p>
|
||||
<h1 className="text-6xl font-semibold text-white text-center">
|
||||
Helpful expert.<br />
|
||||
<span className="text-slate-500">Not salesy.</span>
|
||||
</h1>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'voice-principles',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full px-12">
|
||||
<p className="text-slate-500 text-lg tracking-[0.2em] uppercase mb-12">Four Principles</p>
|
||||
<div className="grid grid-cols-2 gap-8 max-w-3xl">
|
||||
{[
|
||||
{ title: 'Helpful Expert', desc: 'Knowledgeable but never condescending' },
|
||||
{ title: 'Plain Language', desc: 'No jargon, no buzzwords' },
|
||||
{ title: 'Respectful of Time', desc: 'Get to the point' },
|
||||
{ title: 'Encouraging', desc: 'Problems are fixable' },
|
||||
].map((item) => (
|
||||
<div key={item.title} className="text-center p-6">
|
||||
<p className="text-2xl text-white font-semibold mb-2">{item.title}</p>
|
||||
<p className="text-slate-400">{item.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'voice-examples',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full px-12">
|
||||
<p className="text-slate-500 text-lg tracking-[0.2em] uppercase mb-12">How We Write</p>
|
||||
<div className="grid grid-cols-2 gap-12 max-w-4xl">
|
||||
<div>
|
||||
<p className="text-green-400 text-lg mb-4">✓ We say</p>
|
||||
<p className="text-2xl text-white mb-4">“Analyze my reviews”</p>
|
||||
<p className="text-2xl text-white mb-4">“Fix the right problems first”</p>
|
||||
<p className="text-2xl text-white">“Questions? Just reply.”</p>
|
||||
</div>
|
||||
<div className="opacity-50">
|
||||
<p className="text-red-400 text-lg mb-4">✗ We don't say</p>
|
||||
<p className="text-2xl text-white mb-4">“Leverage AI-powered analytics”</p>
|
||||
<p className="text-2xl text-white mb-4">“Unlock synergies”</p>
|
||||
<p className="text-2xl text-white">“Please submit a ticket”</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
// ===== ACT 7: THE COMPLETE IDENTITY =====
|
||||
{
|
||||
id: 'variants-intro',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<p className="text-slate-500 text-lg tracking-[0.2em] uppercase mb-8">Logo Variants</p>
|
||||
<h1 className="text-5xl font-semibold text-white text-center">
|
||||
One identity.<br />
|
||||
<span className="text-slate-400">Many contexts.</span>
|
||||
</h1>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'variants-showcase',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full px-12">
|
||||
<div className="grid grid-cols-3 gap-12">
|
||||
<div className="flex flex-col items-center">
|
||||
<WhyMyRatingLogo size={80} variant="icon" colorScheme="dark" />
|
||||
<p className="text-slate-400 mt-4 text-sm">Icon</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<WhyMyRatingLogo size={100} variant="primary" colorScheme="dark" />
|
||||
<p className="text-slate-400 mt-4 text-sm">Primary</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<WhyMyRatingLogo size={100} variant="full" colorScheme="dark" />
|
||||
<p className="text-slate-400 mt-4 text-sm">Full</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-12 mt-12">
|
||||
<div className="flex flex-col items-center">
|
||||
<WhyMyRatingLogo size={140} variant="horizontal-v2" colorScheme="dark" />
|
||||
<p className="text-slate-400 mt-4 text-sm">Horizontal</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<WhyMyRatingLogo size={140} variant="horizontal-full-v2" colorScheme="dark" />
|
||||
<p className="text-slate-400 mt-4 text-sm">Horizontal Full</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
// ===== FINALE =====
|
||||
{
|
||||
id: 'tagline-reveal',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<div className="mb-12">
|
||||
<WhyMyRatingLogo size={200} variant="full" colorScheme="dark" />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'tagline',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full px-12">
|
||||
<p className="text-slate-500 text-lg tracking-[0.2em] uppercase mb-8">Our Tagline</p>
|
||||
<h1 className="text-7xl font-bold text-white text-center">
|
||||
The story behind<br />
|
||||
<span className="text-amber-400">your stars.</span>
|
||||
</h1>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'end',
|
||||
content: (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<WhyMyRatingLogo size={120} variant="horizontal-v2" colorScheme="dark" />
|
||||
<p className="text-slate-500 mt-8 text-lg">Brand Guidelines v2.0</p>
|
||||
<p className="text-slate-600 mt-2">Press ESC to exit</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const nextSlide = useCallback(() => {
|
||||
if (currentSlide < slides.length - 1 && !isAnimating) {
|
||||
setIsAnimating(true);
|
||||
setCurrentSlide(prev => prev + 1);
|
||||
setTimeout(() => setIsAnimating(false), 300);
|
||||
}
|
||||
}, [currentSlide, slides.length, isAnimating]);
|
||||
|
||||
const prevSlide = useCallback(() => {
|
||||
if (currentSlide > 0 && !isAnimating) {
|
||||
setIsAnimating(true);
|
||||
setCurrentSlide(prev => prev - 1);
|
||||
setTimeout(() => setIsAnimating(false), 300);
|
||||
}
|
||||
}, [currentSlide, isAnimating]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'ArrowRight' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
nextSlide();
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
e.preventDefault();
|
||||
prevSlide();
|
||||
} else if (e.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [nextSlide, prevSlide, onClose]);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black z-[100] flex flex-col">
|
||||
{/* Slide content */}
|
||||
<div
|
||||
className="flex-1 flex items-center justify-center transition-opacity duration-300"
|
||||
style={{ opacity: isAnimating ? 0.5 : 1 }}
|
||||
onClick={nextSlide}
|
||||
>
|
||||
{slides[currentSlide].content}
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
<div className="absolute bottom-0 left-0 right-0 h-1 bg-slate-800">
|
||||
<div
|
||||
className="h-full bg-amber-400 transition-all duration-300"
|
||||
style={{ width: `${((currentSlide + 1) / slides.length) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Slide counter */}
|
||||
<div className="absolute bottom-4 right-6 text-slate-600 text-sm font-mono">
|
||||
{currentSlide + 1} / {slides.length}
|
||||
</div>
|
||||
|
||||
{/* Navigation hints */}
|
||||
<div className="absolute bottom-4 left-6 text-slate-700 text-xs">
|
||||
← → Navigate • Space Next • ESC Exit
|
||||
</div>
|
||||
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-6 right-6 text-slate-600 hover:text-white transition-colors p-2"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
50
whyrating-templates/src/components/PreviewScaler.tsx
Normal file
50
whyrating-templates/src/components/PreviewScaler.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useState, useEffect, ReactNode } from 'react';
|
||||
|
||||
interface PreviewScalerProps {
|
||||
children: ReactNode;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export function PreviewScaler({ children, width, height }: PreviewScalerProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [scale, setScale] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
const updateScale = () => {
|
||||
if (containerRef.current) {
|
||||
const containerWidth = containerRef.current.offsetWidth;
|
||||
const newScale = Math.min(1, containerWidth / width);
|
||||
setScale(newScale);
|
||||
}
|
||||
};
|
||||
|
||||
updateScale();
|
||||
window.addEventListener('resize', updateScale);
|
||||
return () => window.removeEventListener('resize', updateScale);
|
||||
}, [width]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="w-full">
|
||||
<div
|
||||
style={{
|
||||
width: width * scale,
|
||||
height: height * scale,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: 'top left',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
274
whyrating-templates/src/components/WhyMyRatingLogo.tsx
Normal file
274
whyrating-templates/src/components/WhyMyRatingLogo.tsx
Normal file
@@ -0,0 +1,274 @@
|
||||
'use client';
|
||||
|
||||
import { useId } from 'react';
|
||||
|
||||
export type LogoVariant = 'icon' | 'primary' | 'full' | 'horizontal' | 'horizontal-full' | 'horizontal-v2' | 'horizontal-full-v2';
|
||||
export type ColorScheme = 'light' | 'dark' | 'mono-dark' | 'mono-light';
|
||||
|
||||
interface WhyMyRatingLogoProps {
|
||||
size?: number;
|
||||
variant?: LogoVariant;
|
||||
colorScheme?: ColorScheme;
|
||||
}
|
||||
|
||||
export function WhyMyRatingLogo({
|
||||
size = 120,
|
||||
variant = 'primary',
|
||||
colorScheme = 'light'
|
||||
}: WhyMyRatingLogoProps) {
|
||||
// Generate unique ID for clip paths
|
||||
const clipId = useId();
|
||||
|
||||
// Enforce minimum sizes per guidelines
|
||||
const minSize = variant === 'icon' ? 32 : 120;
|
||||
const u = Math.max(size, minSize);
|
||||
|
||||
const calc = {
|
||||
icon: u,
|
||||
wordmarkFont: u * 0.15,
|
||||
taglineFont: u * 0.092,
|
||||
gapIconToWordmark: u * 0.02,
|
||||
gapWordmarkToTagline: u * 0.05,
|
||||
clearSpace: u * 0.12,
|
||||
horizontalIcon: u * 0.60,
|
||||
horizontalWordmarkFont: u * 0.233,
|
||||
horizontalTaglineFont: u * 0.13,
|
||||
horizontalGap: u * 0.176,
|
||||
horizontalClearSpace: u * 0.05,
|
||||
// V2: Tighter proportions - less gap, bigger text
|
||||
// Makes text more prominent relative to icon
|
||||
v2Icon: u * 0.50,
|
||||
v2Gap: u * 0.10, // Much tighter gap
|
||||
v2WordmarkFont: u * 0.28, // Larger text
|
||||
v2TaglineFont: u * 0.15,
|
||||
v2ClearSpace: u * 0.02,
|
||||
};
|
||||
|
||||
const isDark = colorScheme === 'dark';
|
||||
const isMono = colorScheme === 'mono-dark' || colorScheme === 'mono-light';
|
||||
const monoColor = colorScheme === 'mono-light' ? '#FFFFFF' : '#1E293B';
|
||||
const monoContrast = colorScheme === 'mono-light' ? '#1E293B' : '#FFFFFF';
|
||||
|
||||
const colors = isMono ? {
|
||||
star: monoColor,
|
||||
magnifier: monoColor,
|
||||
lens: 'none',
|
||||
barLight: monoColor,
|
||||
barMid: monoColor,
|
||||
barDark: monoColor,
|
||||
wordmark: monoColor,
|
||||
accent: monoColor,
|
||||
tagline: monoColor,
|
||||
stroke: monoContrast,
|
||||
strokeWidth: 2,
|
||||
} : {
|
||||
star: '#FBBC05',
|
||||
magnifier: '#1E293B',
|
||||
lens: '#FEF3C7',
|
||||
barLight: '#86EFAC',
|
||||
barMid: '#22C55E',
|
||||
barDark: '#15803D',
|
||||
wordmark: isDark ? '#FAFAF9' : '#1E293B',
|
||||
accent: '#F59E0B',
|
||||
tagline: isDark ? '#A8A29E' : '#64748B',
|
||||
stroke: isDark ? '#78716C' : 'none',
|
||||
strokeWidth: isDark ? 1.5 : 0,
|
||||
};
|
||||
|
||||
const LogoIcon = ({ iconSize }: { iconSize?: number }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 120 120"
|
||||
width={iconSize || calc.icon}
|
||||
height={iconSize || calc.icon}
|
||||
style={{ display: 'block', verticalAlign: 'middle' }}
|
||||
>
|
||||
<defs>
|
||||
<clipPath id={clipId}>
|
||||
<circle cx="60" cy="62" r="21"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
{isMono && (
|
||||
<polygon
|
||||
points="60,15 71.5,42 101,46 79.5,66 85,95 60,82 35,95 40.5,66 19,46 48.5,42"
|
||||
fill="none"
|
||||
stroke={colors.stroke}
|
||||
strokeWidth="8"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
)}
|
||||
<polygon
|
||||
points="60,15 71.5,42 101,46 79.5,66 85,95 60,82 35,95 40.5,66 19,46 48.5,42"
|
||||
fill={colors.star}
|
||||
stroke={colors.star}
|
||||
strokeWidth="6"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<g>
|
||||
{isDark && !isMono && (
|
||||
<line x1="83" y1="81" x2="95" y2="91" stroke="#78716C" strokeWidth="11" strokeLinecap="round"/>
|
||||
)}
|
||||
{isMono && (
|
||||
<line x1="83" y1="81" x2="95" y2="91" stroke={monoContrast} strokeWidth="12" strokeLinecap="round"/>
|
||||
)}
|
||||
{isMono && (
|
||||
<circle cx="60" cy="62" r="28" fill="none" stroke={monoContrast} strokeWidth="2"/>
|
||||
)}
|
||||
<circle cx="60" cy="62" r="27" fill={colors.magnifier}/>
|
||||
<line x1="83" y1="81" x2="95" y2="91" stroke={colors.magnifier} strokeWidth="9" strokeLinecap="round"/>
|
||||
{!isMono && <circle cx="60" cy="62" r="21" fill={colors.lens}/>}
|
||||
{isMono && (
|
||||
<circle cx="60" cy="62" r="21" fill={monoContrast} stroke={colors.stroke} strokeWidth="2"/>
|
||||
)}
|
||||
{/* Mono right bar - bottom portion */}
|
||||
{isMono && (
|
||||
<rect x="68" y="62" width="11" height="21" rx="1.5" fill={colors.barDark} stroke={monoContrast} strokeWidth="2" clipPath={`url(#${clipId})`}/>
|
||||
)}
|
||||
{/* Mono right bar - top portion */}
|
||||
{isMono && (
|
||||
<rect x="68" y="44" width="11" height="18" rx="1.5" fill={colors.barDark} stroke={monoContrast} strokeWidth="2"/>
|
||||
)}
|
||||
{/* Mono right bar - seam patch */}
|
||||
{isMono && <rect x="69" y="60" width="9" height="4" fill={colors.barDark}/>}
|
||||
{!isMono && <rect x="68" y="44" width="11" height="18" rx="1.5" fill={colors.barDark}/>}
|
||||
<g clipPath={`url(#${clipId})`}>
|
||||
<rect x="42" y="58" width="11" height="35" rx="1.5" fill={colors.barLight}/>
|
||||
<rect x="55" y="51" width="11" height="42" rx="1.5" fill={colors.barMid}/>
|
||||
{!isMono && <rect x="68" y="44" width="11" height="49" rx="1.5" fill={colors.barDark}/>}
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const Wordmark = ({ fontSize }: { fontSize?: number }) => (
|
||||
<span
|
||||
className="font-wordmark font-bold"
|
||||
style={{
|
||||
fontSize: fontSize || calc.wordmarkFont,
|
||||
color: colors.wordmark,
|
||||
lineHeight: 1.1,
|
||||
letterSpacing: '-0.02em',
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle'
|
||||
}}
|
||||
>
|
||||
whyrating<span style={{ color: colors.accent }}>.com</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
const Tagline = ({ fontSize }: { fontSize?: number }) => (
|
||||
<span
|
||||
className="font-body"
|
||||
style={{ fontSize: fontSize || calc.taglineFont, color: colors.tagline }}
|
||||
>
|
||||
The story behind your stars
|
||||
</span>
|
||||
);
|
||||
|
||||
// Icon only
|
||||
if (variant === 'icon') {
|
||||
return (
|
||||
<div className="inline-flex" style={{ padding: calc.clearSpace }}>
|
||||
<LogoIcon />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Primary (vertical stack: icon + wordmark)
|
||||
if (variant === 'primary') {
|
||||
return (
|
||||
<div className="inline-flex flex-col items-center" style={{ padding: calc.clearSpace }}>
|
||||
<LogoIcon />
|
||||
<div style={{ height: calc.gapIconToWordmark }} />
|
||||
<Wordmark />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Full (vertical stack: icon + wordmark + tagline)
|
||||
if (variant === 'full') {
|
||||
return (
|
||||
<div className="inline-flex flex-col items-center" style={{ padding: calc.clearSpace }}>
|
||||
<LogoIcon />
|
||||
<div style={{ height: calc.gapIconToWordmark }} />
|
||||
<Wordmark />
|
||||
<div style={{ height: calc.gapWordmarkToTagline }} />
|
||||
<Tagline />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Horizontal (icon left, wordmark right)
|
||||
if (variant === 'horizontal') {
|
||||
return (
|
||||
<div className="inline-flex items-center" style={{ padding: calc.horizontalClearSpace }}>
|
||||
<div style={{ marginTop: calc.horizontalIcon * 0.02 }}>
|
||||
<LogoIcon iconSize={calc.horizontalIcon} />
|
||||
</div>
|
||||
<div style={{ width: calc.horizontalGap }} />
|
||||
<Wordmark fontSize={calc.horizontalWordmarkFont} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Horizontal full (icon left, wordmark + tagline right)
|
||||
if (variant === 'horizontal-full') {
|
||||
return (
|
||||
<div className="inline-flex items-center" style={{ padding: calc.horizontalClearSpace }}>
|
||||
<LogoIcon iconSize={calc.horizontalIcon} />
|
||||
<div style={{ width: calc.horizontalGap }} />
|
||||
<div className="flex flex-col">
|
||||
<Wordmark fontSize={calc.horizontalWordmarkFont} />
|
||||
<div style={{ height: calc.gapWordmarkToTagline * 0.5 }} />
|
||||
<Tagline fontSize={calc.horizontalTaglineFont} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Horizontal V2 - Correct proportions: Gap = 0.44H, Text = 0.54H (H = star height)
|
||||
if (variant === 'horizontal-v2') {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
padding: calc.v2ClearSpace
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<LogoIcon iconSize={calc.v2Icon} />
|
||||
</div>
|
||||
<div style={{ width: calc.v2Gap }} />
|
||||
<Wordmark fontSize={calc.v2WordmarkFont} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Horizontal Full V2 - Star centered with text block (wordmark + tagline)
|
||||
if (variant === 'horizontal-full-v2') {
|
||||
// Calculate total text block height (wordmark + gap + tagline)
|
||||
const textGap = u * 0.02;
|
||||
const totalTextHeight = calc.v2WordmarkFont + textGap + calc.v2TaglineFont;
|
||||
// Icon size so star spans full text height (star is 66.7% of icon)
|
||||
const fullIconSize = totalTextHeight / 0.667;
|
||||
// Shift icon down to center star with text block
|
||||
const iconOffset = fullIconSize * 0.12;
|
||||
|
||||
return (
|
||||
<div className="inline-flex items-center" style={{ padding: calc.v2ClearSpace }}>
|
||||
<div style={{ marginTop: iconOffset }}>
|
||||
<LogoIcon iconSize={fullIconSize} />
|
||||
</div>
|
||||
<div style={{ width: calc.v2Gap }} />
|
||||
<div className="flex flex-col">
|
||||
<Wordmark fontSize={calc.v2WordmarkFont} />
|
||||
<div style={{ height: textGap }} />
|
||||
<Tagline fontSize={calc.v2TaglineFont} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
5
whyrating-templates/src/components/index.ts
Normal file
5
whyrating-templates/src/components/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { WhyMyRatingLogo } from './WhyMyRatingLogo';
|
||||
export type { LogoVariant, ColorScheme } from './WhyMyRatingLogo';
|
||||
export { AI_CONTEXT_MARKDOWN } from './tabs/AIContextTab';
|
||||
export { Presentation } from './Presentation';
|
||||
export { PreviewScaler } from './PreviewScaler';
|
||||
306
whyrating-templates/src/components/tabs/AIContextTab.tsx
Normal file
306
whyrating-templates/src/components/tabs/AIContextTab.tsx
Normal file
@@ -0,0 +1,306 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo } from 'react';
|
||||
import { marked } from 'marked';
|
||||
import { Section } from '../ui/Section';
|
||||
|
||||
// AI context markdown - condensed brand guidelines for AI systems
|
||||
export const AI_CONTEXT_MARKDOWN = `# whyrating.com Brand Context for AI Systems
|
||||
|
||||
> **Purpose:** This document provides AI systems with comprehensive brand guidelines for generating content, designs, copy, and code related to whyrating.com.
|
||||
|
||||
---
|
||||
|
||||
## Brand Identity
|
||||
|
||||
### What whyrating.com Is
|
||||
A review intelligence tool that helps local business owners understand what's really driving their Google rating — enterprise-grade insights at a small business price, no subscription required.
|
||||
|
||||
### Target Audience
|
||||
Small and medium business owners: bakery owners, dentists, auto shop managers, local service providers. People who want actionable insights without complexity, jargon, or enterprise pricing.
|
||||
|
||||
### Core Positioning
|
||||
|
||||
| Attribute | Value |
|
||||
|-----------|-------|
|
||||
| Tagline | "The story behind your stars" |
|
||||
| Primary CTA | "Analyze my reviews" |
|
||||
| Value Proposition | "Enterprise-grade insights. Small business price." |
|
||||
| Voice | Helpful expert, not salesy |
|
||||
| Feel | Professional but approachable |
|
||||
|
||||
### The Logo Story (One Sentence)
|
||||
"Your Google rating (yellow star) hides insights (dark magnifier) that reveal a path to growth (green bars)."
|
||||
|
||||
---
|
||||
|
||||
## Design System
|
||||
|
||||
### Key Principle
|
||||
> **Logo = Distinctive.** Creates recognition and differentiation.
|
||||
> **UI = Familiar.** Feels like tools users already know.
|
||||
|
||||
### Logo Colors (Reserved for Brand Mark Only)
|
||||
|
||||
| Element | HEX | RGB | Usage |
|
||||
|---------|-----|-----|-------|
|
||||
| Star | \`#FBBC05\` | rgb(251, 188, 5) | Google Yellow — instant recognition |
|
||||
| Magnifier | \`#1E293B\` | rgb(30, 41, 59) | Dark Slate — "we see deeper" |
|
||||
| Lens | \`#FEF3C7\` | rgb(254, 243, 199) | Light amber interior |
|
||||
| Bar Light | \`#86EFAC\` | rgb(134, 239, 172) | Shortest growth bar |
|
||||
| Bar Mid | \`#22C55E\` | rgb(34, 197, 94) | Medium growth bar |
|
||||
| Bar Dark | \`#15803D\` | rgb(21, 128, 61) | Tallest growth bar |
|
||||
|
||||
### UI Colors (Product Interface & Marketing)
|
||||
|
||||
| Role | HEX | RGB | Usage |
|
||||
|------|-----|-----|-------|
|
||||
| Primary | \`#4285F4\` | rgb(66, 133, 244) | CTAs, links, buttons, focus states |
|
||||
| Secondary | \`#1E40AF\` | rgb(30, 64, 175) | Hover states, depth |
|
||||
| Accent | \`#F59E0B\` | rgb(245, 158, 11) | Highlights, \`.com\` in wordmark |
|
||||
| Success | \`#34A853\` | rgb(52, 168, 83) | Success states, positive metrics |
|
||||
| Error | \`#EA4335\` | rgb(234, 67, 53) | Errors, warnings |
|
||||
| Dark | \`#1E293B\` | rgb(30, 41, 59) | Primary text |
|
||||
| Slate | \`#64748B\` | rgb(100, 116, 139) | Secondary text |
|
||||
| Light | \`#F8FAFC\` | rgb(248, 250, 252) | Page backgrounds |
|
||||
| Card | \`#FFFFFF\` | rgb(255, 255, 255) | Card backgrounds |
|
||||
| Dark BG | \`#1C1917\` | rgb(28, 25, 23) | Dark mode background (Stone 900) |
|
||||
|
||||
### Typography
|
||||
|
||||
| Context | Font | Weight | Size |
|
||||
|---------|------|--------|------|
|
||||
| Wordmark only | Nunito | 700 | Variable |
|
||||
| H1 | Inter | 700 | 36px |
|
||||
| H2 | Inter | 600 | 30px |
|
||||
| H3 | Inter | 600 | 24px |
|
||||
| H4 | Inter | 500 | 20px |
|
||||
| Body | Inter | 400 | 16px |
|
||||
| Small | Inter | 400 | 14px |
|
||||
| Caption | Inter | 400 | 12px |
|
||||
|
||||
**Line Heights:** Headings 1.2-1.3, Body 1.5-1.6
|
||||
|
||||
**Font Loading:**
|
||||
\`\`\`html
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Nunito:wght@700&display=swap" rel="stylesheet">
|
||||
\`\`\`
|
||||
|
||||
### Border Radius
|
||||
- Brand radius: \`10px\` (professional yet approachable)
|
||||
|
||||
---
|
||||
|
||||
## Brand Voice
|
||||
|
||||
### Tone Principles
|
||||
1. **Helpful expert** — knowledgeable but never condescending
|
||||
2. **Plain language** — no jargon, no buzzwords
|
||||
3. **Respectful of time** — get to the point
|
||||
4. **Encouraging** — problems are fixable
|
||||
|
||||
### Writing Examples
|
||||
|
||||
#### DO ✅
|
||||
|
||||
| Context | Example |
|
||||
|---------|---------|
|
||||
| Headlines | "See exactly what's frustrating your customers" |
|
||||
| Value prop | "Fix the right problems first" |
|
||||
| Pricing | "Enterprise-grade insights. Small business price." |
|
||||
| CTA | "Analyze my reviews" |
|
||||
| Feature | "We read every review so you don't have to" |
|
||||
| Support | "Questions? Just reply to this email." |
|
||||
|
||||
#### DON'T ❌
|
||||
|
||||
| Context | Bad Example | Why |
|
||||
|---------|-------------|-----|
|
||||
| Headlines | "Leverage AI-powered sentiment analysis" | Jargon |
|
||||
| Value prop | "Unlock synergies in your feedback pipeline" | Buzzwords |
|
||||
| Pricing | "🚨 LIMITED TIME: 50% OFF!" | Salesy/desperate |
|
||||
| CTA | "Get started now!" | Generic, pushy |
|
||||
| Feature | "Revolutionary disruptive solution" | Empty hype |
|
||||
| Support | "Please submit a ticket" | Cold, corporate |
|
||||
|
||||
### Email Subject Lines
|
||||
- ✅ "Your June review summary is ready"
|
||||
- ✅ "3 things customers mentioned this week"
|
||||
- ❌ "🚨 URGENT: Don't miss your analytics!"
|
||||
- ❌ "You won't BELIEVE what customers said"
|
||||
|
||||
---
|
||||
|
||||
## Logo Usage Rules
|
||||
|
||||
### Logo Variants
|
||||
1. **Icon Only** — Star + magnifier + bars, no text (favicons, app icons, watermarks)
|
||||
2. **Primary (Vertical)** — Icon above, wordmark below (landing pages, marketing)
|
||||
3. **Full (Vertical)** — Icon + wordmark + tagline (splash screens, presentations)
|
||||
4. **Horizontal** — Icon left, wordmark right (headers, email signatures)
|
||||
5. **Horizontal Full** — Icon + wordmark + tagline horizontal (documentation)
|
||||
|
||||
### Minimum Sizes
|
||||
- **Full logo:** 120px width minimum
|
||||
- **Icon only:** 32px minimum
|
||||
|
||||
### Logo Misuse — DO NOT:
|
||||
- Change logo colors outside approved variations
|
||||
- Stretch, skew, or distort the logo
|
||||
- Add effects (shadows, gradients, outlines)
|
||||
- Place on busy backgrounds without contrast
|
||||
- Rearrange logo elements
|
||||
- Use Google Blue for the magnifier (makes us look like a Google product)
|
||||
|
||||
---
|
||||
|
||||
## CSS Variables Reference
|
||||
|
||||
\`\`\`css
|
||||
:root {
|
||||
/* Brand Logo Colors */
|
||||
--brand-star: #FBBC05;
|
||||
--brand-magnifier: #1E293B;
|
||||
--brand-lens: #FEF3C7;
|
||||
--brand-bar-light: #86EFAC;
|
||||
--brand-bar-mid: #22C55E;
|
||||
--brand-bar-dark: #15803D;
|
||||
--brand-accent: #F59E0B;
|
||||
|
||||
/* UI Colors */
|
||||
--ui-primary: #4285F4;
|
||||
--ui-primary-hover: #1E40AF;
|
||||
--ui-success: #34A853;
|
||||
--ui-error: #EA4335;
|
||||
|
||||
/* Surface Colors */
|
||||
--surface-page: #F8FAFC;
|
||||
--surface-card: #FFFFFF;
|
||||
--surface-muted: #F1F5F9;
|
||||
|
||||
/* Fonts */
|
||||
--font-sans: 'Inter', sans-serif;
|
||||
--font-wordmark: 'Nunito', sans-serif;
|
||||
|
||||
/* Border radius */
|
||||
--radius-brand: 10px;
|
||||
}
|
||||
|
||||
/* Dark mode */
|
||||
.dark {
|
||||
--surface-page: #1C1917;
|
||||
--surface-card: #292524;
|
||||
--surface-muted: #44403C;
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
|
||||
## AI Generation Rules
|
||||
|
||||
When generating content for whyrating.com:
|
||||
|
||||
### Copy/Text
|
||||
1. Use plain language — avoid jargon like "leverage", "synergy", "pipeline"
|
||||
2. Be direct and respectful of time
|
||||
3. Frame problems as fixable, tone should be encouraging
|
||||
4. Never use desperate/salesy language (🚨, "URGENT", "LIMITED TIME")
|
||||
5. Primary CTA is always "Analyze my reviews" or close variation
|
||||
|
||||
### Design/UI
|
||||
1. Use \`#4285F4\` (Primary Blue) for interactive elements
|
||||
2. Use \`#1E293B\` (Dark) for primary text
|
||||
3. Reserve logo colors for the logo only — don't use \`#FBBC05\` or greens in UI
|
||||
4. Use \`#F59E0B\` (Accent) sparingly for warmth and star-related visuals
|
||||
5. Border radius should be \`10px\` for cards/buttons
|
||||
6. Always support dark mode with warm Stone palette
|
||||
|
||||
### Code
|
||||
1. Use Inter font family for UI, Nunito only for wordmark
|
||||
2. Include font preconnect for performance
|
||||
3. Use CSS variables for colors
|
||||
4. Tailwind classes follow brand system
|
||||
|
||||
---
|
||||
|
||||
## Quick Copy Reference
|
||||
|
||||
| Need | Copy |
|
||||
|------|------|
|
||||
| Tagline | "The story behind your stars" |
|
||||
| Primary CTA | "Analyze my reviews" |
|
||||
| Value prop | "Enterprise-grade insights. Small business price." |
|
||||
| Problem statement | "Your Google rating tells customers whether to visit — but it doesn't tell you why it's dropping." |
|
||||
| Solution | "whyrating.com analyzes every review and shows you exactly what's frustrating customers, what they love, and what to fix first." |
|
||||
|
||||
---
|
||||
|
||||
*Version: 2.0 | Last updated: January 2025*
|
||||
`;
|
||||
|
||||
export function AIContextTab() {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const htmlContent = useMemo(() => {
|
||||
return marked.parse(AI_CONTEXT_MARKDOWN) as string;
|
||||
}, []);
|
||||
|
||||
const copyToClipboard = () => {
|
||||
navigator.clipboard.writeText(AI_CONTEXT_MARKDOWN).then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
});
|
||||
};
|
||||
|
||||
const downloadMarkdown = () => {
|
||||
const blob = new Blob([AI_CONTEXT_MARKDOWN], { type: 'text/markdown' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'whyrating-ai-context.md';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Section
|
||||
title="AI Context Document"
|
||||
description="Use this markdown as context/system prompt for AI systems generating whyrating.com content"
|
||||
>
|
||||
<div className="flex flex-wrap gap-3 mb-6">
|
||||
<button
|
||||
onClick={downloadMarkdown}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-[var(--ui-primary)] text-white rounded-lg text-sm font-medium hover:bg-[var(--ui-primary-hover)] transition-colors cursor-pointer"
|
||||
>
|
||||
↓ Download .md
|
||||
</button>
|
||||
<button
|
||||
onClick={copyToClipboard}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors cursor-pointer ${
|
||||
copied
|
||||
? 'bg-green-500 text-white'
|
||||
: 'bg-slate-100 dark:bg-stone-800 border border-slate-100 dark:border-stone-700 text-slate-700 dark:text-stone-300 hover:bg-slate-100 dark:hover:bg-stone-700/70'
|
||||
}`}
|
||||
>
|
||||
{copied ? '✓ Copied!' : 'Copy to clipboard'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-4 mb-6">
|
||||
<p className="text-sm text-amber-800 dark:text-amber-200">
|
||||
<strong>Usage:</strong> Copy this document and include it in your AI system prompt, Claude Project Knowledge,
|
||||
or as context when asking AI to generate content, code, or designs for whyrating.com.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="markdown-content bg-slate-50 dark:bg-stone-800 rounded-xl p-6 border border-slate-100 dark:border-stone-700"
|
||||
dangerouslySetInnerHTML={{ __html: htmlContent }}
|
||||
/>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
143
whyrating-templates/src/components/tabs/ColorsTab.tsx
Normal file
143
whyrating-templates/src/components/tabs/ColorsTab.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { logoColors, uiColors } from '@/lib/constants';
|
||||
|
||||
// Local helper components
|
||||
function CopyButton({ text, label }: { text: string; label?: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const copy = () => {
|
||||
navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
};
|
||||
return (
|
||||
<button
|
||||
onClick={copy}
|
||||
className={`px-2 py-1 text-xs font-mono rounded border transition-all cursor-pointer
|
||||
${copied
|
||||
? 'bg-green-500 border-green-500 text-white'
|
||||
: 'bg-white dark:bg-stone-800 border-slate-100 dark:border-stone-700 text-slate-600 dark:text-stone-400 hover:border-slate-300 dark:hover:border-stone-600'
|
||||
}`}
|
||||
>
|
||||
{copied ? 'Copied!' : label || text}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function Section({ title, description, children }: { title: string; description?: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<section className="bg-white dark:bg-stone-900 rounded-2xl p-8 shadow-sm mb-6 border border-slate-100 dark:border-stone-700/50">
|
||||
{title && <h2 className="text-xl font-semibold text-slate-900 dark:text-stone-50 mb-1">{title}</h2>}
|
||||
{description && <p className="text-sm text-slate-600 dark:text-stone-400 mb-6">{description}</p>}
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
interface ColorInfo {
|
||||
hex: string;
|
||||
name: string;
|
||||
rgb: string;
|
||||
usage?: string;
|
||||
element?: string;
|
||||
}
|
||||
|
||||
function ColorSwatch({ color }: { color: ColorInfo }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const copyHex = () => {
|
||||
navigator.clipboard.writeText(color.hex);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-stone-900 rounded-xl border border-slate-100 shadow-sm dark:border-stone-700 overflow-hidden">
|
||||
<div
|
||||
onClick={copyHex}
|
||||
className="h-24 flex items-center justify-center cursor-pointer transition-transform hover:scale-105"
|
||||
style={{ background: color.hex }}
|
||||
title="Click to copy HEX"
|
||||
>
|
||||
{copied && <span className="bg-white text-slate-800 px-3 py-1 rounded-full text-xs font-semibold">Copied!</span>}
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="text-sm font-semibold text-slate-900 dark:text-stone-50 mb-1">{color.name}</div>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<CopyButton text={color.hex} />
|
||||
<CopyButton text={`rgb(${color.rgb})`} label="RGB" />
|
||||
</div>
|
||||
{color.usage && <div className="text-xs text-slate-500 dark:text-stone-500 mt-2">{color.usage}</div>}
|
||||
{color.element && <div className="text-xs text-slate-500 dark:text-stone-500 mt-2">Element: {color.element}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ColorsTab() {
|
||||
const contrastItems = [
|
||||
{ combo: 'Dark on Light', colors: ['#1E293B', '#F8FAFC'], ratio: '12.6:1', status: 'AAA' },
|
||||
{ combo: 'Dark on White', colors: ['#1E293B', '#FFFFFF'], ratio: '14.5:1', status: 'AAA' },
|
||||
{ combo: 'White on Blue', colors: ['#FFFFFF', '#4285F4'], ratio: '4.6:1', status: 'AA' },
|
||||
{ combo: 'White on Dark', colors: ['#FFFFFF', '#1E293B'], ratio: '14.5:1', status: 'AAA' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Logo Colors */}
|
||||
<Section title="Logo Colors" description="Reserved exclusively for the brand mark. Do NOT use in UI.">
|
||||
<div className="grid grid-cols-6 gap-4">
|
||||
{Object.values(logoColors).map((color, i) => <ColorSwatch key={i} color={color} />)}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* UI Colors */}
|
||||
<Section title="UI Colors" description="For product interface, website, and marketing. Google-inspired for familiarity.">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{Object.values(uiColors).map((color, i) => <ColorSwatch key={i} color={color} />)}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Color Rules */}
|
||||
<Section title="Color Usage Rules">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="bg-green-50 dark:bg-green-900/20 rounded-xl p-6 border-l-4 border-green-500">
|
||||
<h4 className="text-sm font-semibold text-green-700 dark:text-green-300 mb-4">Do</h4>
|
||||
<ul className="text-sm text-green-600 dark:text-green-400 pl-5 list-disc space-y-2">
|
||||
<li>Use Primary Blue for all interactive elements</li>
|
||||
<li>Use Amber sparingly for warmth</li>
|
||||
<li>Use Success Green for positive metrics</li>
|
||||
<li>Use Dark for primary text</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="bg-red-50 dark:bg-red-900/20 rounded-xl p-6 border-l-4 border-red-500">
|
||||
<h4 className="text-sm font-semibold text-red-700 dark:text-red-300 mb-4">Don't</h4>
|
||||
<ul className="text-sm text-red-600 dark:text-red-400 pl-5 list-disc space-y-2">
|
||||
<li>Use Amber for text (fails accessibility)</li>
|
||||
<li>Use multiple bright colors competing</li>
|
||||
<li>Use logo greens in UI (reserved for logo)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Contrast */}
|
||||
<Section title="Contrast Ratios" description="WCAG AA/AAA compliant combinations">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{contrastItems.map((item, i) => (
|
||||
<div key={i} className="flex items-center gap-4 p-4 bg-slate-100 dark:bg-stone-800 rounded-lg">
|
||||
<div className="flex">
|
||||
<div className="w-8 h-8 rounded-l-lg" style={{ background: item.colors[0] }} />
|
||||
<div className="w-8 h-8 rounded-r-lg border border-slate-100 dark:border-stone-600" style={{ background: item.colors[1] }} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-slate-900 dark:text-stone-50">{item.combo}</div>
|
||||
<div className="text-xs text-slate-500 dark:text-stone-500">{item.ratio} <span className="text-green-500">✓ {item.status}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
612
whyrating-templates/src/components/tabs/DownloadsTab.tsx
Normal file
612
whyrating-templates/src/components/tabs/DownloadsTab.tsx
Normal file
@@ -0,0 +1,612 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { WhyMyRatingLogo, LogoVariant, ColorScheme } from '@/components';
|
||||
import { DownloadDropdown, DownloadOptionOrDivider } from '@/components/ui/DownloadDropdown';
|
||||
import { Section } from '@/components/ui/Section';
|
||||
|
||||
interface LogoAsset {
|
||||
name: string;
|
||||
file: string;
|
||||
type: string;
|
||||
desc: string;
|
||||
forDark: boolean;
|
||||
variant: LogoVariant;
|
||||
colorScheme: ColorScheme;
|
||||
}
|
||||
|
||||
// PNG download using API endpoint
|
||||
const downloadPng = async (
|
||||
variant: string,
|
||||
colorScheme: string,
|
||||
size: number,
|
||||
background: string,
|
||||
scale: number,
|
||||
filename: string
|
||||
) => {
|
||||
const params = new URLSearchParams({
|
||||
variant,
|
||||
colorScheme,
|
||||
size: size.toString(),
|
||||
background,
|
||||
scale: scale.toString()
|
||||
});
|
||||
const response = await fetch(`/api/generate-png?${params}`);
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
// Size presets with scale options
|
||||
const sizePresets: { label: string; size: number; scale: number; desc: string }[] = [
|
||||
// Standard sizes at 2x (good for most uses)
|
||||
{ label: 'Small @2x', size: 64, scale: 2, desc: '~160px output' },
|
||||
{ label: 'Medium @2x', size: 128, scale: 2, desc: '~320px output' },
|
||||
{ label: 'Large @2x', size: 256, scale: 2, desc: '~640px output' },
|
||||
{ label: 'XL @2x', size: 512, scale: 2, desc: '~1280px output' },
|
||||
// High-res at 3x for print/large displays
|
||||
{ label: 'Large @3x', size: 256, scale: 3, desc: '~960px output' },
|
||||
{ label: 'XL @3x', size: 512, scale: 3, desc: '~1920px output' },
|
||||
];
|
||||
|
||||
// Generate download options for a logo asset
|
||||
const getLogoDownloadOptions = (asset: LogoAsset): DownloadOptionOrDivider[] => {
|
||||
const baseName = asset.file.replace('.svg', '');
|
||||
|
||||
const options: DownloadOptionOrDivider[] = [
|
||||
{
|
||||
label: 'SVG (Vector)',
|
||||
sublabel: 'Scalable, best quality',
|
||||
action: () => {
|
||||
const link = document.createElement('a');
|
||||
link.href = `/assets/${asset.file}`;
|
||||
link.download = asset.file;
|
||||
link.click();
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
// Add size presets with background options
|
||||
sizePresets.forEach(preset => {
|
||||
const scaleLabel = preset.scale > 1 ? `@${preset.scale}x` : '';
|
||||
const filename = `${baseName}-${preset.size}px${scaleLabel}`;
|
||||
options.push({ divider: true, label: `PNG - ${preset.label}` });
|
||||
options.push({
|
||||
label: 'Transparent',
|
||||
sublabel: preset.desc,
|
||||
action: () => downloadPng(asset.variant, asset.colorScheme, preset.size, 'transparent', preset.scale, `${filename}.png`)
|
||||
});
|
||||
options.push({
|
||||
label: 'White BG',
|
||||
sublabel: preset.desc,
|
||||
action: () => downloadPng(asset.variant, asset.colorScheme, preset.size, 'white', preset.scale, `${filename}-white.png`)
|
||||
});
|
||||
options.push({
|
||||
label: 'Dark BG',
|
||||
sublabel: preset.desc,
|
||||
action: () => downloadPng(asset.variant, asset.colorScheme, preset.size, 'dark', preset.scale, `${filename}-dark.png`)
|
||||
});
|
||||
});
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
export function DownloadsTab() {
|
||||
const [darkMode, setDarkMode] = useState(false);
|
||||
|
||||
// Detect dark mode
|
||||
useEffect(() => {
|
||||
const checkDarkMode = () => {
|
||||
setDarkMode(document.documentElement.classList.contains('dark'));
|
||||
};
|
||||
checkDarkMode();
|
||||
|
||||
// Watch for changes
|
||||
const observer = new MutationObserver(checkDarkMode);
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const assets: LogoAsset[] = [
|
||||
// Icon variants
|
||||
{ name: 'Icon', file: 'whyrating-icon.svg', type: 'SVG', desc: 'Full color icon', forDark: false, variant: 'icon', colorScheme: 'light' },
|
||||
{ name: 'Icon (Dark BG)', file: 'whyrating-icon-dark-bg.svg', type: 'SVG', desc: 'For dark backgrounds', forDark: true, variant: 'icon', colorScheme: 'dark' },
|
||||
{ name: 'Icon Mono Dark', file: 'whyrating-icon-single-color-dark.svg', type: 'SVG', desc: 'Single color dark', forDark: false, variant: 'icon', colorScheme: 'mono-dark' },
|
||||
{ name: 'Icon Mono White', file: 'whyrating-icon-single-color-white.svg', type: 'SVG', desc: 'Single color white', forDark: true, variant: 'icon', colorScheme: 'mono-light' },
|
||||
// Primary variants (vertical with wordmark)
|
||||
{ name: 'Primary', file: 'whyrating-logo-primary.svg', type: 'SVG', desc: 'Icon + wordmark vertical', forDark: false, variant: 'primary', colorScheme: 'light' },
|
||||
{ name: 'Primary (Dark BG)', file: 'whyrating-logo-primary-dark-bg.svg', type: 'SVG', desc: 'For dark backgrounds', forDark: true, variant: 'primary', colorScheme: 'dark' },
|
||||
// Full variants (vertical with wordmark + tagline)
|
||||
{ name: 'Full', file: 'whyrating-logo-full.svg', type: 'SVG', desc: 'Icon + wordmark + tagline', forDark: false, variant: 'full', colorScheme: 'light' },
|
||||
{ name: 'Full (Dark BG)', file: 'whyrating-logo-full-dark-bg.svg', type: 'SVG', desc: 'For dark backgrounds', forDark: true, variant: 'full', colorScheme: 'dark' },
|
||||
// Horizontal variants
|
||||
{ name: 'Horizontal', file: 'whyrating-logo-horizontal.svg', type: 'SVG', desc: 'Icon + wordmark side by side', forDark: false, variant: 'horizontal', colorScheme: 'light' },
|
||||
{ name: 'Horizontal (Dark BG)', file: 'whyrating-logo-horizontal-dark-bg.svg', type: 'SVG', desc: 'For dark backgrounds', forDark: true, variant: 'horizontal', colorScheme: 'dark' },
|
||||
// Horizontal Full variants
|
||||
{ name: 'Horizontal Full', file: 'whyrating-logo-horizontal-full.svg', type: 'SVG', desc: 'Icon + wordmark + tagline', forDark: false, variant: 'horizontal-full', colorScheme: 'light' },
|
||||
{ name: 'Horizontal Full (Dark BG)', file: 'whyrating-logo-horizontal-full-dark-bg.svg', type: 'SVG', desc: 'For dark backgrounds', forDark: true, variant: 'horizontal-full', colorScheme: 'dark' },
|
||||
];
|
||||
|
||||
// Profile picture download options
|
||||
const profileOptions: DownloadOptionOrDivider[] = [
|
||||
{ label: '400x400', sublabel: 'Standard - Twitter, LinkedIn', action: () => downloadPng('icon', darkMode ? 'dark' : 'light', 400, 'white', 2, 'whyrating-profile-400x400.png') },
|
||||
{ label: '800x800', sublabel: 'High-res - Retina displays', action: () => downloadPng('icon', darkMode ? 'dark' : 'light', 800, 'white', 2, 'whyrating-profile-800x800.png') },
|
||||
{ label: '1024x1024', sublabel: 'Extra large - High DPI', action: () => downloadPng('icon', darkMode ? 'dark' : 'light', 1024, 'white', 2, 'whyrating-profile-1024x1024.png') },
|
||||
];
|
||||
|
||||
// Banner platforms
|
||||
const bannerPlatforms = [
|
||||
{ platform: 'LinkedIn', width: 1584, height: 396, ratio: '4:1' },
|
||||
{ platform: 'Facebook', width: 820, height: 312, ratio: '2.6:1' },
|
||||
{ platform: 'Twitter/X', width: 1500, height: 500, ratio: '3:1' },
|
||||
{ platform: 'YouTube', width: 2560, height: 1440, ratio: '16:9' },
|
||||
];
|
||||
|
||||
const downloadBanner = async (platform: string, width: number, height: number, scale: number = 1) => {
|
||||
// Create canvas for banner generation
|
||||
const canvas = document.createElement('canvas');
|
||||
const w = width * scale;
|
||||
const h = height * scale;
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const isYouTube = platform === 'YouTube';
|
||||
|
||||
// Background gradient
|
||||
const gradient = ctx.createLinearGradient(0, 0, w, h);
|
||||
gradient.addColorStop(0, '#1E293B');
|
||||
gradient.addColorStop(1, '#0F172A');
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
// Helper to draw a 5-point star
|
||||
const drawStar = (cx: number, cy: number, size: number, color: string, soft: boolean = false) => {
|
||||
if (soft) {
|
||||
ctx.shadowColor = color;
|
||||
ctx.shadowBlur = size * 0.15;
|
||||
}
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
const outerRadius = size * 0.5;
|
||||
const innerRadius = size * 0.2;
|
||||
for (let j = 0; j < 10; j++) {
|
||||
const radius = j % 2 === 0 ? outerRadius : innerRadius;
|
||||
const angle = (j * Math.PI / 5) - Math.PI / 2;
|
||||
const x = cx + Math.cos(angle) * radius;
|
||||
const y = cy + Math.sin(angle) * radius;
|
||||
if (j === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
if (soft) {
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to draw the full logo icon
|
||||
const drawLogoIcon = (cx: number, cy: number, iconSize: number) => {
|
||||
// Star
|
||||
const starSize = iconSize * 0.8;
|
||||
const starY = cy - iconSize * 0.15;
|
||||
drawStar(cx, starY, starSize, '#FBBC05', false);
|
||||
|
||||
// Magnifier circle (dark)
|
||||
const magRadius = iconSize * 0.22;
|
||||
const magY = starY + iconSize * 0.12;
|
||||
ctx.fillStyle = '#1E293B';
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, magY, magRadius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Lens (light yellow)
|
||||
ctx.fillStyle = '#FEF3C7';
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, magY, magRadius * 0.78, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Handle
|
||||
ctx.strokeStyle = '#1E293B';
|
||||
ctx.lineWidth = iconSize * 0.08;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(cx + magRadius * 0.7, magY + magRadius * 0.7);
|
||||
ctx.lineTo(cx + magRadius * 1.3, magY + magRadius * 1.3);
|
||||
ctx.stroke();
|
||||
|
||||
// Bars
|
||||
const barWidth = iconSize * 0.09;
|
||||
const barColors = ['#86EFAC', '#22C55E', '#15803D'];
|
||||
const barHeights = [0.25, 0.35, 0.45];
|
||||
barColors.forEach((color, idx) => {
|
||||
ctx.fillStyle = color;
|
||||
const barH = iconSize * barHeights[idx];
|
||||
const barX = cx - barWidth * 1.5 + idx * barWidth * 1.2;
|
||||
const barY = magY + magRadius * 0.5 - barH;
|
||||
ctx.fillRect(barX, barY, barWidth, barH);
|
||||
});
|
||||
};
|
||||
|
||||
// Draw decorative scattered stars
|
||||
const starPositions = isYouTube ? [
|
||||
{ x: 0.82, y: 0.18, size: 0.12, opacity: 0.18 },
|
||||
{ x: 0.18, y: 0.78, size: 0.1, opacity: 0.15 },
|
||||
{ x: 0.12, y: 0.15, size: 0.05, opacity: 0.1 },
|
||||
{ x: 0.88, y: 0.82, size: 0.06, opacity: 0.12 },
|
||||
] : [
|
||||
{ x: 0.72, y: 0.65, size: 0.28, opacity: 0.15 },
|
||||
{ x: 0.88, y: 0.25, size: 0.18, opacity: 0.12 },
|
||||
{ x: 0.95, y: 0.7, size: 0.1, opacity: 0.1 },
|
||||
{ x: 0.78, y: 0.15, size: 0.08, opacity: 0.08 },
|
||||
];
|
||||
|
||||
starPositions.forEach(star => {
|
||||
ctx.globalAlpha = star.opacity;
|
||||
drawStar(w * star.x, h * star.y, h * star.size, '#FBBC05', true);
|
||||
});
|
||||
ctx.globalAlpha = 1;
|
||||
|
||||
// Subtle gradient overlay on left for contrast
|
||||
const overlayGradient = ctx.createLinearGradient(0, 0, w * 0.5, 0);
|
||||
overlayGradient.addColorStop(0, 'rgba(15, 23, 42, 0.3)');
|
||||
overlayGradient.addColorStop(1, 'rgba(15, 23, 42, 0)');
|
||||
ctx.fillStyle = overlayGradient;
|
||||
ctx.fillRect(0, 0, w * 0.5, h);
|
||||
|
||||
// Logo positioning
|
||||
const iconSize = h * (isYouTube ? 0.3 : 0.85);
|
||||
const padding = h * 0.15;
|
||||
const logoX = isYouTube ? w / 2 : padding + iconSize * 0.6;
|
||||
const logoY = h / 2 - iconSize * 0.05;
|
||||
|
||||
drawLogoIcon(logoX, logoY, iconSize);
|
||||
|
||||
// Wordmark
|
||||
const fontSize = iconSize * (isYouTube ? 0.4 : 0.38);
|
||||
ctx.font = `700 ${fontSize}px Nunito, sans-serif`;
|
||||
ctx.textBaseline = 'middle';
|
||||
|
||||
const text1 = 'whyrating';
|
||||
const text2 = '.com';
|
||||
const width1 = ctx.measureText(text1).width;
|
||||
const width2 = ctx.measureText(text2).width;
|
||||
|
||||
if (isYouTube) {
|
||||
ctx.textAlign = 'left';
|
||||
const textX = w / 2 - (width1 + width2) / 2;
|
||||
const textY = logoY + iconSize * 0.7;
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
ctx.fillText(text1, textX, textY);
|
||||
ctx.fillStyle = '#F59E0B';
|
||||
ctx.fillText(text2, textX + width1, textY);
|
||||
|
||||
ctx.font = `400 ${fontSize * 0.45}px Nunito, sans-serif`;
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('The story behind your stars', w / 2, textY + fontSize * 0.8);
|
||||
} else {
|
||||
ctx.textAlign = 'left';
|
||||
const textX = logoX + iconSize * 0.65;
|
||||
const textY = h / 2 - fontSize * 0.3;
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
ctx.fillText(text1, textX, textY);
|
||||
ctx.fillStyle = '#F59E0B';
|
||||
ctx.fillText(text2, textX + width1, textY);
|
||||
|
||||
ctx.font = `400 ${fontSize * 0.45}px Nunito, sans-serif`;
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
|
||||
ctx.fillText('The story behind your stars', textX, textY + fontSize * 0.9);
|
||||
}
|
||||
|
||||
const link = document.createElement('a');
|
||||
const suffix = scale > 1 ? `-${scale}x` : '';
|
||||
link.download = `whyrating-banner-${platform.toLowerCase().replace('/', '-')}-${w}x${h}${suffix}.png`;
|
||||
link.href = canvas.toDataURL('image/png');
|
||||
link.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Logo Assets */}
|
||||
<Section title="Logo Assets" description="Download logo variations in SVG or PNG format">
|
||||
{/* Icon variants */}
|
||||
<h4 className="text-sm font-semibold text-slate-900 dark:text-stone-50 mb-3">Icon Only</h4>
|
||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||
{assets.filter(a => a.variant === 'icon').map((asset, i) => (
|
||||
<div key={i} className="flex items-center justify-between bg-slate-100 dark:bg-stone-800 rounded-xl p-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-14 h-14 rounded-[var(--radius-brand)] flex items-center justify-center border border-slate-100 dark:border-stone-600 ${asset.forDark ? 'bg-slate-800' : 'bg-white'}`}>
|
||||
<WhyMyRatingLogo size={32} variant={asset.variant} colorScheme={asset.colorScheme} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-slate-900 dark:text-stone-50">{asset.name}</div>
|
||||
<div className="text-xs text-slate-500 dark:text-stone-500">{asset.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
<DownloadDropdown options={getLogoDownloadOptions(asset)} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Primary variants (vertical) */}
|
||||
<h4 className="text-sm font-semibold text-slate-900 dark:text-stone-50 mb-3">Primary (Vertical)</h4>
|
||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||
{assets.filter(a => a.variant === 'primary').map((asset, i) => (
|
||||
<div key={i} className="flex flex-col bg-slate-100 dark:bg-stone-800 rounded-xl p-4">
|
||||
<div className={`w-full h-48 rounded-[var(--radius-brand)] flex items-center justify-center border border-slate-100 dark:border-stone-600 overflow-hidden p-4 ${asset.forDark ? 'bg-slate-800' : 'bg-white'}`}>
|
||||
<WhyMyRatingLogo size={120} variant={asset.variant} colorScheme={asset.colorScheme} />
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-3">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-slate-900 dark:text-stone-50">{asset.name}</div>
|
||||
<div className="text-xs text-slate-500 dark:text-stone-500">{asset.desc}</div>
|
||||
</div>
|
||||
<DownloadDropdown options={getLogoDownloadOptions(asset)} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Full variants (vertical with tagline) */}
|
||||
<h4 className="text-sm font-semibold text-slate-900 dark:text-stone-50 mb-3">Full (Vertical + Tagline)</h4>
|
||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||
{assets.filter(a => a.variant === 'full').map((asset, i) => (
|
||||
<div key={i} className="flex flex-col bg-slate-100 dark:bg-stone-800 rounded-xl p-4">
|
||||
<div className={`w-full h-56 rounded-[var(--radius-brand)] flex items-center justify-center border border-slate-100 dark:border-stone-600 overflow-hidden p-4 ${asset.forDark ? 'bg-slate-800' : 'bg-white'}`}>
|
||||
<WhyMyRatingLogo size={140} variant={asset.variant} colorScheme={asset.colorScheme} />
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-3">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-slate-900 dark:text-stone-50">{asset.name}</div>
|
||||
<div className="text-xs text-slate-500 dark:text-stone-500">{asset.desc}</div>
|
||||
</div>
|
||||
<DownloadDropdown options={getLogoDownloadOptions(asset)} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Horizontal variants */}
|
||||
<h4 className="text-sm font-semibold text-slate-900 dark:text-stone-50 mb-3">Horizontal</h4>
|
||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||
{assets.filter(a => a.variant === 'horizontal').map((asset, i) => (
|
||||
<div key={i} className="flex flex-col bg-slate-100 dark:bg-stone-800 rounded-xl p-4">
|
||||
<div className={`w-full h-24 rounded-[var(--radius-brand)] flex items-center justify-center border border-slate-100 dark:border-stone-600 overflow-hidden px-4 ${asset.forDark ? 'bg-slate-800' : 'bg-white'}`}>
|
||||
<WhyMyRatingLogo size={48} variant={asset.variant} colorScheme={asset.colorScheme} />
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-3">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-slate-900 dark:text-stone-50">{asset.name}</div>
|
||||
<div className="text-xs text-slate-500 dark:text-stone-500">{asset.desc}</div>
|
||||
</div>
|
||||
<DownloadDropdown options={getLogoDownloadOptions(asset)} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Horizontal Full variants */}
|
||||
<h4 className="text-sm font-semibold text-slate-900 dark:text-stone-50 mb-3">Horizontal Full (+ Tagline)</h4>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{assets.filter(a => a.variant === 'horizontal-full').map((asset, i) => (
|
||||
<div key={i} className="flex flex-col bg-slate-100 dark:bg-stone-800 rounded-xl p-4">
|
||||
<div className={`w-full h-28 rounded-[var(--radius-brand)] flex items-center justify-center border border-slate-100 dark:border-stone-600 overflow-hidden px-4 ${asset.forDark ? 'bg-slate-800' : 'bg-white'}`}>
|
||||
<WhyMyRatingLogo size={52} variant={asset.variant} colorScheme={asset.colorScheme} />
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-3">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-slate-900 dark:text-stone-50">{asset.name}</div>
|
||||
<div className="text-xs text-slate-500 dark:text-stone-500">{asset.desc}</div>
|
||||
</div>
|
||||
<DownloadDropdown options={getLogoDownloadOptions(asset)} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Favicon Sizes */}
|
||||
<Section title="Favicon Pack" description="Required sizes for all platforms">
|
||||
<div className="grid grid-cols-6 gap-4">
|
||||
{[
|
||||
{ size: 16, usage: 'Browser tab' },
|
||||
{ size: 32, usage: 'Browser (retina)' },
|
||||
{ size: 48, usage: 'Windows' },
|
||||
{ size: 180, usage: 'Apple touch' },
|
||||
{ size: 192, usage: 'Android' },
|
||||
{ size: 512, usage: 'PWA' },
|
||||
].map((item, i) => (
|
||||
<div key={i} className="bg-slate-100 dark:bg-stone-800 rounded-xl p-4 text-center">
|
||||
<div className="text-2xl font-bold text-slate-900 dark:text-stone-50">{item.size}</div>
|
||||
<div className="text-xs text-slate-500 dark:text-stone-500">{item.usage}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-4 p-4 bg-amber-50 dark:bg-amber-900/20 rounded-lg border-l-4 border-[var(--brand-accent)]">
|
||||
<p className="text-sm text-amber-800 dark:text-amber-200">
|
||||
<strong>Note:</strong> Favicon pack generation coming soon. For now, use the SVG and resize as needed.
|
||||
</p>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Social Assets */}
|
||||
<Section title="Social Media Assets" description="Profile pictures and banners for all platforms">
|
||||
{/* Profile Pictures */}
|
||||
<div className="mb-8">
|
||||
<h4 className="text-sm font-semibold text-slate-900 dark:text-stone-50 mb-4">Profile Pictures</h4>
|
||||
<div className="flex items-center justify-between bg-slate-100 dark:bg-stone-800 rounded-xl p-5">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-16 h-16 bg-white dark:bg-stone-900 rounded-full flex items-center justify-center border border-slate-100 dark:border-stone-600 overflow-hidden">
|
||||
<WhyMyRatingLogo size={48} variant="icon" colorScheme={darkMode ? 'dark' : 'light'} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-slate-900 dark:text-stone-50">Profile Picture</div>
|
||||
<div className="text-xs text-slate-500 dark:text-stone-500">Square icon for social media profiles</div>
|
||||
</div>
|
||||
</div>
|
||||
<DownloadDropdown options={profileOptions} buttonText="Download" buttonClass="px-4 py-2 text-sm" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Banners */}
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-slate-900 dark:text-stone-50 mb-4">Banners / Cover Images</h4>
|
||||
<div className="space-y-6">
|
||||
{bannerPlatforms.map((item, i) => {
|
||||
const aspectRatio = item.width / item.height;
|
||||
const logoSize = item.platform === 'YouTube' ? 180 : 200;
|
||||
|
||||
const bannerOptions: DownloadOptionOrDivider[] = [
|
||||
{ label: `Standard (${item.width}x${item.height})`, sublabel: 'Recommended size', action: () => downloadBanner(item.platform, item.width, item.height, 1) },
|
||||
{ label: `2x Retina (${item.width * 2}x${item.height * 2})`, sublabel: 'High DPI displays', action: () => downloadBanner(item.platform, item.width, item.height, 2) },
|
||||
];
|
||||
|
||||
return (
|
||||
<div key={i} className="bg-slate-100 dark:bg-stone-800 rounded-xl p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-slate-900 dark:text-stone-50">{item.platform}</div>
|
||||
<div className="text-xs text-slate-500 dark:text-stone-500">{item.width}x{item.height} - Ratio {item.ratio}</div>
|
||||
</div>
|
||||
<DownloadDropdown options={bannerOptions} buttonText="Download" buttonClass="px-4 py-2 text-sm" />
|
||||
</div>
|
||||
<div
|
||||
className="w-full bg-gradient-to-br from-slate-800 to-slate-900 rounded-[var(--radius-brand)] border border-slate-700 overflow-hidden relative"
|
||||
style={{ aspectRatio: aspectRatio }}
|
||||
>
|
||||
{/* Decorative stars */}
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
{item.platform === 'YouTube' ? (
|
||||
<>
|
||||
<div className="absolute text-[var(--brand-star)]" style={{ top: '15%', left: '80%', fontSize: '4rem', opacity: 0.18, filter: 'blur(1px)' }}>★</div>
|
||||
<div className="absolute text-[var(--brand-star)]" style={{ top: '75%', left: '15%', fontSize: '3rem', opacity: 0.15, filter: 'blur(1px)' }}>★</div>
|
||||
<div className="absolute text-[var(--brand-star)]" style={{ top: '12%', left: '10%', fontSize: '1.5rem', opacity: 0.1, filter: 'blur(0.5px)' }}>★</div>
|
||||
<div className="absolute text-[var(--brand-star)]" style={{ top: '80%', left: '85%', fontSize: '1.8rem', opacity: 0.12, filter: 'blur(0.5px)' }}>★</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="absolute text-[var(--brand-star)]" style={{ top: '50%', left: '72%', fontSize: '3.5rem', opacity: 0.15, filter: 'blur(1px)', transform: 'translateY(-50%)' }}>★</div>
|
||||
<div className="absolute text-[var(--brand-star)]" style={{ top: '20%', left: '88%', fontSize: '2.2rem', opacity: 0.12, filter: 'blur(0.5px)' }}>★</div>
|
||||
<div className="absolute text-[var(--brand-star)]" style={{ top: '65%', left: '95%', fontSize: '1.2rem', opacity: 0.1, filter: 'blur(0.5px)' }}>★</div>
|
||||
<div className="absolute text-[var(--brand-star)]" style={{ top: '12%', left: '78%', fontSize: '1rem', opacity: 0.08, filter: 'blur(0.5px)' }}>★</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* Logo composition */}
|
||||
{item.platform === 'YouTube' ? (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||
<WhyMyRatingLogo size={logoSize} variant="full" colorScheme="dark" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="absolute inset-0 flex items-center" style={{ paddingLeft: '8%' }}>
|
||||
<WhyMyRatingLogo size={logoSize} variant="horizontal-full-v2" colorScheme="dark" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 bg-green-50 dark:bg-green-900/20 rounded-lg border-l-4 border-green-500">
|
||||
<p className="text-sm text-green-800 dark:text-green-200">
|
||||
<strong>Multiple sizes:</strong> Use the dropdown to download standard or 2x retina versions for each platform.
|
||||
</p>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Application Examples */}
|
||||
<Section title="Application Examples" description="How to use the logo in common contexts">
|
||||
{/* Website Header */}
|
||||
<div className="mb-6">
|
||||
<div className="text-xs text-slate-500 dark:text-stone-500 uppercase tracking-wider mb-3">Website Header</div>
|
||||
<div className="bg-white dark:bg-stone-900 border border-slate-100 dark:border-stone-700 rounded-xl p-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<WhyMyRatingLogo size={120} variant="horizontal-v2" colorScheme={darkMode ? 'dark' : 'light'} />
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm text-slate-600 dark:text-stone-400">Pricing</span>
|
||||
<span className="text-sm text-slate-600 dark:text-stone-400">Login</span>
|
||||
<button className="px-4 py-2 bg-[var(--ui-primary)] text-white rounded-lg text-sm font-medium">Get Started →</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 dark:text-stone-500 mt-2">Horizontal lockup, left-aligned - Primary Blue CTA - White or Light background</p>
|
||||
</div>
|
||||
|
||||
{/* Email Signature */}
|
||||
<div className="mb-6">
|
||||
<div className="text-xs text-slate-500 dark:text-stone-500 uppercase tracking-wider mb-3">Email Signature</div>
|
||||
<div className="bg-white dark:bg-stone-900 border border-slate-100 dark:border-stone-700 rounded-xl p-6 max-w-md">
|
||||
<div className="border-t border-slate-100 dark:border-stone-700 pt-4">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<WhyMyRatingLogo size={32} variant="icon" colorScheme={darkMode ? 'dark' : 'light'} />
|
||||
<span className="font-wordmark font-bold text-slate-900 dark:text-stone-50">whyrating<span className="text-[var(--brand-accent)]">.com</span></span>
|
||||
</div>
|
||||
<div className="text-sm text-slate-500 dark:text-stone-500 italic mb-1">The story behind your stars</div>
|
||||
<div className="text-sm text-slate-600 dark:text-stone-400">yourname@whyrating.com</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Social Media Profile */}
|
||||
<div className="mb-6">
|
||||
<div className="text-xs text-slate-500 dark:text-stone-500 uppercase tracking-wider mb-3">Social Media Profile</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="w-24 h-24 bg-white border border-slate-100 dark:border-stone-700 rounded-full flex items-center justify-center overflow-hidden">
|
||||
<WhyMyRatingLogo size={96} variant="icon" colorScheme="light" />
|
||||
</div>
|
||||
<div className="w-24 h-24 bg-slate-800 border border-slate-700 rounded-full flex items-center justify-center overflow-hidden">
|
||||
<WhyMyRatingLogo size={96} variant="icon" colorScheme="dark" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 dark:text-stone-500 mt-2">Icon-only version - Centered with padding - Works on light and dark</p>
|
||||
</div>
|
||||
|
||||
{/* Invoice */}
|
||||
<div>
|
||||
<div className="text-xs text-slate-500 dark:text-stone-500 uppercase tracking-wider mb-3">Invoice / Receipt</div>
|
||||
<div className="bg-white dark:bg-stone-900 border border-slate-100 dark:border-stone-700 rounded-xl p-6">
|
||||
<div className="flex justify-between items-start mb-8">
|
||||
<WhyMyRatingLogo size={120} variant="horizontal-v2" colorScheme={darkMode ? 'dark' : 'light'} />
|
||||
<div className="text-right">
|
||||
<div className="text-lg font-semibold text-slate-900 dark:text-stone-50">INVOICE</div>
|
||||
<div className="text-sm text-slate-500 dark:text-stone-500">#INV-2025-001</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-slate-400 dark:text-stone-600">Invoice content would go here...</div>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 dark:text-stone-500 mt-2">Horizontal lockup, top-left - Single color version acceptable for fax/print</p>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* File Naming */}
|
||||
<Section title="File Naming Convention">
|
||||
<div className="bg-slate-900 text-slate-100 p-4 rounded-lg font-mono text-sm leading-relaxed">
|
||||
whyrating-logo-primary.svg<br/>
|
||||
whyrating-logo-primary-dark-bg.svg<br/>
|
||||
whyrating-logo-horizontal.svg<br/>
|
||||
whyrating-logo-horizontal-dark-bg.svg<br/>
|
||||
whyrating-logo-horizontal-full.svg<br/>
|
||||
whyrating-icon.svg<br/>
|
||||
whyrating-icon-dark-bg.svg<br/>
|
||||
whyrating-icon-single-color-dark.svg<br/>
|
||||
whyrating-icon-single-color-white.svg<br/>
|
||||
whyrating-icon-16.png<br/>
|
||||
whyrating-icon-32.png<br/>
|
||||
whyrating-icon-48.png<br/>
|
||||
whyrating-icon-180.png<br/>
|
||||
whyrating-icon-192.png<br/>
|
||||
whyrating-icon-512.png<br/>
|
||||
whyrating-favicon.ico
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
582
whyrating-templates/src/components/tabs/GuidelinesTab.tsx
Normal file
582
whyrating-templates/src/components/tabs/GuidelinesTab.tsx
Normal file
@@ -0,0 +1,582 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { marked } from 'marked';
|
||||
import { Section } from '../ui/Section';
|
||||
|
||||
const brandGuidelinesMarkdown = `# whyrating.com Brand Guidelines
|
||||
|
||||
---
|
||||
|
||||
## Executive Overview
|
||||
|
||||
### What We Are
|
||||
A review intelligence tool that helps local business owners understand what's really driving their Google rating — enterprise-grade insights at a small business price, no subscription required.
|
||||
|
||||
### Who We Serve
|
||||
Small and medium business owners (bakeries, dentists, auto shops) who want actionable insights without complexity, jargon, or enterprise pricing.
|
||||
|
||||
### Brand Positioning
|
||||
|
||||
| Attribute | whyrating.com |
|
||||
|-----------|-------------|
|
||||
| Voice | Helpful expert, not salesy |
|
||||
| Feel | Professional but approachable |
|
||||
| Promise | "The story behind your stars" |
|
||||
|
||||
### Visual Identity at a Glance
|
||||
|
||||
| Element | Decision | Strategic Reason |
|
||||
|---------|----------|------------------|
|
||||
| **Star** | Google Yellow #FBBC05 | Instant "Google reviews" recognition |
|
||||
| **Magnifier** | Dark #1E293B | "We see what Google doesn't" — differentiation |
|
||||
| **Growth bars** | Custom greens | Visualizes improvement — our value prop |
|
||||
| **Wordmark** | Nunito Bold | Friendly, approachable for SMBs |
|
||||
| **UI palette** | Google-inspired blues | Familiar interface, reduced learning curve |
|
||||
|
||||
### Key Design Principle
|
||||
> **Logo = Distinctive.** Creates recognition and differentiation.
|
||||
> **UI = Familiar.** Feels like tools users already know.
|
||||
|
||||
### The Logo Story in One Sentence
|
||||
"Your Google rating (yellow star) hides insights (dark magnifier) that reveal a path to growth (green bars)."
|
||||
|
||||
### Quick Reference
|
||||
|
||||
| Role | Value |
|
||||
|------|-------|
|
||||
| Primary tagline | "The story behind your stars" |
|
||||
| Primary CTA | "Analyze my reviews" |
|
||||
| Heading font | Inter (600/700) |
|
||||
| Wordmark font | Nunito (700) |
|
||||
| Body font | Inter (400/500) |
|
||||
|
||||
**Logo Colors:** #FBBC05 (star), #1E293B (magnifier), #86EFAC/#22C55E/#15803D (bars)
|
||||
|
||||
**UI Colors:** #4285F4 (primary), #1E40AF (secondary), #F59E0B (accent), #34A853 (success), #EA4335 (error)
|
||||
|
||||
---
|
||||
|
||||
## Brand Overview
|
||||
|
||||
### Mission
|
||||
whyrating.com helps local business owners understand what's really driving their Google rating so they can fix the right problems first — without reading hundreds of reviews or paying $300/month for enterprise software.
|
||||
|
||||
### Tagline
|
||||
**"The story behind your stars"**
|
||||
|
||||
### Brand Messaging
|
||||
|
||||
**PRIMARY (general use):**
|
||||
> whyrating.com helps local business owners understand what's really driving their Google rating so they can fix the right problems first — without reading hundreds of reviews or paying $300/month for enterprise software.
|
||||
|
||||
**EXPANDED (landing page):**
|
||||
> Your Google rating tells customers whether to visit — but it doesn't tell you why it's dropping. whyrating.com analyzes every review and shows you exactly what's frustrating customers, what they love, and what to fix first. Enterprise-grade insights. Small business price. No subscription.
|
||||
|
||||
### Brand Voice
|
||||
- **Tone:** Professional but approachable
|
||||
- **Language:** Plain language, no jargon
|
||||
- **Personality:** Helpful expert (not salesy)
|
||||
|
||||
### Target Audience
|
||||
Small and medium business owners — the bakery owner, the auto shop manager, the local dentist. People who want actionable insights, not enterprise complexity.
|
||||
|
||||
---
|
||||
|
||||
## Design Rationale
|
||||
|
||||
### The Strategic Challenge
|
||||
whyrating.com exists in a unique position: we analyze **Google** reviews, but we're **not** a Google product. Our visual identity needed to:
|
||||
|
||||
1. Create instant recognition of what we do (Google reviews)
|
||||
2. Avoid looking like a Google product clone
|
||||
3. Feel approachable to SMB owners (not enterprise/tech intimidating)
|
||||
4. Convey analytical depth and trust
|
||||
|
||||
### Why Two Color Systems
|
||||
|
||||
We deliberately use **different colors for the logo vs. the UI**.
|
||||
|
||||
| System | Colors | Purpose |
|
||||
|--------|--------|---------|
|
||||
| **Logo** | Google Yellow, Dark, Custom Greens | Distinctive brand mark with one Google anchor |
|
||||
| **UI** | Google Blue, Amber, Google palette | Familiar interface that feels like tools users already know |
|
||||
|
||||
**The reasoning:** Users spend seconds looking at a logo but hours in the interface. The logo needs to be memorable and distinctive. The UI needs to feel familiar and reduce cognitive load.
|
||||
|
||||
### The Google Alignment Trade-off
|
||||
|
||||
We considered three approaches:
|
||||
|
||||
| Approach | Pros | Cons |
|
||||
|----------|------|------|
|
||||
| **Full Google palette** | Maximum familiarity, feels integrated | Looks like a Google clone, no differentiation |
|
||||
| **Completely unique** | Maximum distinctiveness | Disconnected from what we do, learning curve |
|
||||
| **Hybrid (chosen)** | Recognition + differentiation | Requires careful balance |
|
||||
|
||||
**Our decision:** Hybrid approach. Use Google Yellow for the star (instant recognition), but take creative licenses elsewhere to establish our own identity.
|
||||
|
||||
### Creative License #1: Dark Magnifier
|
||||
|
||||
**The question:** Should the magnifying glass be Google Blue (#4285F4) or Dark (#1E293B)?
|
||||
|
||||
**We chose Dark. Here's why:**
|
||||
|
||||
| Google Blue Magnifier | Dark Magnifier |
|
||||
|-----------------------|----------------|
|
||||
| Looks like a Google product | Looks like a tool that analyzes Google |
|
||||
| "We're part of Google" | "We see what Google doesn't show you" |
|
||||
| Blends in | Stands out |
|
||||
| Familiar | Premium, analytical |
|
||||
|
||||
The dark magnifier communicates: "We look deeper." It's the visual representation of our value prop — revealing insights hidden beneath the surface.
|
||||
|
||||
### Creative License #2: Custom Green Gradient
|
||||
|
||||
**The question:** Should growth bars use Google Green (#34A853) or a custom gradient?
|
||||
|
||||
**We chose a 3-shade gradient (#86EFAC → #22C55E → #15803D). Here's why:**
|
||||
|
||||
| Single Google Green | Custom Gradient |
|
||||
|--------------------|-----------------|
|
||||
| Flat, static | Progressive, tells a story |
|
||||
| "Success" (binary) | "Growth over time" (journey) |
|
||||
| Google's color | Our color |
|
||||
|
||||
The ascending bars with deepening green visually communicate: "Things get better." This is literally what SMB owners want — improvement. The gradient makes it dynamic, not just a checkmark.
|
||||
|
||||
### Creative License #3: Nunito Wordmark
|
||||
|
||||
**The question:** Should we use Inter (clean, Google-like) or Nunito (rounded, friendly)?
|
||||
|
||||
**We chose Nunito for the wordmark only. Here's why:**
|
||||
|
||||
| Inter | Nunito |
|
||||
|-------|--------|
|
||||
| Clean, professional | Warm, approachable |
|
||||
| Tech/enterprise feel | Small business friendly |
|
||||
| Google-like | Human |
|
||||
| "Platform" | "Helper" |
|
||||
|
||||
Our brand voice is "helpful expert, not salesy." Nunito's rounded letterforms reflect that warmth. But we still use Inter for everything else to maintain professionalism.
|
||||
|
||||
### How Target Audience Shaped Decisions
|
||||
|
||||
Our users are **not** designers or tech executives. They're:
|
||||
- The bakery owner checking reviews between customers
|
||||
- The dentist who dreads looking at feedback
|
||||
- The auto shop manager who doesn't have time for complex tools
|
||||
|
||||
**This influenced:**
|
||||
|
||||
| Decision | User-Driven Reasoning |
|
||||
|----------|----------------------|
|
||||
| Warm font | "This feels friendly, not intimidating" |
|
||||
| Google Yellow star | "Oh, this is about my Google reviews" |
|
||||
| Google-like UI | "This works like tools I already know" |
|
||||
| Growth bars | "I can see things are improving" |
|
||||
|
||||
### Future Design Decisions
|
||||
|
||||
When making new design decisions, ask:
|
||||
|
||||
1. **Does this feel approachable to a small business owner?**
|
||||
2. **Does this connect to Google reviews without looking like a Google clone?**
|
||||
3. **Does this communicate insight, depth, or improvement?**
|
||||
4. **Does this match our "helpful expert" voice?**
|
||||
|
||||
If yes to all four, proceed. If not, reconsider.
|
||||
|
||||
---
|
||||
|
||||
## Logo
|
||||
|
||||
### Primary Logo
|
||||
The logo consists of three elements:
|
||||
1. **Star (Google Yellow)** — represents Google star ratings; creates instant recognition
|
||||
2. **Magnifying glass (Dark)** — represents deep analysis; "seeing what Google doesn't show"
|
||||
3. **Growth bars (Greens)** — represents improvement; the outcome SMBs want
|
||||
|
||||
### Logo Rationale
|
||||
|
||||
| Element | Color | Message |
|
||||
|---------|-------|---------|
|
||||
| Star | Google Yellow #FBBC05 | "This is about your Google rating" |
|
||||
| Magnifier | Dark #1E293B | "We reveal what's hidden underneath" |
|
||||
| Growth bars | Custom greens | "You'll see improvement" |
|
||||
|
||||
The dark magnifier differentiates us from looking like a Google product. We're the tool that **sees what Google doesn't show**.
|
||||
|
||||
### Clear Space
|
||||
Maintain clear space around the logo equal to the height of the letter "y" in "whyrating" on all sides.
|
||||
|
||||
### Minimum Size
|
||||
- **Full logo:** 120px width minimum (digital), 1 inch (print)
|
||||
- **Icon only:** 32px minimum (digital), 0.5 inch (print)
|
||||
|
||||
### Logo Misuse
|
||||
Do not:
|
||||
- Change logo colors outside of approved variations
|
||||
- Stretch, skew, or distort the logo
|
||||
- Add effects (shadows, gradients, outlines)
|
||||
- Place on busy backgrounds without sufficient contrast
|
||||
- Rearrange logo elements
|
||||
- Use Google Blue for the magnifier (this makes us look like a Google product)
|
||||
|
||||
---
|
||||
|
||||
## Color Palette
|
||||
|
||||
### Logo Colors
|
||||
Reserved exclusively for the brand mark.
|
||||
|
||||
| Color | Name | HEX | RGB | Element |
|
||||
|-------|------|-----|-----|---------|
|
||||
| 🟡 | Google Yellow | \`#FBBC05\` | rgb(251, 188, 5) | Star |
|
||||
| ⬛ | Dark | \`#1E293B\` | rgb(30, 41, 59) | Magnifier ring + handle |
|
||||
| 🟡 | Amber 100 | \`#FEF3C7\` | rgb(254, 243, 199) | Lens (interior) |
|
||||
| 🟢 | Green Light | \`#86EFAC\` | rgb(134, 239, 172) | Bar 1 (shortest) |
|
||||
| 🟢 | Green Mid | \`#22C55E\` | rgb(34, 197, 94) | Bar 2 (medium) |
|
||||
| 🟢 | Green Dark | \`#15803D\` | rgb(21, 128, 61) | Bar 3 (tallest) |
|
||||
|
||||
### UI Colors (Google-Inspired)
|
||||
For product interface, website, and marketing materials.
|
||||
|
||||
| Color | Name | HEX | RGB | Usage |
|
||||
|-------|------|-----|-----|-------|
|
||||
| 🔵 | Primary Blue | \`#4285F4\` | rgb(66, 133, 244) | CTAs, links, primary actions, focus states |
|
||||
| 🔵 | Secondary Blue | \`#1E40AF\` | rgb(30, 64, 175) | Hover states, depth, headers |
|
||||
| 🟡 | Accent Amber | \`#F59E0B\` | rgb(245, 158, 11) | Highlights, star ratings, decorative |
|
||||
| 🟢 | Success Green | \`#34A853\` | rgb(52, 168, 83) | Success states, positive metrics |
|
||||
| 🔴 | Error Red | \`#EA4335\` | rgb(234, 67, 53) | Errors, negative trends, warnings |
|
||||
| ⬛ | Dark | \`#1E293B\` | rgb(30, 41, 59) | Primary text, dark backgrounds |
|
||||
| ⬛ | Dark Mode BG | \`#1C1917\` | rgb(28, 25, 23) | Dark mode backgrounds (Stone 900 — warm neutral) |
|
||||
| 🔘 | Slate | \`#64748B\` | rgb(100, 116, 139) | Secondary text, captions |
|
||||
| ⬜ | Light | \`#F8FAFC\` | rgb(248, 250, 252) | Backgrounds, cards |
|
||||
| ⬜ | White | \`#FFFFFF\` | rgb(255, 255, 255) | Card backgrounds, inputs |
|
||||
|
||||
### Color Usage Rules
|
||||
|
||||
**Do:**
|
||||
- Use Primary Blue for all interactive elements (buttons, links)
|
||||
- Use Accent Amber sparingly for warmth and star-related visuals
|
||||
- Use Success Green for positive metrics and growth indicators
|
||||
- Use Dark for primary text
|
||||
|
||||
**Don't:**
|
||||
- Use Amber for text on light backgrounds (fails accessibility)
|
||||
- Use multiple bright colors competing for attention
|
||||
- Use logo greens in the UI (reserved for logo only)
|
||||
|
||||
### Contrast Ratios (WCAG AA Compliant)
|
||||
|
||||
| Combination | Ratio | Status |
|
||||
|-------------|-------|--------|
|
||||
| Dark on Light (#1E293B on #F8FAFC) | 12.6:1 | ✅ AAA |
|
||||
| Dark on White (#1E293B on #FFFFFF) | 14.5:1 | ✅ AAA |
|
||||
| Slate on Light (#64748B on #F8FAFC) | 4.7:1 | ✅ AA |
|
||||
| White on Primary Blue (#FFFFFF on #4285F4) | 4.6:1 | ✅ AA |
|
||||
| White on Dark (#FFFFFF on #1E293B) | 14.5:1 | ✅ AAA |
|
||||
|
||||
---
|
||||
|
||||
## Typography
|
||||
|
||||
### Font Family: Inter
|
||||
Single font family for consistency and faster load times.
|
||||
|
||||
| Context | Weight | Size | Usage |
|
||||
|---------|--------|------|-------|
|
||||
| Wordmark | Nunito 700 | 36px | "whyrating.com" only |
|
||||
| H1 | Inter 700 | 36px | Page titles |
|
||||
| H2 | Inter 600 | 30px | Section headings |
|
||||
| H3 | Inter 600 | 24px | Subsection headings |
|
||||
| H4 | Inter 500 | 20px | Card titles |
|
||||
| Body | Inter 400 | 16px | Paragraphs, content |
|
||||
| Small | Inter 400 | 14px | Secondary info |
|
||||
| Caption | Inter 400 | 12px | Labels, metadata |
|
||||
|
||||
### Typography Scale (1.25 Ratio)
|
||||
|
||||
\`\`\`
|
||||
36px → 30px → 24px → 20px → 16px → 14px → 12px
|
||||
\`\`\`
|
||||
|
||||
### Line Heights
|
||||
- **Headings:** 1.2 – 1.3
|
||||
- **Body:** 1.5 – 1.6
|
||||
|
||||
### Wordmark Exception
|
||||
The wordmark "whyrating.com" uses **Nunito Bold** for a friendlier, more approachable feel that matches our brand voice. All other text uses Inter.
|
||||
|
||||
### Font Loading
|
||||
|
||||
**HTML (recommended):**
|
||||
\`\`\`html
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Nunito:wght@700&display=swap" rel="stylesheet">
|
||||
\`\`\`
|
||||
|
||||
**CSS:**
|
||||
\`\`\`css
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Nunito:wght@700&display=swap');
|
||||
\`\`\`
|
||||
|
||||
**Font Stack:**
|
||||
\`\`\`css
|
||||
--font-primary: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
--font-wordmark: 'Nunito', 'Inter', sans-serif;
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
|
||||
## Logo Variations
|
||||
|
||||
### 1. Primary (Vertical Stack)
|
||||
- Icon above, text below, centered
|
||||
- **Use on:** Landing pages, about pages, marketing materials, splash screens
|
||||
|
||||
### 2. Icon Only
|
||||
- Star + magnifying glass + bars, no text
|
||||
- **Use on:** Favicon, app icon, social profile pictures, loading states, watermarks
|
||||
|
||||
### 3. Horizontal Lockup
|
||||
- Icon left, wordmark right, vertically centered
|
||||
- **Use on:** Website header, email signature, business cards, documentation
|
||||
|
||||
### 4. Horizontal Full Lockup
|
||||
- Icon left, wordmark + tagline right
|
||||
- **Use on:** Documentation headers, marketing footers, presentations
|
||||
|
||||
### 5. Dark Background Version
|
||||
- Background: Dark #1E293B or darker
|
||||
- Text: White #FFFFFF
|
||||
- Icon colors: Unchanged (yellow star pops on dark)
|
||||
- **Use on:** Dark mode UI, dark photography overlays, footer
|
||||
|
||||
### 6. Single Color Version
|
||||
- All elements in Dark #1E293B or White #FFFFFF
|
||||
- **Use on:** Watermarks, embroidery, single-color printing, fax (yes, SMBs still fax)
|
||||
|
||||
---
|
||||
|
||||
## Icon Specifications
|
||||
|
||||
### Favicon Sizes
|
||||
|
||||
| Size | Usage |
|
||||
|------|-------|
|
||||
| 16x16 | Browser tab |
|
||||
| 32x32 | Browser tab (retina) |
|
||||
| 48x48 | Windows site icon |
|
||||
| 180x180 | Apple touch icon |
|
||||
| 192x192 | Android Chrome |
|
||||
| 512x512 | PWA icon |
|
||||
|
||||
### Social Media Sizes
|
||||
|
||||
| Platform | Size |
|
||||
|----------|------|
|
||||
| Twitter/X | 400x400 |
|
||||
| LinkedIn | 300x300 |
|
||||
| Facebook | 170x170 |
|
||||
| Instagram | 110x110 |
|
||||
|
||||
---
|
||||
|
||||
## Brand Voice Guide
|
||||
|
||||
### Tone Principles
|
||||
1. **Helpful expert** — knowledgeable but never condescending
|
||||
2. **Plain language** — no jargon, no buzzwords
|
||||
3. **Respectful of time** — get to the point
|
||||
4. **Encouraging** — problems are fixable
|
||||
|
||||
### Do ✅
|
||||
|
||||
| Context | Example |
|
||||
|---------|---------|
|
||||
| Headlines | "See exactly what's frustrating your customers" |
|
||||
| Value prop | "Fix the right problems first" |
|
||||
| Pricing | "Enterprise-grade insights. Small business price." |
|
||||
| CTA | "Analyze my reviews" |
|
||||
| Feature | "We read every review so you don't have to" |
|
||||
| Support | "Questions? Just reply to this email." |
|
||||
|
||||
### Don't ❌
|
||||
|
||||
| Context | Example | Why |
|
||||
|---------|---------|-----|
|
||||
| Headlines | "Leverage AI-powered sentiment analysis" | Jargon |
|
||||
| Value prop | "Unlock synergies in your feedback pipeline" | Buzzwords |
|
||||
| Pricing | "🚨 LIMITED TIME: 50% OFF!" | Salesy/desperate |
|
||||
| CTA | "Get started now!" | Generic, pushy |
|
||||
| Feature | "Revolutionary disruptive solution" | Empty hype |
|
||||
| Support | "Please submit a ticket" | Cold, corporate |
|
||||
|
||||
### Email Subject Lines
|
||||
- ✅ "Your June review summary is ready"
|
||||
- ✅ "3 things customers mentioned this week"
|
||||
- ❌ "🚨 URGENT: Don't miss your analytics!"
|
||||
- ❌ "You won't BELIEVE what customers said"
|
||||
|
||||
### Handling Objections
|
||||
|
||||
**"I don't have time for another tool"**
|
||||
- ✅ "That's exactly why we built this — 5 minutes gives you what used to take hours."
|
||||
- ❌ "Our platform is incredibly easy to use with minimal onboarding!"
|
||||
|
||||
**"I already read my reviews"**
|
||||
- ✅ "You know what they said. We show you the patterns you'd miss."
|
||||
- ❌ "Our AI provides deeper insights than manual review reading."
|
||||
|
||||
---
|
||||
|
||||
## Application Examples
|
||||
|
||||
### Website Header
|
||||
\`\`\`
|
||||
[Icon 32px] whyrating.com [Pricing] [Login] [Get Started →]
|
||||
\`\`\`
|
||||
- Horizontal lockup, left-aligned
|
||||
- Primary Blue CTA button
|
||||
- Background: White or Light
|
||||
|
||||
### Email Signature
|
||||
\`\`\`
|
||||
---
|
||||
[Icon 24px] whyrating.com
|
||||
The story behind your stars
|
||||
yourname@whyrating.com
|
||||
\`\`\`
|
||||
|
||||
### Social Media Profile
|
||||
- Icon-only version
|
||||
- Centered with padding
|
||||
- Works on both light and dark backgrounds
|
||||
|
||||
### Invoice/Receipt
|
||||
- Horizontal lockup, top-left
|
||||
- Single color version acceptable
|
||||
|
||||
---
|
||||
|
||||
## File Naming Convention
|
||||
|
||||
\`\`\`
|
||||
whyrating-logo-primary.svg
|
||||
whyrating-logo-primary-dark-bg.svg
|
||||
whyrating-logo-horizontal.svg
|
||||
whyrating-logo-horizontal-dark-bg.svg
|
||||
whyrating-logo-single-color-dark.svg
|
||||
whyrating-logo-single-color-light.svg
|
||||
whyrating-icon.svg
|
||||
whyrating-icon-16.png
|
||||
whyrating-icon-32.png
|
||||
whyrating-icon-48.png
|
||||
whyrating-icon-180.png
|
||||
whyrating-icon-192.png
|
||||
whyrating-icon-512.png
|
||||
whyrating-favicon.ico
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Logo Colors
|
||||
| Element | HEX |
|
||||
|---------|-----|
|
||||
| Star | \`#FBBC05\` |
|
||||
| Magnifier | \`#1E293B\` |
|
||||
| Lens | \`#FEF3C7\` |
|
||||
| Bar 1 | \`#86EFAC\` |
|
||||
| Bar 2 | \`#22C55E\` |
|
||||
| Bar 3 | \`#15803D\` |
|
||||
|
||||
### UI Colors
|
||||
| Role | HEX |
|
||||
|------|-----|
|
||||
| Primary | \`#4285F4\` |
|
||||
| Secondary | \`#1E40AF\` |
|
||||
| Accent | \`#F59E0B\` |
|
||||
| Success | \`#34A853\` |
|
||||
| Error | \`#EA4335\` |
|
||||
| Dark | \`#1E293B\` |
|
||||
| Slate | \`#64748B\` |
|
||||
| Light | \`#F8FAFC\` |
|
||||
|
||||
### Typography
|
||||
| Role | Font |
|
||||
|------|------|
|
||||
| Wordmark | Nunito 700 |
|
||||
| Headings | Inter 600/700 |
|
||||
| Body | Inter 400/500 |
|
||||
|
||||
### Key Copy
|
||||
| Element | Text |
|
||||
|---------|------|
|
||||
| Tagline | "The story behind your stars" |
|
||||
| Primary CTA | "Analyze my reviews" |
|
||||
| Value prop | "Enterprise-grade insights. Small business price." |
|
||||
|
||||
---
|
||||
|
||||
*Last updated: January 2025*
|
||||
*Version: 2.0*
|
||||
`;
|
||||
|
||||
function copyToClipboard(text: string): void {
|
||||
navigator.clipboard.writeText(text).then(
|
||||
() => {
|
||||
alert('Markdown copied to clipboard!');
|
||||
},
|
||||
() => {
|
||||
alert('Failed to copy to clipboard');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function downloadMarkdown(): void {
|
||||
const blob = new Blob([brandGuidelinesMarkdown], { type: 'text/markdown' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'whyrating-brand-guidelines-v2.md';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
export function GuidelinesTab() {
|
||||
const htmlContent = useMemo(() => {
|
||||
return marked.parse(brandGuidelinesMarkdown) as string;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Section title="Brand Guidelines Document" description="Complete written guidelines in markdown format">
|
||||
<div className="flex gap-3 mb-6">
|
||||
<button
|
||||
onClick={downloadMarkdown}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-[var(--ui-primary)] text-white rounded-lg text-sm font-medium hover:bg-[var(--ui-primary-hover)] transition-colors cursor-pointer"
|
||||
>
|
||||
<span>↓</span> Download .md
|
||||
</button>
|
||||
<button
|
||||
onClick={() => copyToClipboard(brandGuidelinesMarkdown)}
|
||||
className="px-4 py-2 bg-slate-100 dark:bg-stone-800 border border-slate-100 dark:border-stone-700 rounded-lg text-sm font-medium text-slate-700 dark:text-stone-300 hover:bg-slate-100 dark:hover:bg-stone-700/70 transition-colors cursor-pointer"
|
||||
>
|
||||
Copy to clipboard
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="markdown-content bg-white dark:bg-stone-900 rounded-xl p-8 border border-slate-100 dark:border-stone-700"
|
||||
dangerouslySetInnerHTML={{ __html: htmlContent }}
|
||||
/>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
292
whyrating-templates/src/components/tabs/LogoTab.tsx
Normal file
292
whyrating-templates/src/components/tabs/LogoTab.tsx
Normal file
@@ -0,0 +1,292 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { WhyMyRatingLogo } from '@/components/WhyMyRatingLogo';
|
||||
|
||||
// Helper component for sections
|
||||
interface SectionProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function Section({ title, description, children }: SectionProps) {
|
||||
return (
|
||||
<section className="bg-white dark:bg-stone-900 rounded-2xl p-8 shadow-sm mb-6 border border-slate-100 dark:border-stone-700/50">
|
||||
{title && <h2 className="text-xl font-semibold text-slate-900 dark:text-stone-50 mb-1">{title}</h2>}
|
||||
{description && <p className="text-sm text-slate-600 dark:text-stone-400 mb-6">{description}</p>}
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Helper component for logo display cards
|
||||
interface LogoCardProps {
|
||||
children: React.ReactNode;
|
||||
label: string;
|
||||
bgColor?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function LogoCard({ children, label, bgColor, className = '' }: LogoCardProps) {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-3 flex-1">
|
||||
<div
|
||||
className={`p-6 rounded-xl flex items-center justify-center border border-slate-100 dark:border-stone-700 min-h-40 w-full overflow-hidden ${className}`}
|
||||
style={{ backgroundColor: bgColor || undefined }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<span className="text-xs font-medium text-slate-500 dark:text-stone-500 uppercase tracking-wider">{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Custom hook to detect dark mode
|
||||
function useDarkMode(): boolean {
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check initial state
|
||||
const checkDarkMode = () => {
|
||||
setIsDark(document.documentElement.classList.contains('dark'));
|
||||
};
|
||||
|
||||
checkDarkMode();
|
||||
|
||||
// Watch for changes
|
||||
const observer = new MutationObserver(checkDarkMode);
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class']
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return isDark;
|
||||
}
|
||||
|
||||
export function LogoTab() {
|
||||
const darkMode = useDarkMode();
|
||||
|
||||
const anatomyItems = [
|
||||
{ color: '#FBBC05', name: 'Star', reason: 'Google Yellow - instant recognition of "Google reviews"' },
|
||||
{ color: '#1E293B', name: 'Magnifier', reason: 'Dark Slate - "we look deeper"' },
|
||||
{ color: '#22C55E', name: 'Growth Bars', reason: 'Custom greens - shows progression, our value prop' },
|
||||
];
|
||||
|
||||
const misuseItems = [
|
||||
'Change logo colors',
|
||||
'Stretch or distort',
|
||||
'Add shadows/effects',
|
||||
'Use Google Blue for magnifier',
|
||||
'Rearrange elements',
|
||||
'Place on busy backgrounds',
|
||||
];
|
||||
|
||||
const verticalProportions = [
|
||||
['Icon (base)', '1u'],
|
||||
['Wordmark font', '0.15u'],
|
||||
['Tagline font', '0.092u'],
|
||||
['Gap: icon -> wordmark', '0.133u'],
|
||||
['Gap: wordmark -> tagline', '0.05u'],
|
||||
['Clear space', '0.12u'],
|
||||
];
|
||||
|
||||
const exampleProportions = [
|
||||
['Icon', '120px'],
|
||||
['Wordmark font', '18px'],
|
||||
['Tagline font', '11px'],
|
||||
['Gap: icon -> wordmark', '16px'],
|
||||
['Gap: wordmark -> tagline', '6px'],
|
||||
['Clear space', '14.4px'],
|
||||
];
|
||||
|
||||
const horizontalProportions = [
|
||||
['Icon size', '0.60u'],
|
||||
['Wordmark font', '0.233u'],
|
||||
['Tagline font', '0.13u'],
|
||||
['Gap: icon -> text', '0.176u'],
|
||||
['Clear space', '0.05u'],
|
||||
];
|
||||
|
||||
const tightFitRatios = [
|
||||
['Symbol : Text width', '1 : 4.7'],
|
||||
['Text height / Symbol height', '54%'],
|
||||
['Gap / Symbol height', '0.44'],
|
||||
['Star fills viewBox', '68% x 67%'],
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Anatomy */}
|
||||
<Section title="Logo Anatomy" description="Understanding the meaning behind each element">
|
||||
<div className="flex justify-center py-8">
|
||||
<div className="bg-slate-100 dark:bg-stone-800 rounded-2xl p-10 border-2 border-dashed border-slate-300 dark:border-stone-600">
|
||||
<WhyMyRatingLogo size={180} variant="icon" colorScheme={darkMode ? 'dark' : 'light'} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{anatomyItems.map((item, i) => (
|
||||
<div key={i} className="bg-white dark:bg-stone-900 border border-slate-100 dark:border-stone-700 rounded-xl p-5">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-8 h-8 rounded-lg" style={{ background: item.color }} />
|
||||
<span className="text-base font-semibold text-slate-900 dark:text-stone-50">{item.name}</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 dark:text-stone-400">{item.reason}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Variants - Vertical */}
|
||||
<Section title="Logo Variants - Vertical" description="Stacked configurations for marketing and splash screens">
|
||||
<div className="flex gap-6">
|
||||
<LogoCard label="Icon Only" className="bg-slate-100 dark:bg-stone-800">
|
||||
<WhyMyRatingLogo size={70} variant="icon" colorScheme={darkMode ? 'dark' : 'light'} />
|
||||
</LogoCard>
|
||||
<LogoCard label="Primary" className="bg-slate-100 dark:bg-stone-800">
|
||||
<WhyMyRatingLogo size={70} variant="primary" colorScheme={darkMode ? 'dark' : 'light'} />
|
||||
</LogoCard>
|
||||
<LogoCard label="Full Lockup" className="bg-slate-100 dark:bg-stone-800">
|
||||
<WhyMyRatingLogo size={70} variant="full" colorScheme={darkMode ? 'dark' : 'light'} />
|
||||
</LogoCard>
|
||||
</div>
|
||||
<div className="mt-6 p-4 bg-slate-100 dark:bg-stone-800 rounded-lg text-sm text-slate-600 dark:text-stone-400">
|
||||
<strong className="text-slate-900 dark:text-stone-50">Usage:</strong> Icon for favicons & social - Primary for headers & signatures - Full for marketing & splash screens
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Variants - Horizontal */}
|
||||
<Section title="Logo Variants - Horizontal" description="Side-by-side configurations for headers and navigation">
|
||||
<div className="flex flex-col gap-6">
|
||||
<LogoCard label="Horizontal" className="bg-slate-100 dark:bg-stone-800">
|
||||
<WhyMyRatingLogo size={120} variant="horizontal-v2" colorScheme={darkMode ? 'dark' : 'light'} />
|
||||
</LogoCard>
|
||||
<LogoCard label="Horizontal Full" className="bg-slate-100 dark:bg-stone-800">
|
||||
<WhyMyRatingLogo size={120} variant="horizontal-full-v2" colorScheme={darkMode ? 'dark' : 'light'} />
|
||||
</LogoCard>
|
||||
</div>
|
||||
<div className="mt-6 p-4 bg-slate-100 dark:bg-stone-800 rounded-lg text-sm text-slate-600 dark:text-stone-400">
|
||||
<strong className="text-slate-900 dark:text-stone-50">Usage:</strong> Horizontal for website headers, email signatures, business cards - Horizontal Full for documentation headers
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Single Color Versions */}
|
||||
<Section title="Single Color Versions" description="For watermarks, embroidery, single-color printing, and fax">
|
||||
<div className="flex gap-6">
|
||||
<LogoCard label="Mono Dark" bgColor="#F8FAFC">
|
||||
<WhyMyRatingLogo size={80} variant="primary" colorScheme="mono-dark" />
|
||||
</LogoCard>
|
||||
<LogoCard label="Mono Light" bgColor="#1E293B">
|
||||
<WhyMyRatingLogo size={80} variant="primary" colorScheme="mono-light" />
|
||||
</LogoCard>
|
||||
</div>
|
||||
<div className="mt-4 grid grid-cols-2 gap-4">
|
||||
<LogoCard label="Mono Dark - Horizontal" bgColor="#F8FAFC">
|
||||
<WhyMyRatingLogo size={120} variant="horizontal-v2" colorScheme="mono-dark" />
|
||||
</LogoCard>
|
||||
<LogoCard label="Mono Light - Horizontal" bgColor="#1E293B">
|
||||
<WhyMyRatingLogo size={120} variant="horizontal-v2" colorScheme="mono-light" />
|
||||
</LogoCard>
|
||||
</div>
|
||||
<div className="mt-6 p-4 bg-slate-100 dark:bg-stone-800 rounded-lg text-sm text-slate-600 dark:text-stone-400">
|
||||
<strong className="text-slate-900 dark:text-stone-50">Usage:</strong> Watermarks, embroidery, single-color printing, fax (yes, SMBs still fax)
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Scaling */}
|
||||
<Section title="Proportional Scaling" description="All elements scale with locked ratios. Minimum sizes are auto-enforced.">
|
||||
<div className="flex items-stretch justify-center gap-6 py-4">
|
||||
{[32, 48, 64, 80].map(size => (
|
||||
<LogoCard key={size} label={`${size}px`} className="bg-slate-100 dark:bg-stone-800">
|
||||
<WhyMyRatingLogo size={size} variant="icon" colorScheme={darkMode ? 'dark' : 'light'} />
|
||||
</LogoCard>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6 p-5 bg-blue-50 dark:bg-blue-900/20 rounded-xl border-l-4 border-[var(--ui-primary)]">
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||
<strong>Minimum sizes enforced:</strong> Icon variant = 32px minimum - All other variants = 120px minimum. Sizes below the minimum are automatically clamped.
|
||||
</p>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Background Modes */}
|
||||
<Section title="Background Modes" description="Logo is always transparent. Use colorScheme prop to set appropriate colors.">
|
||||
<div className="flex gap-6">
|
||||
<LogoCard label="Light BG" bgColor="#F8FAFC">
|
||||
<WhyMyRatingLogo size={80} variant="primary" colorScheme="light" />
|
||||
</LogoCard>
|
||||
<LogoCard label="Dark BG" bgColor="#1C1917">
|
||||
<WhyMyRatingLogo size={80} variant="primary" colorScheme="dark" />
|
||||
</LogoCard>
|
||||
<LogoCard label="Custom BG" className="bg-gradient-to-br from-amber-100 to-amber-200">
|
||||
<WhyMyRatingLogo size={80} variant="primary" colorScheme="light" />
|
||||
</LogoCard>
|
||||
</div>
|
||||
<div className="mt-6 p-5 bg-amber-50 dark:bg-amber-900/20 rounded-xl border-l-4 border-[var(--brand-accent)]">
|
||||
<p className="text-sm text-amber-800 dark:text-amber-200">
|
||||
<strong>SVG = Transparent:</strong> The logo SVG has no background. Use <code className="bg-amber-100 dark:bg-amber-900/40 px-1.5 py-0.5 rounded text-xs">colorScheme="dark"</code> when placing on dark backgrounds.
|
||||
</p>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Ratio System */}
|
||||
<Section title="Ratio System" description="1u = icon size. All elements scale proportionally.">
|
||||
<div className="grid grid-cols-2 gap-6 mb-6">
|
||||
<div className="bg-slate-100 dark:bg-stone-800 rounded-xl p-6">
|
||||
<h4 className="text-sm font-semibold text-slate-900 dark:text-stone-50 mb-4">Vertical Proportions</h4>
|
||||
{verticalProportions.map(([label, value], i, arr) => (
|
||||
<div key={i} className={`flex justify-between py-2.5 ${i < arr.length - 1 ? 'border-b border-slate-100 dark:border-stone-700' : ''}`}>
|
||||
<span className="text-slate-600 dark:text-stone-400">{label}</span>
|
||||
<span className="font-mono font-semibold text-slate-900 dark:text-stone-50">{value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="bg-slate-100 dark:bg-stone-800 rounded-xl p-6">
|
||||
<h4 className="text-sm font-semibold text-slate-900 dark:text-stone-50 mb-4">Example: 120px icon</h4>
|
||||
{exampleProportions.map(([label, value], i, arr) => (
|
||||
<div key={i} className={`flex justify-between py-2.5 ${i < arr.length - 1 ? 'border-b border-slate-100 dark:border-stone-700' : ''}`}>
|
||||
<span className="text-slate-600 dark:text-stone-400">{label}</span>
|
||||
<span className="font-mono font-semibold text-slate-900 dark:text-stone-50">{value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-xl p-6 border border-blue-200 dark:border-blue-800">
|
||||
<h4 className="text-sm font-semibold text-blue-900 dark:text-blue-100 mb-4">Horizontal Proportions</h4>
|
||||
{horizontalProportions.map(([label, value], i, arr) => (
|
||||
<div key={i} className={`flex justify-between py-2.5 ${i < arr.length - 1 ? 'border-b border-blue-200 dark:border-blue-800' : ''}`}>
|
||||
<span className="text-blue-700 dark:text-blue-300">{label}</span>
|
||||
<span className="font-mono font-semibold text-blue-900 dark:text-blue-100">{value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-xl p-6 border border-blue-200 dark:border-blue-800">
|
||||
<h4 className="text-sm font-semibold text-blue-900 dark:text-blue-100 mb-4">Tight-Fit Ratios (Measured)</h4>
|
||||
{tightFitRatios.map(([label, value], i, arr) => (
|
||||
<div key={i} className={`flex justify-between py-2.5 ${i < arr.length - 1 ? 'border-b border-blue-200 dark:border-blue-800' : ''}`}>
|
||||
<span className="text-blue-700 dark:text-blue-300">{label}</span>
|
||||
<span className="font-mono font-semibold text-blue-900 dark:text-blue-100">{value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Misuse */}
|
||||
<Section title="Logo Misuse" description="What NOT to do with the logo">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{misuseItems.map((item, i) => (
|
||||
<div key={i} className="bg-red-50 dark:bg-red-900/20 rounded-lg p-4 flex items-center gap-2">
|
||||
<span className="text-red-500 text-base">X</span>
|
||||
<span className="text-sm text-red-700 dark:text-red-300">{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
83
whyrating-templates/src/components/tabs/OverviewTab.tsx
Normal file
83
whyrating-templates/src/components/tabs/OverviewTab.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
'use client';
|
||||
|
||||
import { WhyMyRatingLogo } from '@/components/WhyMyRatingLogo';
|
||||
import { Section } from '@/components/ui/Section';
|
||||
|
||||
interface OverviewTabProps {
|
||||
darkMode: boolean;
|
||||
}
|
||||
|
||||
export function OverviewTab({ darkMode }: OverviewTabProps) {
|
||||
return (
|
||||
<div>
|
||||
{/* Hero */}
|
||||
<div className="text-center py-16 px-6 bg-slate-100 dark:bg-stone-800 rounded-2xl mb-8 border border-slate-100 dark:border-stone-700">
|
||||
<WhyMyRatingLogo size={200} variant="full" colorScheme={darkMode ? 'dark' : 'light'} />
|
||||
<p className="text-lg text-slate-600 dark:text-stone-400 mt-6 max-w-lg mx-auto">
|
||||
A review intelligence tool that helps local business owners understand what's really driving their Google rating.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quick Reference Cards */}
|
||||
<div className="grid grid-cols-3 gap-4 mb-8">
|
||||
<div className="bg-white dark:bg-stone-900 rounded-xl p-6 border border-slate-100 dark:border-stone-700">
|
||||
<div className="text-xs text-slate-400 dark:text-stone-500 uppercase tracking-wider mb-2">Tagline</div>
|
||||
<div className="text-lg font-semibold text-slate-900 dark:text-stone-50">"The story behind your stars"</div>
|
||||
</div>
|
||||
<div className="bg-white dark:bg-stone-900 rounded-xl p-6 border border-slate-100 dark:border-stone-700">
|
||||
<div className="text-xs text-slate-400 dark:text-stone-500 uppercase tracking-wider mb-2">Primary CTA</div>
|
||||
<div className="text-lg font-semibold text-[var(--ui-primary)]">"Analyze my reviews"</div>
|
||||
</div>
|
||||
<div className="bg-white dark:bg-stone-900 rounded-xl p-6 border border-slate-100 dark:border-stone-700">
|
||||
<div className="text-xs text-slate-400 dark:text-stone-500 uppercase tracking-wider mb-2">Voice</div>
|
||||
<div className="text-lg font-semibold text-slate-900 dark:text-stone-50">Helpful expert, not salesy</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Key Principle */}
|
||||
<Section>
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="p-6 bg-[var(--brand-star)] rounded-xl">
|
||||
<div className="text-sm font-semibold text-slate-800 mb-2">Logo = Distinctive</div>
|
||||
<div className="text-sm text-slate-700">Creates recognition and differentiation. Uses unique colors.</div>
|
||||
</div>
|
||||
<div className="p-6 bg-[var(--ui-primary)] rounded-xl">
|
||||
<div className="text-sm font-semibold text-white mb-2">UI = Familiar</div>
|
||||
<div className="text-sm text-white/90">Feels like tools users already know. Google-inspired palette.</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Logo Story */}
|
||||
<Section title="The Logo Story">
|
||||
<div className="bg-slate-800 rounded-xl p-8 text-center mb-6">
|
||||
<p className="text-xl text-white leading-relaxed">
|
||||
Your Google rating <span className="text-[var(--brand-star)]">(star)</span> hides insights <span className="text-slate-400">(magnifier)</span> that reveal a path to growth <span className="text-green-500">(bars)</span>.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{[
|
||||
{ icon: '\u2B50', color: '#FBBC05', title: 'Star', msg: '"This is about your Google rating"' },
|
||||
{ icon: '\uD83D\uDD0D', color: '#1E293B', title: 'Magnifier', msg: '"We see what Google doesn\'t show"' },
|
||||
{ icon: '\uD83D\uDCCA', color: '#22C55E', title: 'Bars', msg: '"You\'ll see improvement"' },
|
||||
].map((item, i) => (
|
||||
<div key={i} className="bg-slate-100 dark:bg-stone-800 rounded-xl p-5" style={{ borderLeft: `3px solid ${item.color}` }}>
|
||||
<div className="text-2xl mb-2">{item.icon}</div>
|
||||
<div className="text-sm font-semibold text-slate-900 dark:text-stone-50 mb-1">{item.title}</div>
|
||||
<div className="text-sm text-slate-600 dark:text-stone-400 italic">{item.msg}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Target Audience */}
|
||||
<Section title="Who We Serve">
|
||||
<p className="text-base text-slate-600 dark:text-stone-400 leading-relaxed">
|
||||
Small and medium business owners — the bakery owner, the auto shop manager, the local dentist.
|
||||
People who want <strong className="text-slate-900 dark:text-stone-50">actionable insights</strong>, not enterprise complexity. They don't have time for complex tools
|
||||
and don't want to pay $300/month for enterprise software.
|
||||
</p>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
519
whyrating-templates/src/components/tabs/ProportionsTab.tsx
Normal file
519
whyrating-templates/src/components/tabs/ProportionsTab.tsx
Normal file
@@ -0,0 +1,519 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
interface SectionProps {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function Section({ title, subtitle, children }: SectionProps) {
|
||||
return (
|
||||
<section className="bg-white dark:bg-stone-900 rounded-2xl p-8 shadow-sm mb-6 border border-slate-100 dark:border-stone-700/50">
|
||||
{title && <h2 className="text-xl font-semibold text-slate-900 dark:text-stone-50 mb-1">{title}</h2>}
|
||||
{subtitle && <p className="text-sm text-slate-600 dark:text-stone-400 mb-6">{subtitle}</p>}
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProportionsTab() {
|
||||
// Use a simple check for dark mode based on class on html element
|
||||
const [darkMode, setDarkMode] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const checkDarkMode = () => {
|
||||
setDarkMode(document.documentElement.classList.contains('dark'));
|
||||
};
|
||||
checkDarkMode();
|
||||
|
||||
// Watch for dark mode changes
|
||||
const observer = new MutationObserver(checkDarkMode);
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-10">
|
||||
{/* Intro */}
|
||||
<Section title="Logo Proportions System" subtitle="Tight-fit bounding boxes for precise measurement">
|
||||
<p className="text-slate-600 dark:text-stone-400 mb-6">
|
||||
Understanding logo proportions requires <strong className="text-slate-900 dark:text-stone-50">tight-fit bounding boxes</strong> —
|
||||
containers that touch the exact edges of visual elements with no extra padding. This ensures measurements are accurate and reproducible.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
{/* Concept: Tight vs Loose */}
|
||||
<Section title="Tight Fit vs Loose Fit" subtitle="Why measurement methodology matters">
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{/* Loose Fit (Incorrect) */}
|
||||
<div className="bg-red-50 dark:bg-red-900/20 rounded-xl p-6 border border-red-200 dark:border-red-800">
|
||||
<h4 className="text-sm font-semibold text-red-700 dark:text-red-300 mb-4 flex items-center gap-2">
|
||||
<span className="text-lg">✗</span> Loose Fit (Incorrect)
|
||||
</h4>
|
||||
<svg viewBox="0 0 280 140" className="w-full h-auto mb-4">
|
||||
<rect x="20" y="15" width="100" height="110" fill="none" stroke="#EF4444" strokeWidth="2" strokeDasharray="6 3"/>
|
||||
<polygon points="70,35 78,60 105,60 83,75 91,100 70,85 49,100 57,75 35,60 62,60" fill="#FBBC05"/>
|
||||
<rect x="20" y="15" width="100" height="18" fill="rgba(239,68,68,0.2)"/>
|
||||
<rect x="20" y="105" width="100" height="20" fill="rgba(239,68,68,0.2)"/>
|
||||
<rect x="140" y="35" width="120" height="70" fill="none" stroke="#22C55E" strokeWidth="2" strokeDasharray="6 3"/>
|
||||
<text x="200" y="77" fontSize="13" textAnchor="middle" fill={darkMode ? '#FAFAF9' : '#1E293B'} fontFamily="Arial">whyrating</text>
|
||||
<rect x="140" y="35" width="120" height="22" fill="rgba(239,68,68,0.2)"/>
|
||||
<rect x="140" y="87" width="120" height="18" fill="rgba(239,68,68,0.2)"/>
|
||||
</svg>
|
||||
<p className="text-sm text-red-600 dark:text-red-400">
|
||||
Extra padding distorts proportions — ratios become meaningless.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tight Fit (Correct) */}
|
||||
<div className="bg-green-50 dark:bg-green-900/20 rounded-xl p-6 border-2 border-[var(--brand-star)]">
|
||||
<h4 className="text-sm font-semibold text-green-700 dark:text-green-300 mb-4 flex items-center gap-2">
|
||||
<span className="text-lg">✓</span> Tight Fit (Correct)
|
||||
</h4>
|
||||
<svg viewBox="0 0 280 140" className="w-full h-auto mb-4">
|
||||
{/* Star with tight bbox */}
|
||||
<rect x="30" y="25" width="75" height="71" fill="none" stroke="#EF4444" strokeWidth="2.5" strokeDasharray="6 3"/>
|
||||
<polygon points="67.5,25 76.5,51 105,51 82,68 91,96 67.5,79 44,96 53,68 30,51 58.5,51" fill="#FBBC05"/>
|
||||
{/* Text with TRUE tight-fit bbox - getBBox(): width=124.5, height=17.7 */}
|
||||
<text x="190" y="68" fontSize="16" textAnchor="middle" fill={darkMode ? '#FAFAF9' : '#1E293B'} fontFamily="Arial" fontWeight="500">whyrating.com</text>
|
||||
<rect x="128" y="54" width="125" height="18" fill="none" stroke="#22C55E" strokeWidth="2.5" strokeDasharray="6 3"/>
|
||||
</svg>
|
||||
<p className="text-sm text-green-600 dark:text-green-400">
|
||||
Box edges touch content exactly — proportions are real and measurable.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 bg-amber-50 dark:bg-amber-900/20 border-l-4 border-[var(--brand-star)] rounded-r-lg">
|
||||
<p className="text-sm text-amber-800 dark:text-amber-200">
|
||||
<strong>Tight Fit:</strong> The bounding box must touch the extreme points of the object on all sides.
|
||||
No padding, no extra space. Only then are proportions real and useful for design.
|
||||
</p>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Star Anatomy */}
|
||||
<Section title="Star Bounding Box Anatomy" subtitle="5-point star with upward point orientation">
|
||||
<div className="bg-slate-50 dark:bg-stone-800 rounded-xl p-6">
|
||||
<svg viewBox="0 0 500 300" className="w-full h-auto max-w-2xl mx-auto">
|
||||
<defs>
|
||||
<pattern id="starGrid" width="10" height="10" patternUnits="userSpaceOnUse">
|
||||
<path d="M 10 0 L 0 0 0 10" fill="none" stroke={darkMode ? '#44403C' : '#E2E8F0'} strokeWidth="0.5"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect x="50" y="30" width="200" height="190" fill="url(#starGrid)"/>
|
||||
|
||||
{/* BBox */}
|
||||
<rect x="50" y="30" width="200" height="190" fill="none" stroke="#EF4444" strokeWidth="3"/>
|
||||
|
||||
{/* Star touching all edges */}
|
||||
<polygon points="150,30 174,103 250,103 190,148 212,220 150,177 88,220 110,148 50,103 126,103" fill="#FBBC05"/>
|
||||
|
||||
{/* Contact points */}
|
||||
<circle cx="150" cy="30" r="6" fill="#EF4444"/>
|
||||
<circle cx="250" cy="103" r="6" fill="#EF4444"/>
|
||||
<circle cx="212" cy="220" r="6" fill="#EF4444"/>
|
||||
<circle cx="88" cy="220" r="6" fill="#EF4444"/>
|
||||
<circle cx="50" cy="103" r="6" fill="#EF4444"/>
|
||||
|
||||
{/* Labels */}
|
||||
<text x="150" y="20" fontSize="10" fill="#EF4444" textAnchor="middle">↓ top (y=30)</text>
|
||||
<text x="268" y="108" fontSize="10" fill="#EF4444">← right</text>
|
||||
<text x="32" y="108" fontSize="10" fill="#EF4444" textAnchor="end">left →</text>
|
||||
<text x="150" y="240" fontSize="10" fill="#EF4444" textAnchor="middle">↑ bottom (y=220)</text>
|
||||
|
||||
{/* Dimensions */}
|
||||
<line x1="50" y1="260" x2="250" y2="260" stroke="#FBBC05" strokeWidth="2"/>
|
||||
<line x1="50" y1="255" x2="50" y2="265" stroke="#FBBC05" strokeWidth="2"/>
|
||||
<line x1="250" y1="255" x2="250" y2="265" stroke="#FBBC05" strokeWidth="2"/>
|
||||
<text x="150" y="280" fontSize="13" fill="#FBBC05" textAnchor="middle" fontWeight="600">W = 200px</text>
|
||||
|
||||
<line x1="280" y1="30" x2="280" y2="220" stroke="#FBBC05" strokeWidth="2"/>
|
||||
<line x1="275" y1="30" x2="285" y2="30" stroke="#FBBC05" strokeWidth="2"/>
|
||||
<line x1="275" y1="220" x2="285" y2="220" stroke="#FBBC05" strokeWidth="2"/>
|
||||
<text x="310" y="130" fontSize="13" fill="#FBBC05" fontWeight="600" transform="rotate(90 310 130)">H = 190px</text>
|
||||
|
||||
{/* Info box */}
|
||||
<text x="380" y="60" fontSize="11" fill={darkMode ? '#A8A29E' : '#64748B'}>5-point star</text>
|
||||
<text x="380" y="80" fontSize="11" fill={darkMode ? '#A8A29E' : '#64748B'}>(point upward):</text>
|
||||
<text x="380" y="105" fontSize="12" fill={darkMode ? '#FAFAF9' : '#1E293B'} fontWeight="600">Aspect ratio ≈ 1 : 0.95</text>
|
||||
<text x="380" y="140" fontSize="10" fill="#FBBC05">5 contact points:</text>
|
||||
<text x="380" y="157" fontSize="9" fill={darkMode ? '#A8A29E' : '#64748B'}>• 1 top (upper point)</text>
|
||||
<text x="380" y="172" fontSize="9" fill={darkMode ? '#A8A29E' : '#64748B'}>• 1 left (left point)</text>
|
||||
<text x="380" y="187" fontSize="9" fill={darkMode ? '#A8A29E' : '#64748B'}>• 1 right (right point)</text>
|
||||
<text x="380" y="202" fontSize="9" fill={darkMode ? '#A8A29E' : '#64748B'}>• 2 bottom (lower points)</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 bg-slate-100 dark:bg-stone-800 rounded-lg">
|
||||
<p className="text-sm text-slate-600 dark:text-stone-400">
|
||||
<strong className="text-slate-900 dark:text-stone-50">Geometry:</strong> In a 5-point star with the point facing up,
|
||||
the two lower points define the bottom edge of the bounding box. Both must touch exactly that edge.
|
||||
</p>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Text Anatomy */}
|
||||
<Section title="Text Bounding Box Anatomy" subtitle="Typography metrics and tight-fit boundaries">
|
||||
<div className="bg-slate-50 dark:bg-stone-800 rounded-xl p-6">
|
||||
<svg viewBox="0 0 600 200" className="w-full h-auto">
|
||||
{/* Typography lines */}
|
||||
<line x1="50" y1="50" x2="550" y2="50" stroke={darkMode ? '#44403C' : '#CBD5E1'} strokeWidth="1"/>
|
||||
<text x="560" y="54" fontSize="9" fill={darkMode ? '#78716C' : '#94A3B8'}>ascender</text>
|
||||
|
||||
<line x1="50" y1="75" x2="550" y2="75" stroke={darkMode ? '#57534E' : '#94A3B8'} strokeWidth="1"/>
|
||||
<text x="560" y="79" fontSize="9" fill={darkMode ? '#78716C' : '#94A3B8'}>cap height</text>
|
||||
|
||||
<line x1="50" y1="120" x2="550" y2="120" stroke={darkMode ? '#78716C' : '#64748B'} strokeWidth="1"/>
|
||||
<text x="560" y="124" fontSize="9" fill={darkMode ? '#A8A29E' : '#64748B'}>baseline</text>
|
||||
|
||||
<line x1="50" y1="145" x2="550" y2="145" stroke={darkMode ? '#44403C' : '#CBD5E1'} strokeWidth="1"/>
|
||||
<text x="560" y="149" fontSize="9" fill={darkMode ? '#78716C' : '#94A3B8'}>descender</text>
|
||||
|
||||
{/* Text */}
|
||||
<text x="70" y="120" fontSize="42" fill={darkMode ? '#FAFAF9' : '#1E293B'} fontFamily="Arial">whyrating.com</text>
|
||||
|
||||
{/* Tight BBox: getBBox() = x:70, y:82.1, width:326.7, height:47 */}
|
||||
<rect x="70" y="82" width="327" height="47" fill="none" stroke="#22C55E" strokeWidth="3" strokeDasharray="8 4"/>
|
||||
|
||||
{/* Contact points */}
|
||||
<circle cx="70" cy="105" r="5" fill="#22C55E"/>
|
||||
<circle cx="397" cy="105" r="5" fill="#22C55E"/>
|
||||
<circle cx="233" cy="82" r="5" fill="#22C55E"/>
|
||||
<circle cx="155" cy="129" r="5" fill="#22C55E"/>
|
||||
|
||||
{/* Dimension labels */}
|
||||
<line x1="70" y1="170" x2="397" y2="170" stroke="#FBBC05" strokeWidth="2"/>
|
||||
<text x="233" y="188" fontSize="12" fill="#FBBC05" textAnchor="middle" fontWeight="600">W text = 327px</text>
|
||||
|
||||
<line x1="30" y1="82" x2="30" y2="129" stroke="#FBBC05" strokeWidth="2"/>
|
||||
<text x="20" y="110" fontSize="11" fill="#FBBC05" transform="rotate(-90 20 110)">H = 47px</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 bg-slate-100 dark:bg-stone-800 rounded-lg">
|
||||
<p className="text-sm text-slate-600 dark:text-stone-400">
|
||||
<strong className="text-slate-900 dark:text-stone-50">For text:</strong> The bounding box extends from the first pixel of the initial character
|
||||
to the last pixel of the final character. Vertically: from cap-height to descender (if letters like g, y, p exist).
|
||||
</p>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Ratio Examples */}
|
||||
<Section title="Symbol-to-Text Ratios" subtitle="Different proportions for different contexts">
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
{/* Compact */}
|
||||
<div className="bg-slate-50 dark:bg-stone-800 rounded-xl p-5 border border-slate-100 dark:border-stone-700">
|
||||
<h4 className="text-sm font-medium text-slate-600 dark:text-stone-400 mb-4 text-center">Compact Symbol</h4>
|
||||
<svg viewBox="0 0 320 110" className="w-full h-auto mb-4">
|
||||
{/* Star tight bbox */}
|
||||
<rect x="20" y="20" width="55" height="52" fill="none" stroke="#EF4444" strokeWidth="2"/>
|
||||
<polygon points="47.5,20 55,39 75,39 59,51 66,72 47.5,60 29,72 36,51 20,39 40,39" fill="#FBBC05"/>
|
||||
{/* Text tight bbox: getBBox() = width:140, height:20 */}
|
||||
{/* Gap = 0.44 x 52 = 23px. Symbol ends at 75, text starts at 98 */}
|
||||
<text x="168" y="54" fontSize="18" textAnchor="middle" fill={darkMode ? '#FAFAF9' : '#1E293B'} fontFamily="Arial">whyrating.com</text>
|
||||
<rect x="98" y="38" width="140" height="20" fill="none" stroke="#22C55E" strokeWidth="2"/>
|
||||
<text x="47" y="88" fontSize="9" fill="#EF4444" textAnchor="middle">55 x 52</text>
|
||||
<text x="168" y="88" fontSize="9" fill="#22C55E" textAnchor="middle">140 x 20</text>
|
||||
</svg>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-slate-900 dark:text-stone-50">1 : 2.5</div>
|
||||
<div className="text-xs text-slate-500 dark:text-stone-500">Symbol width : Text width</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Optimal (highlighted) */}
|
||||
<div className="bg-gradient-to-br from-slate-900 to-slate-800 dark:from-stone-900 dark:to-stone-800 rounded-xl p-5 border-2 border-[var(--brand-star)]">
|
||||
<h4 className="text-sm font-medium text-[var(--brand-star)] mb-4 text-center flex items-center justify-center gap-2">
|
||||
<span>⭐</span> Optimal Balance
|
||||
</h4>
|
||||
<svg viewBox="0 0 320 110" className="w-full h-auto mb-4">
|
||||
{/* Star tight bbox */}
|
||||
<rect x="20" y="13" width="70" height="67" fill="none" stroke="#EF4444" strokeWidth="2.5"/>
|
||||
<polygon points="55,13 65,37 90,37 71,53 79,80 55,65 31,80 39,53 20,37 45,37" fill="#FBBC05"/>
|
||||
{/* Text tight bbox: getBBox() = width:171.1, height:24.4 */}
|
||||
<text x="205" y="53" fontSize="22" textAnchor="middle" fill="#FAFAF9" fontFamily="Arial" fontWeight="500">whyrating.com</text>
|
||||
<rect x="119" y="33" width="171" height="24" fill="none" stroke="#22C55E" strokeWidth="2.5"/>
|
||||
<text x="55" y="95" fontSize="9" fill="#EF4444" textAnchor="middle">70 x 67</text>
|
||||
<text x="205" y="95" fontSize="9" fill="#22C55E" textAnchor="middle">171 x 24</text>
|
||||
</svg>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-white">1 : 2.4</div>
|
||||
<div className="text-xs text-slate-400">Professional standard</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Prominent */}
|
||||
<div className="bg-slate-50 dark:bg-stone-800 rounded-xl p-5 border border-slate-100 dark:border-stone-700">
|
||||
<h4 className="text-sm font-medium text-slate-600 dark:text-stone-400 mb-4 text-center">Prominent Symbol</h4>
|
||||
<svg viewBox="0 0 340 110" className="w-full h-auto mb-4">
|
||||
{/* Star tight bbox */}
|
||||
<rect x="20" y="10" width="80" height="76" fill="none" stroke="#EF4444" strokeWidth="2"/>
|
||||
<polygon points="60,10 72,39 100,39 78,57 88,86 60,69 32,86 42,57 20,39 48,39" fill="#FBBC05"/>
|
||||
{/* Text tight bbox: getBBox() = width:187, height:27 */}
|
||||
{/* Gap = 0.44 x 76 = 33px. Symbol ends at 100, text starts at 133 */}
|
||||
<text x="227" y="55" fontSize="24" textAnchor="middle" fill={darkMode ? '#FAFAF9' : '#1E293B'} fontFamily="Arial" fontWeight="500">whyrating.com</text>
|
||||
<rect x="133" y="34" width="187" height="27" fill="none" stroke="#22C55E" strokeWidth="2"/>
|
||||
<text x="60" y="100" fontSize="9" fill="#EF4444" textAnchor="middle">80 x 76</text>
|
||||
<text x="227" y="100" fontSize="9" fill="#22C55E" textAnchor="middle">187 x 27</text>
|
||||
</svg>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-slate-900 dark:text-stone-50">1 : 2.3</div>
|
||||
<div className="text-xs text-slate-500 dark:text-stone-500">More dominant symbol</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Gap Measurement */}
|
||||
<Section title="Gap Measurement from Tight BBox" subtitle="Spacing defined by bounding box edges">
|
||||
<div className="bg-slate-900 dark:bg-stone-950 rounded-xl p-6">
|
||||
<svg viewBox="0 0 650 180" className="w-full h-auto">
|
||||
{/* Background */}
|
||||
<rect x="40" y="30" width="570" height="110" fill={darkMode ? '#1C1917' : '#0F172A'} rx="8"/>
|
||||
|
||||
{/* Star with tight bbox */}
|
||||
<rect x="70" y="42" width="90" height="86" fill="none" stroke="#EF4444" strokeWidth="2.5"/>
|
||||
<polygon points="115,42 129,74 160,74 136,94 147,128 115,109 83,128 94,94 70,74 101,74" fill="#FBBC05"/>
|
||||
|
||||
{/* Gap zone */}
|
||||
<rect x="160" y="50" width="38" height="70" fill="rgba(251,188,5,0.25)" stroke="#FBBC05" strokeWidth="2" strokeDasharray="6 3"/>
|
||||
|
||||
{/* Text with tight bbox: getBBox() = x:198, y:69.7, width:217.8, height:31.3 */}
|
||||
<text x="198" y="95" fontSize="28" fill="#FAFAF9" fontFamily="Arial" fontWeight="500">whyrating.com</text>
|
||||
<rect x="198" y="70" width="218" height="31" fill="none" stroke="#22C55E" strokeWidth="2.5"/>
|
||||
|
||||
{/* Gap dimension */}
|
||||
<line x1="160" y1="155" x2="198" y2="155" stroke="#FBBC05" strokeWidth="3"/>
|
||||
<line x1="160" y1="150" x2="160" y2="160" stroke="#FBBC05" strokeWidth="2"/>
|
||||
<line x1="198" y1="150" x2="198" y2="160" stroke="#FBBC05" strokeWidth="2"/>
|
||||
<text x="179" y="173" fontSize="12" fill="#FBBC05" textAnchor="middle" fontWeight="600">Gap = 38px</text>
|
||||
|
||||
{/* Height reference */}
|
||||
<line x1="30" y1="42" x2="30" y2="128" stroke="#EF4444" strokeWidth="2"/>
|
||||
<text x="20" y="90" fontSize="10" fill="#EF4444" transform="rotate(-90 20 90)">H = 86px</text>
|
||||
|
||||
{/* Ratio calculation */}
|
||||
<text x="480" y="165" fontSize="11" fill="#94A3B8">Gap / H symbol = 38 / 86 = </text>
|
||||
<text x="595" y="165" fontSize="13" fill="#FBBC05" fontWeight="600">0.44</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 bg-amber-50 dark:bg-amber-900/20 border-l-4 border-[var(--brand-star)] rounded-r-lg">
|
||||
<p className="text-sm text-amber-800 dark:text-amber-200">
|
||||
<strong>Spacing rule:</strong> The gap is measured from the right edge of the symbol's bounding box to the left edge of the text's bounding box.
|
||||
Optimal value: <strong>0.4x – 0.5x</strong> of the symbol height.
|
||||
</p>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Complete Specification */}
|
||||
<Section title="Complete Technical Specification" subtitle="All measurements from tight-fit bounding boxes">
|
||||
<div className="bg-slate-50 dark:bg-stone-800 rounded-xl p-6">
|
||||
<svg viewBox="0 0 700 300" className="w-full h-auto">
|
||||
<defs>
|
||||
<pattern id="specGrid" width="25" height="25" patternUnits="userSpaceOnUse">
|
||||
<path d="M 25 0 L 0 0 0 25" fill="none" stroke={darkMode ? '#44403C' : '#E2E8F0'} strokeWidth="0.5"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect x="50" y="40" width="600" height="160" fill="url(#specGrid)"/>
|
||||
<rect x="50" y="40" width="600" height="160" fill="none" stroke={darkMode ? '#57534E' : '#CBD5E1'} strokeWidth="1" rx="4"/>
|
||||
|
||||
{/* Symbol */}
|
||||
<rect x="80" y="55" width="100" height="95" fill="rgba(239,68,68,0.1)" stroke="#EF4444" strokeWidth="3"/>
|
||||
<polygon points="130,55 145,91 180,91 153,113 165,150 130,130 95,150 107,113 80,91 115,91" fill="#FBBC05"/>
|
||||
<text x="130" y="170" fontSize="10" fill="#EF4444" textAnchor="middle" fontWeight="600">SYMBOL</text>
|
||||
<text x="130" y="183" fontSize="9" fill={darkMode ? '#A8A29E' : '#64748B'} textAnchor="middle">100 x 95 px</text>
|
||||
|
||||
{/* Gap */}
|
||||
<rect x="180" y="75" width="42" height="65" fill="rgba(251,188,5,0.2)" stroke="#FBBC05" strokeWidth="2" strokeDasharray="6 3"/>
|
||||
<text x="201" y="170" fontSize="10" fill="#FBBC05" textAnchor="middle" fontWeight="600">GAP</text>
|
||||
<text x="201" y="183" fontSize="9" fill={darkMode ? '#A8A29E' : '#64748B'} textAnchor="middle">42px = 0.44xH</text>
|
||||
|
||||
{/* Text: getBBox() = x:222, y:89, width:249, height:36 */}
|
||||
<text x="222" y="118" fontSize="32" fill={darkMode ? '#FAFAF9' : '#1E293B'} fontFamily="Arial" fontWeight="500">whyrating.com</text>
|
||||
<rect x="222" y="89" width="249" height="36" fill="rgba(34,197,94,0.1)" stroke="#22C55E" strokeWidth="3"/>
|
||||
<text x="346" y="170" fontSize="10" fill="#22C55E" textAnchor="middle" fontWeight="600">LOGOTYPE</text>
|
||||
<text x="346" y="183" fontSize="9" fill={darkMode ? '#A8A29E' : '#64748B'} textAnchor="middle">249 x 36 px</text>
|
||||
|
||||
{/* Total width: 100 + 42 + 249 = 391px */}
|
||||
<line x1="80" y1="220" x2="471" y2="220" stroke={darkMode ? '#FAFAF9' : '#1E293B'} strokeWidth="2"/>
|
||||
<text x="276" y="238" fontSize="12" fill={darkMode ? '#FAFAF9' : '#1E293B'} textAnchor="middle" fontWeight="600">Total width: 391px</text>
|
||||
|
||||
{/* Ratios bar */}
|
||||
<rect x="80" y="255" width="391" height="30" fill={darkMode ? '#292524' : '#1E293B'} rx="4"/>
|
||||
<text x="100" y="275" fontSize="11" fill="#94A3B8">Final ratios:</text>
|
||||
<text x="195" y="275" fontSize="11" fill="#EF4444">W symbol</text>
|
||||
<text x="250" y="275" fontSize="11" fill="#FAFAF9">1</text>
|
||||
<text x="268" y="275" fontSize="11" fill="#64748B">:</text>
|
||||
<text x="285" y="275" fontSize="11" fill="#FBBC05">Gap</text>
|
||||
<text x="315" y="275" fontSize="11" fill="#FAFAF9">0.42</text>
|
||||
<text x="350" y="275" fontSize="11" fill="#64748B">:</text>
|
||||
<text x="368" y="275" fontSize="11" fill="#22C55E">W text</text>
|
||||
<text x="415" y="275" fontSize="11" fill="#FAFAF9">2.5</text>
|
||||
</svg>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Values Table */}
|
||||
<Section title="Proportion Values Reference" subtitle="Tight-fit measurement guidelines">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b-2 border-[var(--brand-star)]">
|
||||
<th className="py-4 px-4 text-left text-slate-500 dark:text-stone-500 font-medium">Measurement</th>
|
||||
<th className="py-4 px-4 text-center text-slate-500 dark:text-stone-500 font-medium">Range</th>
|
||||
<th className="py-4 px-4 text-right text-slate-500 dark:text-stone-500 font-medium">Optimal</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-200 dark:divide-stone-700">
|
||||
<tr>
|
||||
<td className="py-4 px-4 text-slate-900 dark:text-stone-50">W symbol : W text</td>
|
||||
<td className="py-4 px-4 text-center text-slate-600 dark:text-stone-400">1:2 — 1:4</td>
|
||||
<td className="py-4 px-4 text-right text-[var(--brand-star)] font-semibold">1 : 2.5 – 3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-4 px-4 text-slate-900 dark:text-stone-50">H text / H symbol</td>
|
||||
<td className="py-4 px-4 text-center text-slate-600 dark:text-stone-400">50% — 75%</td>
|
||||
<td className="py-4 px-4 text-right text-[var(--brand-star)] font-semibold">60%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-4 px-4 text-slate-900 dark:text-stone-50">Gap / H symbol</td>
|
||||
<td className="py-4 px-4 text-center text-slate-600 dark:text-stone-400">0.3x — 0.6x</td>
|
||||
<td className="py-4 px-4 text-right text-[var(--brand-star)] font-semibold">0.44x</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-4 px-4 text-slate-900 dark:text-stone-50">Vertical text offset</td>
|
||||
<td className="py-4 px-4 text-center text-slate-600 dark:text-stone-400">3% — 8% up</td>
|
||||
<td className="py-4 px-4 text-right text-[var(--brand-star)] font-semibold">5% up</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-4 px-4 text-slate-900 dark:text-stone-50">Complete ratio (S:G:T)</td>
|
||||
<td className="py-4 px-4 text-center text-slate-600 dark:text-stone-400">—</td>
|
||||
<td className="py-4 px-4 text-right text-[var(--brand-star)] font-semibold">1 : 0.42 : 4</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid md:grid-cols-2 gap-4">
|
||||
<div className="bg-slate-100 dark:bg-stone-800 rounded-lg p-4">
|
||||
<h4 className="text-sm font-semibold text-slate-900 dark:text-stone-50 mb-2">S:G:T Notation</h4>
|
||||
<p className="text-sm text-slate-600 dark:text-stone-400">
|
||||
<strong>S</strong> = Symbol width, <strong>G</strong> = Gap width, <strong>T</strong> = Text width.
|
||||
All measured from tight-fit bounding boxes.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-slate-100 dark:bg-stone-800 rounded-lg p-4">
|
||||
<h4 className="text-sm font-semibold text-slate-900 dark:text-stone-50 mb-2">Why 0.44x Gap?</h4>
|
||||
<p className="text-sm text-slate-600 dark:text-stone-400">
|
||||
This ratio creates visual breathing room while maintaining cohesion.
|
||||
Too tight feels cramped; too loose breaks the lockup.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Technical: Measurement Algorithm */}
|
||||
<Section title="Technical: Measurement Algorithm" subtitle="How tight-fit measurements are calculated (for developers)">
|
||||
<details className="group">
|
||||
<summary className="cursor-pointer text-sm font-medium text-[var(--ui-primary)] hover:text-[var(--ui-primary-hover)] mb-4 list-none flex items-center gap-2">
|
||||
<span className="transition-transform group-open:rotate-90">▶</span>
|
||||
Show canvas trim algorithm
|
||||
</summary>
|
||||
<div className="bg-slate-900 dark:bg-stone-950 rounded-lg p-4 overflow-x-auto mb-4">
|
||||
<pre className="text-xs leading-relaxed text-slate-300">
|
||||
{`function getTightFitBounds(text, font, fontSize) {
|
||||
// 1. Create offscreen canvas
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// 2. Render text to canvas
|
||||
ctx.font = \`\${fontSize}px \${font}\`;
|
||||
ctx.fillText(text, 0, fontSize);
|
||||
|
||||
// 3. Scan pixels for non-transparent bounds
|
||||
const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
let top = null, bottom = null, left = null, right = null;
|
||||
|
||||
for (let i = 0; i < pixels.data.length; i += 4) {
|
||||
if (pixels.data[i + 3] !== 0) { // Non-transparent pixel
|
||||
const x = (i / 4) % canvas.width;
|
||||
const y = Math.floor((i / 4) / canvas.width);
|
||||
if (top === null) top = y;
|
||||
bottom = y;
|
||||
if (left === null || x < left) left = x;
|
||||
if (right === null || x > right) right = x;
|
||||
}
|
||||
}
|
||||
return { width: right - left, height: bottom - top };
|
||||
}`}
|
||||
</pre>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 dark:text-stone-400">
|
||||
This algorithm scans every pixel to find the exact boundaries of rendered content, ignoring font metrics, padding, and whitespace.
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<details className="group mt-6">
|
||||
<summary className="cursor-pointer text-sm font-medium text-[var(--ui-primary)] hover:text-[var(--ui-primary-hover)] mb-4 list-none flex items-center gap-2">
|
||||
<span className="transition-transform group-open:rotate-90">▶</span>
|
||||
Show proportion calculation formulas
|
||||
</summary>
|
||||
<div className="bg-slate-900 dark:bg-stone-950 rounded-lg p-4 overflow-x-auto">
|
||||
<pre className="text-xs leading-relaxed text-slate-300">
|
||||
{`// Horizontal variant calculation (u = base unit, e.g., 120px)
|
||||
|
||||
// Icon size: 60% of base unit
|
||||
horizontalIcon = u * 0.60 // 72px
|
||||
|
||||
// Star tight-fit within 72px icon:
|
||||
starWidth = 72 * 0.683 = 49.2px // (star fills 68.3% of viewBox)
|
||||
starHeight = 72 * 0.667 = 48.0px // (star fills 66.7% of viewBox)
|
||||
|
||||
// Text height target: 54% of star height
|
||||
targetTextHeight = 48 * 0.54 = 25.9px
|
||||
|
||||
// Gap target: 0.44 × star height
|
||||
targetGap = 48 * 0.44 = 21.1px
|
||||
horizontalGap = u * 0.176 // 21.1px`}
|
||||
</pre>
|
||||
</div>
|
||||
</details>
|
||||
</Section>
|
||||
|
||||
{/* Measurement Tools */}
|
||||
<Section title="Measurement Tools" subtitle="Scripts for verifying rendered logo proportions">
|
||||
<div className="grid md:grid-cols-3 gap-4">
|
||||
<div className="bg-slate-100 dark:bg-stone-800 rounded-xl p-5">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h4 className="text-sm font-semibold text-slate-900 dark:text-stone-50">render-logo.html</h4>
|
||||
<span className="px-2 py-1 bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300 text-xs font-mono rounded">HTML</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-600 dark:text-stone-400 mb-2">Interactive logo renderer with PNG export at 2x scale.</p>
|
||||
<div className="font-mono text-xs text-slate-400 dark:text-stone-500">tools/render-logo.html</div>
|
||||
</div>
|
||||
<div className="bg-slate-100 dark:bg-stone-800 rounded-xl p-5">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h4 className="text-sm font-semibold text-slate-900 dark:text-stone-50">verify_proportions.py</h4>
|
||||
<span className="px-2 py-1 bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300 text-xs font-mono rounded">Python</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-600 dark:text-stone-400 mb-2">Pixel-analysis tool that verifies proportions against spec (±15% tolerance).</p>
|
||||
<div className="font-mono text-xs text-slate-400 dark:text-stone-500">tools/verify_proportions.py</div>
|
||||
</div>
|
||||
<div className="bg-slate-100 dark:bg-stone-800 rounded-xl p-5">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h4 className="text-sm font-semibold text-slate-900 dark:text-stone-50">measure_proportions.py</h4>
|
||||
<span className="px-2 py-1 bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300 text-xs font-mono rounded">Python</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-600 dark:text-stone-400 mb-2">Detailed measurement report with color-based element detection.</p>
|
||||
<div className="font-mono text-xs text-slate-400 dark:text-stone-500">tools/measure_proportions.py</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
253
whyrating-templates/src/components/tabs/QATab.tsx
Normal file
253
whyrating-templates/src/components/tabs/QATab.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
'use client';
|
||||
|
||||
import { WhyMyRatingLogo, LogoVariant } from '@/components/WhyMyRatingLogo';
|
||||
|
||||
interface SectionProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function Section({ title, description, children }: SectionProps) {
|
||||
return (
|
||||
<section className="bg-white dark:bg-stone-900 rounded-2xl p-8 shadow-sm mb-6 border border-slate-100 dark:border-stone-700/50">
|
||||
{title && <h2 className="text-xl font-semibold text-slate-900 dark:text-stone-50 mb-1">{title}</h2>}
|
||||
{description && <p className="text-sm text-slate-600 dark:text-stone-400 mb-6">{description}</p>}
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
interface StatusBadgeProps {
|
||||
match: number;
|
||||
}
|
||||
|
||||
function StatusBadge({ match }: StatusBadgeProps) {
|
||||
const isPass = match >= 85 && match <= 115;
|
||||
return (
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-semibold ${isPass ? 'bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300' : 'bg-red-100 dark:bg-red-900/40 text-red-700 dark:text-red-300'}`}>
|
||||
{match}% {isPass ? '\u2713' : '\u2717'}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
interface VerificationMetric {
|
||||
measured: number;
|
||||
expected: number;
|
||||
match: number;
|
||||
}
|
||||
|
||||
interface DimensionData {
|
||||
width: number;
|
||||
height: number;
|
||||
expectedW: number;
|
||||
expectedH: number;
|
||||
matchW: number;
|
||||
matchH: number;
|
||||
}
|
||||
|
||||
interface QATabProps {
|
||||
darkMode?: boolean;
|
||||
}
|
||||
|
||||
export function QATab({ darkMode = false }: QATabProps) {
|
||||
const verificationResults = {
|
||||
horizontal: {
|
||||
starWidth: { measured: 51.5, expected: 49.2, match: 105 },
|
||||
starHeight: { measured: 50.5, expected: 48.0, match: 105 },
|
||||
svgWidth: { measured: 75.4, expected: 72.0, match: 105 },
|
||||
gap: { measured: 20.5, expected: 21.1, match: 97 },
|
||||
textHeight: { measured: 25.0, expected: 25.9, match: 96 },
|
||||
textRatio: { measured: 0.50, expected: 0.54, match: 92 },
|
||||
},
|
||||
dimensions: {
|
||||
icon: { width: 151, height: 151, expectedW: 149, expectedH: 149, matchW: 101, matchH: 101 },
|
||||
primary: { width: 174, height: 185, expectedW: 149, expectedH: 183, matchW: 117, matchH: 101 },
|
||||
full: { width: 175, height: 202, expectedW: 149, expectedH: 200, matchW: 118, matchH: 101 },
|
||||
horizontal: { width: 329, height: 88, expectedW: 335, expectedH: 84, matchW: 98, matchH: 105 },
|
||||
horizontalFull: { width: 329, height: 86, expectedW: 335, expectedH: 84, matchW: 98, matchH: 102 },
|
||||
}
|
||||
};
|
||||
|
||||
const horizontalMetrics: [string, VerificationMetric, string][] = [
|
||||
['Star width', verificationResults.horizontal.starWidth, 'px'],
|
||||
['Star height', verificationResults.horizontal.starHeight, 'px'],
|
||||
['SVG icon width', verificationResults.horizontal.svgWidth, 'px'],
|
||||
['Gap (SVG → text)', verificationResults.horizontal.gap, 'px'],
|
||||
['Text height', verificationResults.horizontal.textHeight, 'px'],
|
||||
['Text/Star ratio', verificationResults.horizontal.textRatio, ''],
|
||||
];
|
||||
|
||||
const dimensionVariants: [string, DimensionData][] = [
|
||||
['icon', verificationResults.dimensions.icon],
|
||||
['primary', verificationResults.dimensions.primary],
|
||||
['full', verificationResults.dimensions.full],
|
||||
['horizontal', verificationResults.dimensions.horizontal],
|
||||
['horizontal-full', verificationResults.dimensions.horizontalFull],
|
||||
];
|
||||
|
||||
const verticalVariants: { variant: LogoVariant; label: string }[] = [
|
||||
{ variant: 'icon', label: 'Icon' },
|
||||
{ variant: 'primary', label: 'Primary' },
|
||||
{ variant: 'full', label: 'Full' },
|
||||
];
|
||||
|
||||
const horizontalVariants: { variant: LogoVariant; label: string }[] = [
|
||||
{ variant: 'horizontal', label: 'Horizontal' },
|
||||
{ variant: 'horizontal-full', label: 'Horizontal Full' },
|
||||
];
|
||||
|
||||
const methodologySteps = [
|
||||
{ step: '1', title: 'Render', desc: 'Export logo variants to PNG at 2x scale using html2canvas' },
|
||||
{ step: '2', title: 'Scan', desc: 'Python PIL scans pixels to find element boundaries by color' },
|
||||
{ step: '3', title: 'Measure', desc: 'Calculate actual dimensions, gaps, and ratios from pixel data' },
|
||||
{ step: '4', title: 'Compare', desc: 'Verify measurements are within +/-15% of design spec' },
|
||||
];
|
||||
|
||||
const toleranceNotes = [
|
||||
{ title: 'Anti-aliasing (+3-5%)', desc: 'Browser rendering adds semi-transparent edge pixels that slightly expand measured boundaries.' },
|
||||
{ title: 'Sub-pixel rounding (+/-1-2px)', desc: 'CSS values like 21.1px get rounded to whole pixels, causing minor variations.' },
|
||||
{ title: 'Font rendering (+/-2-3%)', desc: 'Different browsers/OSes render fonts slightly differently, affecting text measurements.' },
|
||||
{ title: 'Scale factor (+/-1%)', desc: '2x scale screenshots divided back to 1x can introduce minor rounding errors.' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Summary */}
|
||||
<Section title="Verification Summary" description="Automated QA results from pixel-level analysis">
|
||||
<div className="grid grid-cols-3 gap-4 mb-6">
|
||||
<div className="bg-green-50 dark:bg-green-900/20 rounded-xl p-6 text-center border border-green-200 dark:border-green-800">
|
||||
<div className="text-4xl font-bold text-green-600 dark:text-green-400 mb-2">6/6</div>
|
||||
<div className="text-sm text-green-700 dark:text-green-300">Horizontal Checks</div>
|
||||
</div>
|
||||
<div className="bg-green-50 dark:bg-green-900/20 rounded-xl p-6 text-center border border-green-200 dark:border-green-800">
|
||||
<div className="text-4xl font-bold text-green-600 dark:text-green-400 mb-2">5/5</div>
|
||||
<div className="text-sm text-green-700 dark:text-green-300">Variant Dimensions</div>
|
||||
</div>
|
||||
<div className="bg-green-50 dark:bg-green-900/20 rounded-xl p-6 text-center border border-green-200 dark:border-green-800">
|
||||
<div className="text-4xl font-bold text-green-600 dark:text-green-400 mb-2">100%</div>
|
||||
<div className="text-sm text-green-700 dark:text-green-300">Pass Rate</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg border-l-4 border-green-500">
|
||||
<p className="text-sm text-green-800 dark:text-green-200">
|
||||
<strong>All proportions verified.</strong> Logo variants render within +/-15% of design specifications. Measurements performed using Python pixel analysis on 2x rendered screenshots.
|
||||
</p>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Horizontal Detail */}
|
||||
<Section title="Horizontal Logo - Detailed Analysis" description="Internal proportions at u=120px">
|
||||
<div className="overflow-hidden rounded-xl border border-slate-100 shadow-sm dark:border-stone-700 mb-6">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-slate-100 dark:bg-stone-800">
|
||||
<tr>
|
||||
<th className="text-left p-4 font-semibold text-slate-900 dark:text-stone-50">Metric</th>
|
||||
<th className="text-right p-4 font-semibold text-slate-900 dark:text-stone-50">Measured</th>
|
||||
<th className="text-right p-4 font-semibold text-slate-900 dark:text-stone-50">Expected</th>
|
||||
<th className="text-right p-4 font-semibold text-slate-900 dark:text-stone-50">Match</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-stone-900">
|
||||
{horizontalMetrics.map(([label, data, unit], i) => (
|
||||
<tr key={i} className="border-t border-slate-100 dark:border-stone-700">
|
||||
<td className="p-4 text-slate-600 dark:text-stone-400">{label}</td>
|
||||
<td className="p-4 text-right font-mono text-slate-900 dark:text-stone-50">{data.measured}{unit}</td>
|
||||
<td className="p-4 text-right font-mono text-slate-500 dark:text-stone-500">{data.expected}{unit}</td>
|
||||
<td className="p-4 text-right"><StatusBadge match={data.match} /></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="flex gap-6 items-center justify-center py-6 bg-slate-100 dark:bg-stone-800 rounded-xl">
|
||||
<WhyMyRatingLogo size={120} variant="horizontal-v2" colorScheme={darkMode ? 'dark' : 'light'} />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* All Variants */}
|
||||
<Section title="All Variants - Dimension Check" description="Container dimensions at u=120px">
|
||||
<div className="overflow-hidden rounded-xl border border-slate-100 shadow-sm dark:border-stone-700 mb-6">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-slate-100 dark:bg-stone-800">
|
||||
<tr>
|
||||
<th className="text-left p-4 font-semibold text-slate-900 dark:text-stone-50">Variant</th>
|
||||
<th className="text-right p-4 font-semibold text-slate-900 dark:text-stone-50">Width</th>
|
||||
<th className="text-right p-4 font-semibold text-slate-900 dark:text-stone-50">Height</th>
|
||||
<th className="text-right p-4 font-semibold text-slate-900 dark:text-stone-50">W Match</th>
|
||||
<th className="text-right p-4 font-semibold text-slate-900 dark:text-stone-50">H Match</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-stone-900">
|
||||
{dimensionVariants.map(([name, data], i) => (
|
||||
<tr key={i} className="border-t border-slate-100 dark:border-stone-700">
|
||||
<td className="p-4 font-medium text-slate-900 dark:text-stone-50">{name}</td>
|
||||
<td className="p-4 text-right font-mono text-slate-600 dark:text-stone-400">{data.width}px <span className="text-slate-400 dark:text-stone-600">/ {data.expectedW}</span></td>
|
||||
<td className="p-4 text-right font-mono text-slate-600 dark:text-stone-400">{data.height}px <span className="text-slate-400 dark:text-stone-600">/ {data.expectedH}</span></td>
|
||||
<td className="p-4 text-right"><StatusBadge match={data.matchW} /></td>
|
||||
<td className="p-4 text-right"><StatusBadge match={data.matchH} /></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Visual Gallery */}
|
||||
<Section title="Visual Verification Gallery" description="All variants rendered at u=120px">
|
||||
<div className="grid grid-cols-3 gap-6 mb-6">
|
||||
{verticalVariants.map((item, i) => (
|
||||
<div key={i} className="bg-slate-100 dark:bg-stone-800 rounded-xl p-6 flex flex-col items-center">
|
||||
<div className="bg-white dark:bg-stone-900 rounded-lg p-4 border border-slate-100 dark:border-stone-700 mb-3 min-h-48 flex items-center justify-center">
|
||||
<WhyMyRatingLogo size={120} variant={item.variant} colorScheme={darkMode ? 'dark' : 'light'} />
|
||||
</div>
|
||||
<span className="text-xs font-semibold text-slate-500 dark:text-stone-500 uppercase tracking-wider">{item.label}</span>
|
||||
<span className="text-xs text-green-600 dark:text-green-400 mt-1">{'\u2713'} Verified</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
{horizontalVariants.map((item, i) => (
|
||||
<div key={i} className="bg-slate-100 dark:bg-stone-800 rounded-xl p-6 flex flex-col items-center">
|
||||
<div className="bg-white dark:bg-stone-900 rounded-lg p-4 border border-slate-100 dark:border-stone-700 mb-3 w-full flex items-center justify-center min-h-24">
|
||||
<WhyMyRatingLogo size={120} variant={item.variant} colorScheme={darkMode ? 'dark' : 'light'} />
|
||||
</div>
|
||||
<span className="text-xs font-semibold text-slate-500 dark:text-stone-500 uppercase tracking-wider">{item.label}</span>
|
||||
<span className="text-xs text-green-600 dark:text-green-400 mt-1">{'\u2713'} Verified</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Methodology */}
|
||||
<Section title="QA Methodology" description="How verification was performed">
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{methodologySteps.map((item, i) => (
|
||||
<div key={i} className="bg-slate-100 dark:bg-stone-800 rounded-xl p-5">
|
||||
<div className="text-2xl font-bold text-[var(--ui-primary)] mb-2">{item.step}</div>
|
||||
<div className="text-sm font-semibold text-slate-900 dark:text-stone-50 mb-1">{item.title}</div>
|
||||
<div className="text-xs text-slate-500 dark:text-stone-500">{item.desc}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Known Tolerances */}
|
||||
<Section title="Tolerance Notes" description="Why some measurements differ slightly from spec">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{toleranceNotes.map((item, i) => (
|
||||
<div key={i} className="bg-blue-50 dark:bg-blue-900/20 rounded-xl p-5 border border-blue-200 dark:border-blue-800">
|
||||
<h4 className="text-sm font-semibold text-blue-700 dark:text-blue-300 mb-2">{item.title}</h4>
|
||||
<p className="text-sm text-blue-600 dark:text-blue-400">{item.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6 p-4 bg-slate-100 dark:bg-stone-800 rounded-lg">
|
||||
<p className="text-sm text-slate-600 dark:text-stone-400">
|
||||
<strong className="text-slate-900 dark:text-stone-50">Acceptance threshold:</strong> +/-15% tolerance accounts for these rendering variations while ensuring proportions remain visually correct.
|
||||
</p>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
192
whyrating-templates/src/components/tabs/RationaleTab.tsx
Normal file
192
whyrating-templates/src/components/tabs/RationaleTab.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
'use client';
|
||||
|
||||
import { Section } from '../ui/Section';
|
||||
|
||||
export function RationaleTab() {
|
||||
return (
|
||||
<div>
|
||||
{/* Strategic Challenge */}
|
||||
<Section title="The Strategic Challenge" description="Why our visual identity looks the way it does">
|
||||
<p className="text-base text-slate-600 dark:text-stone-400 leading-relaxed mb-6">
|
||||
whyrating.com exists in a unique position: we analyze <strong className="text-slate-900 dark:text-stone-50">Google</strong> reviews, but we're <strong className="text-slate-900 dark:text-stone-50">not</strong> a Google product. Our visual identity needed to:
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{[
|
||||
{ num: '1', text: 'Create instant recognition of what we do (Google reviews)' },
|
||||
{ num: '2', text: 'Avoid looking like a Google product clone' },
|
||||
{ num: '3', text: 'Feel approachable to SMB owners (not enterprise/tech intimidating)' },
|
||||
{ num: '4', text: 'Convey analytical depth and trust' },
|
||||
].map((item, i) => (
|
||||
<div key={i} className="flex items-center justify-center gap-4 p-5 bg-slate-100 dark:bg-stone-800 rounded-xl text-center">
|
||||
<span className="text-2xl font-bold text-ui-primary">{item.num}</span>
|
||||
<span className="text-sm text-slate-700 dark:text-stone-300">{item.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Why Two Color Systems */}
|
||||
<Section title="Why Two Color Systems" description="Logo colors ≠ UI colors — and that's intentional">
|
||||
<div className="grid grid-cols-2 gap-6 mb-6">
|
||||
<div className="p-6 bg-brand-star/20 rounded-xl border-l-4 border-brand-star">
|
||||
<div className="text-sm font-semibold text-slate-900 dark:text-stone-50 mb-2">Logo Colors</div>
|
||||
<div className="text-sm text-slate-600 dark:text-stone-400 mb-3">Google Yellow, Dark, Custom Greens</div>
|
||||
<div className="text-xs text-slate-500 dark:text-stone-500">Distinctive brand mark with one Google anchor</div>
|
||||
</div>
|
||||
<div className="p-6 bg-ui-primary/20 rounded-xl border-l-4 border-ui-primary">
|
||||
<div className="text-sm font-semibold text-slate-900 dark:text-stone-50 mb-2">UI Colors</div>
|
||||
<div className="text-sm text-slate-600 dark:text-stone-400 mb-3">Google Blue, Amber, Google palette</div>
|
||||
<div className="text-xs text-slate-500 dark:text-stone-500">Familiar interface that feels like tools users already know</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-5 bg-slate-100 dark:bg-stone-800 rounded-xl">
|
||||
<p className="text-sm text-slate-600 dark:text-stone-400">
|
||||
<strong className="text-slate-900 dark:text-stone-50">The reasoning:</strong> Users spend seconds looking at a logo but hours in the interface. The logo needs to be memorable and distinctive. The UI needs to feel familiar and reduce cognitive load.
|
||||
</p>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* The Google Alignment Trade-off */}
|
||||
<Section title="The Google Alignment Trade-off" description="Three approaches we considered">
|
||||
<div className="overflow-hidden rounded-xl border border-slate-100 shadow-sm dark:border-stone-700">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-slate-100 dark:bg-stone-800">
|
||||
<tr>
|
||||
<th className="text-left p-4 font-semibold text-slate-900 dark:text-stone-50">Approach</th>
|
||||
<th className="text-left p-4 font-semibold text-slate-900 dark:text-stone-50">Pros</th>
|
||||
<th className="text-left p-4 font-semibold text-slate-900 dark:text-stone-50">Cons</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-stone-900">
|
||||
<tr className="border-t border-slate-100 dark:border-stone-700">
|
||||
<td className="p-4 text-slate-600 dark:text-stone-400">Full Google palette</td>
|
||||
<td className="p-4 text-slate-600 dark:text-stone-400">Maximum familiarity, feels integrated</td>
|
||||
<td className="p-4 text-slate-600 dark:text-stone-400">Looks like a Google clone, no differentiation</td>
|
||||
</tr>
|
||||
<tr className="border-t border-slate-100 dark:border-stone-700">
|
||||
<td className="p-4 text-slate-600 dark:text-stone-400">Completely unique</td>
|
||||
<td className="p-4 text-slate-600 dark:text-stone-400">Maximum distinctiveness</td>
|
||||
<td className="p-4 text-slate-600 dark:text-stone-400">Disconnected from what we do, learning curve</td>
|
||||
</tr>
|
||||
<tr className="border-t border-slate-100 dark:border-stone-700 bg-green-50 dark:bg-green-900/20">
|
||||
<td className="p-4 font-semibold text-green-700 dark:text-green-300">Hybrid (chosen) ✓</td>
|
||||
<td className="p-4 text-green-700 dark:text-green-300">Recognition + differentiation</td>
|
||||
<td className="p-4 text-green-700 dark:text-green-300">Requires careful balance</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Creative License #1 */}
|
||||
<Section title="Creative License #1: Dark Magnifier" description="Should the magnifying glass be Google Blue or Dark?">
|
||||
<div className="grid grid-cols-2 gap-6 mb-6">
|
||||
<div className="p-6 bg-red-50 dark:bg-red-900/20 rounded-xl border border-red-200 dark:border-red-800">
|
||||
<div className="text-sm font-semibold text-red-700 dark:text-red-300 mb-4">Google Blue Magnifier ✕</div>
|
||||
<ul className="text-sm text-red-600 dark:text-red-400 space-y-2">
|
||||
<li>Looks like a Google product</li>
|
||||
<li>“We're part of Google”</li>
|
||||
<li>Blends in</li>
|
||||
<li>Familiar</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="p-6 bg-green-50 dark:bg-green-900/20 rounded-xl border border-green-200 dark:border-green-800">
|
||||
<div className="text-sm font-semibold text-green-700 dark:text-green-300 mb-4">Dark Magnifier ✓</div>
|
||||
<ul className="text-sm text-green-600 dark:text-green-400 space-y-2">
|
||||
<li>Looks like a tool that analyzes Google</li>
|
||||
<li>“We see what Google doesn't show you”</li>
|
||||
<li>Stands out</li>
|
||||
<li>Premium, analytical</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-5 bg-slate-800 rounded-xl">
|
||||
<p className="text-sm text-white">
|
||||
The dark magnifier communicates: <strong>“We look deeper.”</strong> It's the visual representation of our value prop — revealing insights hidden beneath the surface.
|
||||
</p>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Creative License #2 */}
|
||||
<Section title="Creative License #2: Custom Green Gradient" description="Should growth bars use Google Green or a custom gradient?">
|
||||
<div className="grid grid-cols-2 gap-6 mb-6">
|
||||
<div className="p-6 bg-red-50 dark:bg-red-900/20 rounded-xl border border-red-200 dark:border-red-800">
|
||||
<div className="text-sm font-semibold text-red-700 dark:text-red-300 mb-4">Single Google Green ✕</div>
|
||||
<ul className="text-sm text-red-600 dark:text-red-400 space-y-2">
|
||||
<li>Flat, static</li>
|
||||
<li>“Success” (binary)</li>
|
||||
<li>Google's color</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="p-6 bg-green-50 dark:bg-green-900/20 rounded-xl border border-green-200 dark:border-green-800">
|
||||
<div className="text-sm font-semibold text-green-700 dark:text-green-300 mb-4">Custom Gradient ✓</div>
|
||||
<ul className="text-sm text-green-600 dark:text-green-400 space-y-2">
|
||||
<li>Progressive, tells a story</li>
|
||||
<li>“Growth over time” (journey)</li>
|
||||
<li>Our color</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 mb-4">
|
||||
<div className="flex-1 h-12 rounded-lg" style={{ background: '#86EFAC' }} />
|
||||
<div className="flex-1 h-12 rounded-lg" style={{ background: '#22C55E' }} />
|
||||
<div className="flex-1 h-12 rounded-lg" style={{ background: '#15803D' }} />
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 dark:text-stone-400">
|
||||
The ascending bars with deepening green visually communicate: <strong className="text-slate-900 dark:text-stone-50">“Things get better.”</strong> This is literally what SMB owners want — improvement.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
{/* Creative License #3 */}
|
||||
<Section title="Creative License #3: Nunito Wordmark" description="Should we use Inter (clean, Google-like) or Nunito (rounded, friendly)?">
|
||||
<div className="grid grid-cols-2 gap-6 mb-6">
|
||||
<div className="p-6 bg-slate-100 dark:bg-stone-800 rounded-xl">
|
||||
<div className="text-sm font-semibold text-slate-500 dark:text-stone-500 mb-4">Inter</div>
|
||||
<div className="font-body text-2xl text-slate-900 dark:text-stone-50 mb-4">whyrating.com</div>
|
||||
<ul className="text-sm text-slate-500 dark:text-stone-500 space-y-1">
|
||||
<li>Clean, professional</li>
|
||||
<li>Tech/enterprise feel</li>
|
||||
<li>Google-like</li>
|
||||
<li>“Platform”</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="p-6 bg-green-50 dark:bg-green-900/20 rounded-xl border border-green-200 dark:border-green-800">
|
||||
<div className="text-sm font-semibold text-green-700 dark:text-green-300 mb-4">Nunito ✓</div>
|
||||
<div className="font-wordmark font-bold text-2xl text-slate-900 dark:text-stone-50 mb-4">whyrating<span className="text-brand-accent">.com</span></div>
|
||||
<ul className="text-sm text-green-600 dark:text-green-400 space-y-1">
|
||||
<li>Warm, approachable</li>
|
||||
<li>Small business friendly</li>
|
||||
<li>Human</li>
|
||||
<li>“Helper”</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 dark:text-stone-400">
|
||||
Our brand voice is “helpful expert, not salesy.” Nunito's rounded letterforms reflect that warmth. But we still use Inter for everything else to maintain professionalism.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
{/* Future Design Decisions */}
|
||||
<Section title="Future Design Decisions" description="Four questions to ask before making new design choices">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{[
|
||||
'Does this feel approachable to a small business owner?',
|
||||
'Does this connect to Google reviews without looking like a Google clone?',
|
||||
'Does this communicate insight, depth, or improvement?',
|
||||
'Does this match our "helpful expert" voice?',
|
||||
].map((q, i) => (
|
||||
<div key={i} className="flex items-center justify-center gap-4 p-5 bg-slate-100 dark:bg-stone-800 rounded-xl text-center">
|
||||
<span className="text-2xl font-bold text-ui-primary">{i + 1}</span>
|
||||
<span className="text-sm text-slate-700 dark:text-stone-300">{q}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6 p-5 bg-ui-primary/10 rounded-xl border-l-4 border-ui-primary">
|
||||
<p className="text-sm text-slate-700 dark:text-stone-300">
|
||||
<strong className="text-slate-900 dark:text-stone-50">If yes to all four, proceed.</strong> If not, reconsider.
|
||||
</p>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
207
whyrating-templates/src/components/tabs/SettingsTab.tsx
Normal file
207
whyrating-templates/src/components/tabs/SettingsTab.tsx
Normal file
@@ -0,0 +1,207 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useBrand } from '@/lib/BrandContext';
|
||||
import { Section } from '../ui/Section';
|
||||
|
||||
export function SettingsTab() {
|
||||
const { config, updateConfig, resetConfig, isLoaded } = useBrand();
|
||||
const [localConfig, setLocalConfig] = useState(config);
|
||||
const [saved, setSaved] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalConfig(config);
|
||||
}, [config]);
|
||||
|
||||
const handleChange = (field: string, value: string) => {
|
||||
setLocalConfig(prev => ({ ...prev, [field]: value }));
|
||||
setSaved(false);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
updateConfig(localConfig);
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 2000);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
if (confirm('Reset all brand settings to defaults?')) {
|
||||
resetConfig();
|
||||
setSaved(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isLoaded) {
|
||||
return <div className="p-8 text-center text-slate-500">Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Section title="Brand Configuration" description="Central settings that apply across all templates and components">
|
||||
<div className="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-xl p-4 mb-6">
|
||||
<p className="text-sm text-amber-800 dark:text-amber-200">
|
||||
Changes here will update all templates, downloads, and preview components. Settings are saved in your browser.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
{/* Domain Settings */}
|
||||
<div className="col-span-2">
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-stone-50 mb-4">Domain & Identity</h3>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-stone-300 mb-2">
|
||||
Domain Name (without TLD)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={localConfig.domain}
|
||||
onChange={(e) => handleChange('domain', e.target.value)}
|
||||
className="w-full px-4 py-3 border border-slate-300 dark:border-stone-600 rounded-lg bg-white dark:bg-stone-800 text-slate-900 dark:text-stone-50 focus:ring-2 focus:ring-[var(--ui-primary)] focus:border-transparent text-lg"
|
||||
placeholder="whyrating"
|
||||
/>
|
||||
<p className="text-xs text-slate-500 dark:text-stone-500 mt-1">e.g., whyrating, whymyrating, mybrand</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-stone-300 mb-2">
|
||||
TLD
|
||||
</label>
|
||||
<select
|
||||
value={localConfig.domainTLD}
|
||||
onChange={(e) => handleChange('domainTLD', e.target.value)}
|
||||
className="w-full px-4 py-3 border border-slate-300 dark:border-stone-600 rounded-lg bg-white dark:bg-stone-800 text-slate-900 dark:text-stone-50 focus:ring-2 focus:ring-[var(--ui-primary)] focus:border-transparent text-lg"
|
||||
>
|
||||
<option value=".com">.com</option>
|
||||
<option value=".io">.io</option>
|
||||
<option value=".co">.co</option>
|
||||
<option value=".app">.app</option>
|
||||
<option value=".ai">.ai</option>
|
||||
<option value=".dev">.dev</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-stone-300 mb-2">
|
||||
Tagline
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={localConfig.tagline}
|
||||
onChange={(e) => handleChange('tagline', e.target.value)}
|
||||
className="w-full px-4 py-3 border border-slate-300 dark:border-stone-600 rounded-lg bg-white dark:bg-stone-800 text-slate-900 dark:text-stone-50 focus:ring-2 focus:ring-[var(--ui-primary)] focus:border-transparent"
|
||||
placeholder="The story behind your stars"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Messaging */}
|
||||
<div className="col-span-2 mt-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-stone-50 mb-4">Default Messaging</h3>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-stone-300 mb-2">
|
||||
Value Proposition
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={localConfig.valueProposition}
|
||||
onChange={(e) => handleChange('valueProposition', e.target.value)}
|
||||
className="w-full px-4 py-3 border border-slate-300 dark:border-stone-600 rounded-lg bg-white dark:bg-stone-800 text-slate-900 dark:text-stone-50 focus:ring-2 focus:ring-[var(--ui-primary)] focus:border-transparent"
|
||||
placeholder="Get AI-powered insights in 45 seconds"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-stone-300 mb-2">
|
||||
Default Hook
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={localConfig.hookDefault}
|
||||
onChange={(e) => handleChange('hookDefault', e.target.value)}
|
||||
className="w-full px-4 py-3 border border-slate-300 dark:border-stone-600 rounded-lg bg-white dark:bg-stone-800 text-slate-900 dark:text-stone-50 focus:ring-2 focus:ring-[var(--ui-primary)] focus:border-transparent"
|
||||
placeholder="Still reading reviews one by one?"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Social */}
|
||||
<div className="col-span-2 mt-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-stone-50 mb-4">Social & Contact</h3>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-stone-300 mb-2">
|
||||
Twitter Handle
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={localConfig.twitterHandle}
|
||||
onChange={(e) => handleChange('twitterHandle', e.target.value)}
|
||||
className="w-full px-4 py-3 border border-slate-300 dark:border-stone-600 rounded-lg bg-white dark:bg-stone-800 text-slate-900 dark:text-stone-50 focus:ring-2 focus:ring-[var(--ui-primary)] focus:border-transparent"
|
||||
placeholder="@whyrating"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-stone-300 mb-2">
|
||||
LinkedIn URL
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={localConfig.linkedInUrl}
|
||||
onChange={(e) => handleChange('linkedInUrl', e.target.value)}
|
||||
className="w-full px-4 py-3 border border-slate-300 dark:border-stone-600 rounded-lg bg-white dark:bg-stone-800 text-slate-900 dark:text-stone-50 focus:ring-2 focus:ring-[var(--ui-primary)] focus:border-transparent"
|
||||
placeholder="https://linkedin.com/company/whyrating"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview */}
|
||||
<div className="mt-8 p-6 bg-slate-100 dark:bg-stone-800 rounded-xl">
|
||||
<h4 className="text-sm font-semibold text-slate-500 dark:text-stone-500 uppercase tracking-wider mb-4">Live Preview</h4>
|
||||
<div className="space-y-2 text-slate-900 dark:text-stone-50">
|
||||
<p className="text-2xl font-bold font-wordmark">{localConfig.domain}<span className="text-[var(--brand-accent)]">{localConfig.domainTLD}</span></p>
|
||||
<p className="text-slate-500 dark:text-stone-400 italic">{localConfig.tagline}</p>
|
||||
<p className="text-sm text-slate-600 dark:text-stone-400 mt-4">{localConfig.supportEmail}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="mt-8 flex gap-4">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="px-6 py-3 bg-[var(--ui-primary)] text-white rounded-lg font-semibold hover:bg-[var(--ui-secondary)] transition-colors"
|
||||
>
|
||||
{saved ? '✓ Saved!' : 'Save Changes'}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="px-6 py-3 bg-slate-200 dark:bg-stone-700 text-slate-700 dark:text-stone-300 rounded-lg font-semibold hover:bg-slate-300 dark:hover:bg-stone-600 transition-colors"
|
||||
>
|
||||
Reset to Defaults
|
||||
</button>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Auto-Generated Values */}
|
||||
<Section title="Auto-Generated Values" description="These values are automatically derived from your settings above">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-slate-50 dark:bg-stone-800 rounded-lg">
|
||||
<div className="text-xs text-slate-500 dark:text-stone-500 uppercase tracking-wider mb-1">Full Domain</div>
|
||||
<div className="text-slate-900 dark:text-stone-50 font-mono">{localConfig.domain}{localConfig.domainTLD}</div>
|
||||
</div>
|
||||
<div className="p-4 bg-slate-50 dark:bg-stone-800 rounded-lg">
|
||||
<div className="text-xs text-slate-500 dark:text-stone-500 uppercase tracking-wider mb-1">Support Email</div>
|
||||
<div className="text-slate-900 dark:text-stone-50 font-mono">support@{localConfig.domain}{localConfig.domainTLD}</div>
|
||||
</div>
|
||||
<div className="col-span-2 p-4 bg-slate-50 dark:bg-stone-800 rounded-lg">
|
||||
<div className="text-xs text-slate-500 dark:text-stone-500 uppercase tracking-wider mb-1">Default CTA</div>
|
||||
<div className="text-slate-900 dark:text-stone-50">Try it free at {localConfig.domain}{localConfig.domainTLD}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
101
whyrating-templates/src/components/tabs/TypographyTab.tsx
Normal file
101
whyrating-templates/src/components/tabs/TypographyTab.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
'use client';
|
||||
|
||||
import { typography } from '@/lib/constants';
|
||||
import { Section } from '@/components/ui/Section';
|
||||
import { CopyButton } from '@/components/ui/CopyButton';
|
||||
|
||||
export function TypographyTab() {
|
||||
const fontLoadingCode = `<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Nunito:wght@700&display=swap" rel="stylesheet">`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Font Stack */}
|
||||
<Section title="Font Stack">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="bg-slate-100 dark:bg-stone-800 rounded-xl p-8 text-center">
|
||||
<span className="font-wordmark font-bold text-4xl text-slate-900 dark:text-stone-50">
|
||||
whyrating<span className="text-[var(--ui-accent)]">.com</span>
|
||||
</span>
|
||||
<div className="mt-4">
|
||||
<span className="text-sm font-semibold text-slate-900 dark:text-stone-50">Nunito Bold</span>
|
||||
<span className="text-sm text-slate-500 dark:text-stone-500"> — Wordmark only</span>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<CopyButton text="font-family: 'Nunito', sans-serif; font-weight: 700;" label="Copy CSS" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-100 dark:bg-stone-800 rounded-xl p-8 text-center">
|
||||
<span className="font-body font-semibold text-4xl text-slate-900 dark:text-stone-50">Inter</span>
|
||||
<div className="mt-4">
|
||||
<span className="text-sm font-semibold text-slate-900 dark:text-stone-50">Inter 400-700</span>
|
||||
<span className="text-sm text-slate-500 dark:text-stone-500"> — Everything else</span>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<CopyButton text="font-family: 'Inter', sans-serif;" label="Copy CSS" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Type Scale */}
|
||||
<Section title="Type Scale" description="1.25 ratio: 36px → 30px → 24px → 20px → 16px → 14px → 12px">
|
||||
<div className="bg-slate-100 dark:bg-stone-800 rounded-xl p-6">
|
||||
{Object.entries(typography).map(([key, val], i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`flex items-baseline justify-between py-4 ${i < Object.keys(typography).length - 1 ? 'border-b border-slate-100 dark:border-stone-700' : ''}`}
|
||||
>
|
||||
<span
|
||||
style={{ fontFamily: `'${val.font}', sans-serif`, fontWeight: val.weight, fontSize: val.size }}
|
||||
className="text-slate-900 dark:text-stone-50"
|
||||
>
|
||||
{key === 'wordmark' ? (
|
||||
<>whyrating<span className="text-[var(--ui-accent)]">.com</span></>
|
||||
) : (
|
||||
`${key.toUpperCase()} — ${val.usage}`
|
||||
)}
|
||||
</span>
|
||||
<span className="text-xs text-slate-500 dark:text-stone-500 font-mono">{val.size} / {val.weight}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Line Heights */}
|
||||
<Section title="Line Heights" description="Consistent vertical rhythm">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="bg-slate-100 dark:bg-stone-800 rounded-xl p-6">
|
||||
<div className="text-sm font-semibold text-slate-900 dark:text-stone-50 mb-4">Headings</div>
|
||||
<div className="text-4xl font-semibold text-slate-900 dark:text-stone-50 leading-tight mb-2">1.2 – 1.3</div>
|
||||
<p className="text-sm text-slate-500 dark:text-stone-500">Tighter line height keeps headings compact and impactful</p>
|
||||
<div className="mt-4">
|
||||
<CopyButton text="line-height: 1.25;" label="Copy CSS" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-100 dark:bg-stone-800 rounded-xl p-6">
|
||||
<div className="text-sm font-semibold text-slate-900 dark:text-stone-50 mb-4">Body Text</div>
|
||||
<div className="text-4xl font-semibold text-slate-900 dark:text-stone-50 leading-tight mb-2">1.5 – 1.6</div>
|
||||
<p className="text-sm text-slate-500 dark:text-stone-500">More breathing room improves readability for paragraphs</p>
|
||||
<div className="mt-4">
|
||||
<CopyButton text="line-height: 1.5;" label="Copy CSS" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Font Loading */}
|
||||
<Section title="Font Loading" description="Add to your HTML head">
|
||||
<div className="code-block">
|
||||
{`<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Nunito:wght@700&display=swap" rel="stylesheet">`}
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<CopyButton text={fontLoadingCode} label="Copy HTML" />
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
120
whyrating-templates/src/components/tabs/VoiceTab.tsx
Normal file
120
whyrating-templates/src/components/tabs/VoiceTab.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
interface SectionProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function Section({ title, description, children }: SectionProps) {
|
||||
return (
|
||||
<section className="bg-white dark:bg-stone-900 rounded-2xl p-8 shadow-sm mb-6 border border-slate-100 dark:border-stone-700/50">
|
||||
{title && <h2 className="text-xl font-semibold text-slate-900 dark:text-stone-50 mb-1">{title}</h2>}
|
||||
{description && <p className="text-sm text-slate-600 dark:text-stone-400 mb-6">{description}</p>}
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
interface VoicePrinciple {
|
||||
title: string;
|
||||
desc: string;
|
||||
}
|
||||
|
||||
interface DoExample {
|
||||
context: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface DontExample {
|
||||
context: string;
|
||||
text: string;
|
||||
why: string;
|
||||
}
|
||||
|
||||
export function VoiceTab() {
|
||||
const voicePrinciples: VoicePrinciple[] = [
|
||||
{ title: 'Helpful Expert', desc: 'Knowledgeable but never condescending' },
|
||||
{ title: 'Plain Language', desc: 'No jargon, no buzzwords' },
|
||||
{ title: 'Respectful of Time', desc: 'Get to the point' },
|
||||
{ title: 'Encouraging', desc: 'Problems are fixable' },
|
||||
];
|
||||
|
||||
const doExamples: DoExample[] = [
|
||||
{ context: 'Headlines', text: '"See exactly what\'s frustrating your customers"' },
|
||||
{ context: 'Value prop', text: '"Fix the right problems first"' },
|
||||
{ context: 'Pricing', text: '"Enterprise-grade insights. Small business price."' },
|
||||
{ context: 'CTA', text: '"Analyze my reviews"' },
|
||||
{ context: 'Support', text: '"Questions? Just reply to this email."' },
|
||||
];
|
||||
|
||||
const dontExamples: DontExample[] = [
|
||||
{ context: 'Headlines', text: '"Leverage AI-powered sentiment analysis"', why: 'Jargon' },
|
||||
{ context: 'Value prop', text: '"Unlock synergies in your feedback pipeline"', why: 'Buzzwords' },
|
||||
{ context: 'Pricing', text: '"URGENT: 50% OFF!"', why: 'Salesy' },
|
||||
{ context: 'CTA', text: '"Get started now!"', why: 'Generic, pushy' },
|
||||
{ context: 'Support', text: '"Please submit a ticket"', why: 'Cold, corporate' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Tone */}
|
||||
<Section title="Brand Voice">
|
||||
<div className="grid grid-cols-4 gap-4 mb-6">
|
||||
{voicePrinciples.map((item, i) => (
|
||||
<div key={i} className="bg-slate-100 dark:bg-stone-800 rounded-xl p-5 text-center">
|
||||
<div className="text-sm font-semibold text-slate-900 dark:text-stone-50 mb-1">{item.title}</div>
|
||||
<div className="text-sm text-slate-500 dark:text-stone-500">{item.desc}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Do / Don't */}
|
||||
<Section title="Writing Examples">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-green-600 mb-4 flex items-center gap-2">
|
||||
<span>✓</span> Do
|
||||
</h4>
|
||||
{doExamples.map((item, i) => (
|
||||
<div key={i} className="bg-green-50 dark:bg-green-900/20 rounded-lg p-4 mb-3">
|
||||
<div className="text-xs text-green-600 dark:text-green-400 mb-1">{item.context}</div>
|
||||
<div className="text-sm text-green-700 dark:text-green-300 italic">{item.text}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-red-600 mb-4 flex items-center gap-2">
|
||||
<span>✕</span> Don't
|
||||
</h4>
|
||||
{dontExamples.map((item, i) => (
|
||||
<div key={i} className="bg-red-50 dark:bg-red-900/20 rounded-lg p-4 mb-3">
|
||||
<div className="text-xs text-red-600 dark:text-red-400 mb-1">{item.context} — <em>{item.why}</em></div>
|
||||
<div className="text-sm text-red-700 dark:text-red-300 italic line-through">{item.text}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Email Subject Lines */}
|
||||
<Section title="Email Subject Lines">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="bg-green-50 dark:bg-green-900/20 rounded-xl p-5">
|
||||
<div className="text-xs text-green-600 dark:text-green-400 mb-3 font-semibold">✓ Good</div>
|
||||
<div className="text-sm text-green-700 dark:text-green-300 mb-2">"Your June review summary is ready"</div>
|
||||
<div className="text-sm text-green-700 dark:text-green-300">"3 things customers mentioned this week"</div>
|
||||
</div>
|
||||
<div className="bg-red-50 dark:bg-red-900/20 rounded-xl p-5">
|
||||
<div className="text-xs text-red-600 dark:text-red-400 mb-3 font-semibold">✕ Bad</div>
|
||||
<div className="text-sm text-red-700 dark:text-red-300 mb-2 line-through">"URGENT: Don't miss your analytics!"</div>
|
||||
<div className="text-sm text-red-700 dark:text-red-300 line-through">"You won't BELIEVE what customers said"</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
whyrating-templates/src/components/tabs/index.ts
Normal file
11
whyrating-templates/src/components/tabs/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export { OverviewTab } from './OverviewTab';
|
||||
export { LogoTab } from './LogoTab';
|
||||
export { ColorsTab } from './ColorsTab';
|
||||
export { TypographyTab } from './TypographyTab';
|
||||
export { VoiceTab } from './VoiceTab';
|
||||
export { DownloadsTab } from './DownloadsTab';
|
||||
export { RationaleTab } from './RationaleTab';
|
||||
export { ProportionsTab } from './ProportionsTab';
|
||||
export { QATab } from './QATab';
|
||||
export { GuidelinesTab } from './GuidelinesTab';
|
||||
export { AIContextTab, AI_CONTEXT_MARKDOWN } from './AIContextTab';
|
||||
@@ -0,0 +1,124 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { WhyMyRatingLogo, PreviewScaler } from '@/components';
|
||||
|
||||
export function CTATemplateLandscape() {
|
||||
const [hook, setHook] = useState('Still reading reviews one by one?');
|
||||
const [valueProp, setValueProp] = useState('Get AI-powered insights in 45 seconds');
|
||||
const [ctaText, setCtaText] = useState('Try it free at whyrating.com');
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
|
||||
const downloadPng = async () => {
|
||||
setIsDownloading(true);
|
||||
try {
|
||||
const response = await fetch('/api/screenshot', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
template: 'cta-landscape',
|
||||
params: { hook, valueProp, ctaText }
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Screenshot failed');
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.download = `whyrating-cta-landscape-${Date.now()}.png`;
|
||||
link.href = url;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Download failed:', error);
|
||||
alert('Download failed. Please try again.');
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-stone-900 rounded-xl border border-slate-100 shadow-sm dark:border-stone-700 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-stone-50">CTA Post (Landscape)</h3>
|
||||
<p className="text-sm text-slate-500 dark:text-stone-500">1200 × 675px • LinkedIn, Twitter</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={downloadPng}
|
||||
disabled={isDownloading}
|
||||
className="px-4 py-2 bg-[var(--ui-primary)] text-white rounded-lg text-sm font-medium hover:bg-[var(--ui-primary-hover)] transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isDownloading ? 'Generating...' : 'Download PNG'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Preview */}
|
||||
<PreviewScaler width={600} height={338}>
|
||||
<div
|
||||
className="rounded-xl p-10 flex flex-col justify-between relative overflow-hidden bg-gradient-to-br from-slate-800 to-slate-900"
|
||||
style={{ width: '600px', height: '338px' }}
|
||||
>
|
||||
{/* Decorative stars */}
|
||||
<div className="absolute top-6 right-8 text-[#FBBC05] text-3xl opacity-20">★</div>
|
||||
<div className="absolute bottom-16 right-12 text-[#FBBC05] text-xl opacity-15">★</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 flex items-center">
|
||||
<div className="flex flex-col gap-4 max-w-[85%]">
|
||||
<div className="text-slate-300 text-xl font-medium leading-tight">{hook}</div>
|
||||
<div className="text-white text-3xl font-bold leading-tight">{valueProp}</div>
|
||||
<div className="mt-2">
|
||||
<span className="inline-block bg-[#F59E0B] text-slate-900 px-5 py-2.5 rounded-lg text-lg font-semibold">
|
||||
{ctaText}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-end justify-end">
|
||||
<WhyMyRatingLogo size={70} variant="horizontal-v2" colorScheme="dark" />
|
||||
</div>
|
||||
</div>
|
||||
</PreviewScaler>
|
||||
|
||||
{/* Editor */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-stone-300 mb-1">Hook</label>
|
||||
<input
|
||||
type="text"
|
||||
value={hook}
|
||||
onChange={(e) => setHook(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 dark:border-stone-600 rounded-lg bg-white dark:bg-stone-800 text-slate-900 dark:text-stone-50 focus:ring-2 focus:ring-[var(--ui-primary)] focus:border-transparent"
|
||||
placeholder="Still reading reviews one by one?"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-stone-300 mb-1">Value Proposition</label>
|
||||
<textarea
|
||||
value={valueProp}
|
||||
onChange={(e) => setValueProp(e.target.value)}
|
||||
rows={2}
|
||||
className="w-full px-3 py-2 border border-slate-300 dark:border-stone-600 rounded-lg bg-white dark:bg-stone-800 text-slate-900 dark:text-stone-50 focus:ring-2 focus:ring-[var(--ui-primary)] focus:border-transparent resize-none"
|
||||
placeholder="Get AI-powered insights in 45 seconds"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-stone-300 mb-1">CTA Text</label>
|
||||
<input
|
||||
type="text"
|
||||
value={ctaText}
|
||||
onChange={(e) => setCtaText(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 dark:border-stone-600 rounded-lg bg-white dark:bg-stone-800 text-slate-900 dark:text-stone-50 focus:ring-2 focus:ring-[var(--ui-primary)] focus:border-transparent"
|
||||
placeholder="Try it free at whyrating.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { WhyMyRatingLogo, PreviewScaler } from '@/components';
|
||||
|
||||
export function CTATemplateSquare() {
|
||||
const [hook, setHook] = useState('Still reading reviews one by one?');
|
||||
const [valueProp, setValueProp] = useState('Get AI-powered insights in 45 seconds');
|
||||
const [ctaText, setCtaText] = useState('Try it free at whyrating.com');
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
|
||||
const downloadPng = async () => {
|
||||
setIsDownloading(true);
|
||||
try {
|
||||
const response = await fetch('/api/screenshot', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
template: 'cta-square',
|
||||
params: { hook, valueProp, ctaText }
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Screenshot failed');
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.download = `whyrating-cta-square-${Date.now()}.png`;
|
||||
link.href = url;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Download failed:', error);
|
||||
alert('Download failed. Please try again.');
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-stone-900 rounded-xl border border-slate-100 shadow-sm dark:border-stone-700 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-stone-50">CTA Post (Square)</h3>
|
||||
<p className="text-sm text-slate-500 dark:text-stone-500">1080 × 1080px • Instagram, Facebook</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={downloadPng}
|
||||
disabled={isDownloading}
|
||||
className="px-4 py-2 bg-[var(--ui-primary)] text-white rounded-lg text-sm font-medium hover:bg-[var(--ui-primary-hover)] transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isDownloading ? 'Generating...' : 'Download PNG'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Preview */}
|
||||
<PreviewScaler width={540} height={540}>
|
||||
<div
|
||||
className="rounded-xl p-12 flex flex-col justify-between relative overflow-hidden bg-gradient-to-br from-slate-800 to-slate-900"
|
||||
style={{ width: '540px', height: '540px' }}
|
||||
>
|
||||
{/* Decorative stars */}
|
||||
<div className="absolute top-8 right-8 text-[#FBBC05] text-4xl opacity-20">★</div>
|
||||
<div className="absolute bottom-32 left-10 text-[#FBBC05] text-2xl opacity-15">★</div>
|
||||
|
||||
{/* Hook */}
|
||||
<div className="pt-4">
|
||||
<div className="text-white text-3xl font-bold leading-tight max-w-md">{hook}</div>
|
||||
</div>
|
||||
|
||||
{/* Value Prop */}
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="text-white text-2xl font-medium leading-snug text-center max-w-lg">{valueProp}</div>
|
||||
</div>
|
||||
|
||||
{/* CTA */}
|
||||
<div className="pb-4">
|
||||
<div className="bg-[#F59E0B] text-slate-900 text-xl font-bold px-6 py-4 rounded-xl text-center mb-6">
|
||||
{ctaText}
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<WhyMyRatingLogo size={100} variant="horizontal-v2" colorScheme="dark" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PreviewScaler>
|
||||
|
||||
{/* Editor */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-stone-300 mb-1">Hook Question</label>
|
||||
<textarea
|
||||
value={hook}
|
||||
onChange={(e) => setHook(e.target.value)}
|
||||
rows={2}
|
||||
className="w-full px-3 py-2 border border-slate-300 dark:border-stone-600 rounded-lg bg-white dark:bg-stone-800 text-slate-900 dark:text-stone-50 focus:ring-2 focus:ring-[var(--ui-primary)] focus:border-transparent resize-none"
|
||||
placeholder="Still reading reviews one by one?"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-stone-300 mb-1">Value Proposition</label>
|
||||
<textarea
|
||||
value={valueProp}
|
||||
onChange={(e) => setValueProp(e.target.value)}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-slate-300 dark:border-stone-600 rounded-lg bg-white dark:bg-stone-800 text-slate-900 dark:text-stone-50 focus:ring-2 focus:ring-[var(--ui-primary)] focus:border-transparent resize-none"
|
||||
placeholder="Get AI-powered insights in 45 seconds"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-stone-300 mb-1">Call to Action</label>
|
||||
<input
|
||||
type="text"
|
||||
value={ctaText}
|
||||
onChange={(e) => setCtaText(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 dark:border-stone-600 rounded-lg bg-white dark:bg-stone-800 text-slate-900 dark:text-stone-50 focus:ring-2 focus:ring-[var(--ui-primary)] focus:border-transparent"
|
||||
placeholder="Try it free at whyrating.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
68
whyrating-templates/src/components/templates/EmailHeader.tsx
Normal file
68
whyrating-templates/src/components/templates/EmailHeader.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { WhyMyRatingLogo, PreviewScaler } from '@/components';
|
||||
|
||||
export function EmailHeader() {
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
|
||||
const downloadPng = async () => {
|
||||
setIsDownloading(true);
|
||||
try {
|
||||
const response = await fetch('/api/screenshot', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
template: 'email-header',
|
||||
params: {}
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Screenshot failed');
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.download = `whyrating-email-header-${Date.now()}.png`;
|
||||
link.href = url;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Download failed:', error);
|
||||
alert('Download failed. Please try again.');
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-stone-900 rounded-xl border border-slate-100 shadow-sm dark:border-stone-700 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-stone-50">Email Header</h3>
|
||||
<p className="text-sm text-slate-500 dark:text-stone-500">600 × 150px • Email banner</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={downloadPng}
|
||||
disabled={isDownloading}
|
||||
className="px-4 py-2 bg-[var(--ui-primary)] text-white rounded-lg text-sm font-medium hover:bg-[var(--ui-primary-hover)] transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isDownloading ? 'Generating...' : 'Download PNG'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Preview */}
|
||||
<PreviewScaler width={600} height={150}>
|
||||
<div
|
||||
className="rounded-lg flex flex-col items-center justify-center gap-2 bg-gradient-to-br from-slate-800 to-slate-900"
|
||||
style={{ width: '600px', height: '150px' }}
|
||||
>
|
||||
<WhyMyRatingLogo size={80} variant="horizontal-v2" colorScheme="dark" />
|
||||
<span className="text-[#A8A29E] text-sm tracking-wide">
|
||||
The story behind your stars
|
||||
</span>
|
||||
</div>
|
||||
</PreviewScaler>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { WhyMyRatingLogo, PreviewScaler } from '@/components';
|
||||
|
||||
export function EmailHeaderMinimal() {
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
|
||||
const downloadPng = async () => {
|
||||
setIsDownloading(true);
|
||||
try {
|
||||
const response = await fetch('/api/screenshot', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
template: 'email-header-minimal',
|
||||
params: {}
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Screenshot failed');
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.download = `whyrating-email-header-minimal-${Date.now()}.png`;
|
||||
link.href = url;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Download failed:', error);
|
||||
alert('Download failed. Please try again.');
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-stone-900 rounded-xl border border-slate-100 shadow-sm dark:border-stone-700 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-stone-50">Email Header (Minimal)</h3>
|
||||
<p className="text-sm text-slate-500 dark:text-stone-500">600 × 100px • Email banner</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={downloadPng}
|
||||
disabled={isDownloading}
|
||||
className="px-4 py-2 bg-[var(--ui-primary)] text-white rounded-lg text-sm font-medium hover:bg-[var(--ui-primary-hover)] transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isDownloading ? 'Generating...' : 'Download PNG'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Preview */}
|
||||
<PreviewScaler width={600} height={100}>
|
||||
<div
|
||||
className="rounded-lg flex items-center justify-center bg-gradient-to-r from-slate-800 to-slate-900"
|
||||
style={{ width: '600px', height: '100px' }}
|
||||
>
|
||||
<WhyMyRatingLogo size={70} variant="horizontal-v2" colorScheme="dark" />
|
||||
</div>
|
||||
</PreviewScaler>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
242
whyrating-templates/src/components/templates/EmailSignature.tsx
Normal file
242
whyrating-templates/src/components/templates/EmailSignature.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { WhyMyRatingLogo, PreviewScaler } from '@/components';
|
||||
|
||||
export function EmailSignature() {
|
||||
const [name, setName] = useState('Your Name');
|
||||
const [title, setTitle] = useState('Founder');
|
||||
const [email, setEmail] = useState('hello@whyrating.com');
|
||||
const [phone, setPhone] = useState('');
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [showPreview, setShowPreview] = useState(true);
|
||||
|
||||
// Brand colors from guidelines
|
||||
const colors = {
|
||||
dark: '#1E293B',
|
||||
slate: '#64748B',
|
||||
accent: '#F59E0B',
|
||||
primary: '#4285F4',
|
||||
};
|
||||
|
||||
// Generate the HTML signature with inline styles
|
||||
const generateHTML = () => {
|
||||
const phoneRow = phone
|
||||
? `<tr>
|
||||
<td style="font-family: Arial, sans-serif; font-size: 14px; color: ${colors.slate}; padding-top: 4px;">
|
||||
<a href="tel:${phone.replace(/[^0-9+]/g, '')}" style="color: ${colors.slate}; text-decoration: none;">${phone}</a>
|
||||
</td>
|
||||
</tr>`
|
||||
: '';
|
||||
|
||||
return `<!-- WhyMyRating Email Signature -->
|
||||
<table cellpadding="0" cellspacing="0" border="0" style="font-family: Arial, sans-serif; max-width: 400px;">
|
||||
<tr>
|
||||
<td style="padding-bottom: 12px; border-bottom: 1px solid #E2E8F0;">
|
||||
<table cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td style="font-family: Arial, sans-serif; font-size: 16px; font-weight: 600; color: ${colors.dark};">
|
||||
${name}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-family: Arial, sans-serif; font-size: 14px; color: ${colors.slate}; padding-top: 2px;">
|
||||
${title}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding-top: 12px;">
|
||||
<table cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td style="vertical-align: middle; padding-right: 10px;">
|
||||
<!-- WhyMyRating Logo Icon (32px) -->
|
||||
<a href="https://whyrating.com" target="_blank" style="text-decoration: none;">
|
||||
<img src="https://whyrating.com/logo-icon.png" alt="WhyMyRating" width="32" height="32" style="display: block; border: 0;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="vertical-align: middle;">
|
||||
<table cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td style="font-family: Arial, sans-serif; font-size: 14px; font-weight: 700; color: ${colors.dark};">
|
||||
<a href="https://whyrating.com" target="_blank" style="text-decoration: none; color: ${colors.dark};">
|
||||
whyrating<span style="color: ${colors.accent};">.com</span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-family: Arial, sans-serif; font-size: 12px; font-style: italic; color: ${colors.slate}; padding-top: 2px;">
|
||||
The story behind your stars
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding-top: 8px;">
|
||||
<table cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td style="font-family: Arial, sans-serif; font-size: 14px; color: ${colors.primary};">
|
||||
<a href="mailto:${email}" style="color: ${colors.primary}; text-decoration: none;">${email}</a>
|
||||
</td>
|
||||
</tr>
|
||||
${phoneRow}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- End WhyMyRating Email Signature -->`;
|
||||
};
|
||||
|
||||
const copyHTML = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(generateHTML());
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-stone-900 rounded-xl border border-slate-100 shadow-sm dark:border-stone-700 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-stone-50">Email Signature</h3>
|
||||
<p className="text-sm text-slate-500 dark:text-stone-500">HTML signature for email clients</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setShowPreview(!showPreview)}
|
||||
className="px-4 py-2 border border-slate-300 dark:border-stone-600 text-slate-700 dark:text-stone-300 rounded-lg text-sm font-medium hover:bg-slate-50 dark:hover:bg-stone-800 transition-colors"
|
||||
>
|
||||
{showPreview ? 'Hide Preview' : 'Preview'}
|
||||
</button>
|
||||
<button
|
||||
onClick={copyHTML}
|
||||
className="px-4 py-2 bg-[var(--ui-primary)] text-white rounded-lg text-sm font-medium hover:bg-[var(--ui-primary-hover)] transition-colors"
|
||||
>
|
||||
{copied ? 'Copied!' : 'Copy HTML'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Preview */}
|
||||
{showPreview && (
|
||||
<div className="bg-slate-50 dark:bg-stone-800 rounded-lg p-6">
|
||||
<p className="text-xs text-slate-500 dark:text-stone-500 mb-4 uppercase tracking-wide font-medium">Preview</p>
|
||||
<div className="bg-white dark:bg-stone-900 rounded-lg p-4 border border-slate-100 dark:border-stone-700">
|
||||
{/* Signature Preview */}
|
||||
<div style={{ fontFamily: 'Arial, sans-serif', maxWidth: '400px' }}>
|
||||
{/* Name and Title with border */}
|
||||
<div style={{ paddingBottom: '12px', borderBottom: '1px solid #E2E8F0' }}>
|
||||
<div style={{ fontSize: '16px', fontWeight: 600, color: colors.dark }} className="dark:!text-stone-50">
|
||||
{name}
|
||||
</div>
|
||||
<div style={{ fontSize: '14px', color: colors.slate, paddingTop: '2px' }} className="dark:!text-stone-400">
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Logo and Wordmark */}
|
||||
<div style={{ paddingTop: '12px', display: 'flex', alignItems: 'center' }}>
|
||||
<div style={{ marginRight: '10px' }}>
|
||||
<WhyMyRatingLogo size={32} variant="icon" colorScheme="light" />
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: '14px', fontWeight: 700, color: colors.dark }} className="dark:!text-stone-50">
|
||||
whyrating<span style={{ color: colors.accent }}>.com</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', fontStyle: 'italic', color: colors.slate, paddingTop: '2px' }} className="dark:!text-stone-400">
|
||||
The story behind your stars
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contact Info */}
|
||||
<div style={{ paddingTop: '8px' }}>
|
||||
<div style={{ fontSize: '14px', color: colors.primary }}>
|
||||
{email}
|
||||
</div>
|
||||
{phone && (
|
||||
<div style={{ fontSize: '14px', color: colors.slate, paddingTop: '4px' }} className="dark:!text-stone-400">
|
||||
{phone}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Editor */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-stone-300 mb-1">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 dark:border-stone-600 rounded-lg bg-white dark:bg-stone-800 text-slate-900 dark:text-stone-50 focus:ring-2 focus:ring-[var(--ui-primary)] focus:border-transparent"
|
||||
placeholder="Your Name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-stone-300 mb-1">
|
||||
Title
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 dark:border-stone-600 rounded-lg bg-white dark:bg-stone-800 text-slate-900 dark:text-stone-50 focus:ring-2 focus:ring-[var(--ui-primary)] focus:border-transparent"
|
||||
placeholder="Founder"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-stone-300 mb-1">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 dark:border-stone-600 rounded-lg bg-white dark:bg-stone-800 text-slate-900 dark:text-stone-50 focus:ring-2 focus:ring-[var(--ui-primary)] focus:border-transparent"
|
||||
placeholder="hello@whyrating.com"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-stone-300 mb-1">
|
||||
Phone <span className="text-slate-400 dark:text-stone-500 font-normal">(optional)</span>
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 dark:border-stone-600 rounded-lg bg-white dark:bg-stone-800 text-slate-900 dark:text-stone-50 focus:ring-2 focus:ring-[var(--ui-primary)] focus:border-transparent"
|
||||
placeholder="+1 (555) 123-4567"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Instructions */}
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 mt-4">
|
||||
<p className="text-sm text-blue-800 dark:text-blue-300 font-medium mb-2">How to use:</p>
|
||||
<ol className="text-sm text-blue-700 dark:text-blue-400 space-y-1 list-decimal list-inside">
|
||||
<li>Fill in your details above</li>
|
||||
<li>Click “Copy HTML” to copy the signature</li>
|
||||
<li>Paste into your email client's signature settings</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { WhyMyRatingLogo, PreviewScaler } from '@/components';
|
||||
|
||||
export function StatTemplateLandscape() {
|
||||
const [stat, setStat] = useState('89%');
|
||||
const [headline, setHeadline] = useState('of consumers read business responses to reviews');
|
||||
const [source, setSource] = useState('BrightLocal 2024');
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
|
||||
const downloadPng = async () => {
|
||||
setIsDownloading(true);
|
||||
try {
|
||||
const response = await fetch('/api/screenshot', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
template: 'stat-landscape',
|
||||
params: { stat, headline, source }
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Screenshot failed');
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.download = `whyrating-stat-landscape-${Date.now()}.png`;
|
||||
link.href = url;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Download failed:', error);
|
||||
alert('Download failed. Please try again.');
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-stone-900 rounded-xl border border-slate-100 shadow-sm dark:border-stone-700 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-stone-50">Stat Post (Landscape)</h3>
|
||||
<p className="text-sm text-slate-500 dark:text-stone-500">1200 × 675px • LinkedIn, Twitter</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={downloadPng}
|
||||
disabled={isDownloading}
|
||||
className="px-4 py-2 bg-[var(--ui-primary)] text-white rounded-lg text-sm font-medium hover:bg-[var(--ui-primary-hover)] transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isDownloading ? 'Generating...' : 'Download PNG'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Preview */}
|
||||
<PreviewScaler width={600} height={338}>
|
||||
<div
|
||||
className="rounded-xl p-8 flex flex-col justify-between relative overflow-hidden bg-gradient-to-br from-slate-800 to-slate-900"
|
||||
style={{ width: '600px', height: '338px' }}
|
||||
>
|
||||
{/* Decorative stars */}
|
||||
<div className="absolute top-6 right-6 text-[#FBBC05] text-3xl opacity-20">★</div>
|
||||
<div className="absolute bottom-16 right-12 text-[#FBBC05] text-xl opacity-15">★</div>
|
||||
<div className="absolute top-1/3 right-8 text-[#FBBC05] text-lg opacity-10">★</div>
|
||||
<div className="absolute top-1/2 left-1/4 text-[#FBBC05] text-sm opacity-10">★</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 flex items-center">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="text-[#FBBC05] text-6xl font-bold shrink-0">{stat}</div>
|
||||
<div className="text-white text-xl font-medium leading-tight max-w-sm">{headline}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-end justify-between">
|
||||
<div className="text-slate-400 text-xs">Source: {source}</div>
|
||||
<WhyMyRatingLogo size={60} variant="horizontal-v2" colorScheme="dark" />
|
||||
</div>
|
||||
</div>
|
||||
</PreviewScaler>
|
||||
|
||||
{/* Editor */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-stone-300 mb-1">Statistic</label>
|
||||
<input
|
||||
type="text"
|
||||
value={stat}
|
||||
onChange={(e) => setStat(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 dark:border-stone-600 rounded-lg bg-white dark:bg-stone-800 text-slate-900 dark:text-stone-50 focus:ring-2 focus:ring-[var(--ui-primary)] focus:border-transparent"
|
||||
placeholder="89%"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-stone-300 mb-1">Headline</label>
|
||||
<textarea
|
||||
value={headline}
|
||||
onChange={(e) => setHeadline(e.target.value)}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-slate-300 dark:border-stone-600 rounded-lg bg-white dark:bg-stone-800 text-slate-900 dark:text-stone-50 focus:ring-2 focus:ring-[var(--ui-primary)] focus:border-transparent resize-none"
|
||||
placeholder="of consumers read business responses to reviews"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-stone-300 mb-1">Source</label>
|
||||
<input
|
||||
type="text"
|
||||
value={source}
|
||||
onChange={(e) => setSource(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 dark:border-stone-600 rounded-lg bg-white dark:bg-stone-800 text-slate-900 dark:text-stone-50 focus:ring-2 focus:ring-[var(--ui-primary)] focus:border-transparent"
|
||||
placeholder="BrightLocal 2024"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { WhyMyRatingLogo, PreviewScaler } from '@/components';
|
||||
|
||||
export function StatTemplateSquare() {
|
||||
const [stat, setStat] = useState('89%');
|
||||
const [headline, setHeadline] = useState('of consumers read business responses to reviews');
|
||||
const [source, setSource] = useState('BrightLocal 2024');
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
|
||||
const downloadPng = async () => {
|
||||
setIsDownloading(true);
|
||||
try {
|
||||
const response = await fetch('/api/screenshot', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
template: 'stat-square',
|
||||
params: { stat, headline, source }
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Screenshot failed');
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.download = `whyrating-stat-square-${Date.now()}.png`;
|
||||
link.href = url;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Download failed:', error);
|
||||
alert('Download failed. Please try again.');
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-stone-900 rounded-xl border border-slate-100 shadow-sm dark:border-stone-700 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-stone-50">Stat Post (Square)</h3>
|
||||
<p className="text-sm text-slate-500 dark:text-stone-500">1080 × 1080px • Instagram, Facebook</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={downloadPng}
|
||||
disabled={isDownloading}
|
||||
className="px-4 py-2 bg-[var(--ui-primary)] text-white rounded-lg text-sm font-medium hover:bg-[var(--ui-primary-hover)] transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isDownloading ? 'Generating...' : 'Download PNG'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Preview */}
|
||||
<PreviewScaler width={540} height={540}>
|
||||
<div
|
||||
className="rounded-xl p-12 flex flex-col justify-between relative overflow-hidden bg-gradient-to-br from-slate-800 to-slate-900"
|
||||
style={{ width: '540px', height: '540px' }}
|
||||
>
|
||||
{/* Decorative stars */}
|
||||
<div className="absolute top-8 right-8 text-[#FBBC05] text-4xl opacity-20">★</div>
|
||||
<div className="absolute bottom-20 right-16 text-[#FBBC05] text-2xl opacity-15">★</div>
|
||||
<div className="absolute top-1/3 right-12 text-[#FBBC05] text-xl opacity-10">★</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 flex flex-col justify-center">
|
||||
<div className="text-[#FBBC05] text-8xl font-bold mb-4">{stat}</div>
|
||||
<div className="text-white text-2xl font-medium leading-tight max-w-md">{headline}</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-end justify-between">
|
||||
<div className="text-slate-400 text-sm">Source: {source}</div>
|
||||
<WhyMyRatingLogo size={80} variant="horizontal-v2" colorScheme="dark" />
|
||||
</div>
|
||||
</div>
|
||||
</PreviewScaler>
|
||||
|
||||
{/* Editor */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-stone-300 mb-1">
|
||||
Statistic
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={stat}
|
||||
onChange={(e) => setStat(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 dark:border-stone-600 rounded-lg bg-white dark:bg-stone-800 text-slate-900 dark:text-stone-50 focus:ring-2 focus:ring-[var(--ui-primary)] focus:border-transparent"
|
||||
placeholder="89%"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-stone-300 mb-1">
|
||||
Headline
|
||||
</label>
|
||||
<textarea
|
||||
value={headline}
|
||||
onChange={(e) => setHeadline(e.target.value)}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-slate-300 dark:border-stone-600 rounded-lg bg-white dark:bg-stone-800 text-slate-900 dark:text-stone-50 focus:ring-2 focus:ring-[var(--ui-primary)] focus:border-transparent resize-none"
|
||||
placeholder="of consumers read business responses to reviews"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-stone-300 mb-1">
|
||||
Source
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={source}
|
||||
onChange={(e) => setSource(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 dark:border-stone-600 rounded-lg bg-white dark:bg-stone-800 text-slate-900 dark:text-stone-50 focus:ring-2 focus:ring-[var(--ui-primary)] focus:border-transparent"
|
||||
placeholder="BrightLocal 2024"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { WhyMyRatingLogo, PreviewScaler } from '@/components';
|
||||
|
||||
export function TipTemplateLandscape() {
|
||||
const [tipNumber, setTipNumber] = useState('1');
|
||||
const [tipTitle, setTipTitle] = useState('Respond to negative reviews within 24 hours');
|
||||
const [tipBody, setTipBody] = useState('Quick responses show customers you care and can turn a negative experience into a positive one.');
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
|
||||
const downloadPng = async () => {
|
||||
setIsDownloading(true);
|
||||
try {
|
||||
const response = await fetch('/api/screenshot', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
template: 'tip-landscape',
|
||||
params: { tipNumber, tipTitle, tipBody }
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Screenshot failed');
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.download = `whyrating-tip-${tipNumber}-landscape-${Date.now()}.png`;
|
||||
link.href = url;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Download failed:', error);
|
||||
alert('Download failed. Please try again.');
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-stone-900 rounded-xl border border-slate-100 shadow-sm dark:border-stone-700 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-stone-50">Tip Post (Landscape)</h3>
|
||||
<p className="text-sm text-slate-500 dark:text-stone-500">1200 × 675px • LinkedIn, Twitter/X</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={downloadPng}
|
||||
disabled={isDownloading}
|
||||
className="px-4 py-2 bg-[var(--ui-primary)] text-white rounded-lg text-sm font-medium hover:bg-[var(--ui-primary-hover)] transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isDownloading ? 'Generating...' : 'Download PNG'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Preview */}
|
||||
<PreviewScaler width={600} height={338}>
|
||||
<div
|
||||
className="rounded-xl p-10 flex relative overflow-hidden bg-gradient-to-br from-slate-800 to-slate-900"
|
||||
style={{ width: '600px', height: '338px' }}
|
||||
>
|
||||
{/* Decorative stars */}
|
||||
<div className="absolute top-6 right-6 text-[#FBBC05] text-4xl opacity-20">★</div>
|
||||
<div className="absolute bottom-16 right-24 text-[#FBBC05] text-2xl opacity-15">★</div>
|
||||
|
||||
{/* Left section */}
|
||||
<div className="flex flex-col justify-center pr-8 border-r border-slate-600">
|
||||
<div className="text-[#FBBC05] text-lg font-bold tracking-widest uppercase">TIP</div>
|
||||
<div className="text-[#FBBC05] text-6xl font-bold leading-none">#{tipNumber}</div>
|
||||
</div>
|
||||
|
||||
{/* Right section */}
|
||||
<div className="flex-1 flex flex-col justify-between pl-8">
|
||||
<div className="flex-1 flex flex-col justify-center">
|
||||
<h2 className="text-white text-2xl font-bold leading-tight mb-3">{tipTitle}</h2>
|
||||
<p className="text-slate-300 text-base leading-relaxed max-w-md">{tipBody}</p>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<WhyMyRatingLogo size={80} variant="horizontal-v2" colorScheme="dark" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PreviewScaler>
|
||||
|
||||
{/* Editor */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-stone-300 mb-1">Tip Number</label>
|
||||
<input
|
||||
type="text"
|
||||
value={tipNumber}
|
||||
onChange={(e) => setTipNumber(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 dark:border-stone-600 rounded-lg bg-white dark:bg-stone-800 text-slate-900 dark:text-stone-50 focus:ring-2 focus:ring-[var(--ui-primary)] focus:border-transparent"
|
||||
placeholder="1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-stone-300 mb-1">Tip Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={tipTitle}
|
||||
onChange={(e) => setTipTitle(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 dark:border-stone-600 rounded-lg bg-white dark:bg-stone-800 text-slate-900 dark:text-stone-50 focus:ring-2 focus:ring-[var(--ui-primary)] focus:border-transparent"
|
||||
placeholder="Respond to negative reviews within 24 hours"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-stone-300 mb-1">Tip Body</label>
|
||||
<textarea
|
||||
value={tipBody}
|
||||
onChange={(e) => setTipBody(e.target.value)}
|
||||
rows={4}
|
||||
className="w-full px-3 py-2 border border-slate-300 dark:border-stone-600 rounded-lg bg-white dark:bg-stone-800 text-slate-900 dark:text-stone-50 focus:ring-2 focus:ring-[var(--ui-primary)] focus:border-transparent resize-none"
|
||||
placeholder="Quick responses show customers you care..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { WhyMyRatingLogo, PreviewScaler } from '@/components';
|
||||
|
||||
export function TipTemplateSquare() {
|
||||
const [tipNumber, setTipNumber] = useState('1');
|
||||
const [tipTitle, setTipTitle] = useState('Respond to negative reviews within 24 hours');
|
||||
const [tipBody, setTipBody] = useState('Quick responses show customers you care and can turn a negative experience into a positive one.');
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
|
||||
const downloadPng = async () => {
|
||||
setIsDownloading(true);
|
||||
try {
|
||||
const response = await fetch('/api/screenshot', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
template: 'tip-square',
|
||||
params: { tipNumber, tipTitle, tipBody }
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Screenshot failed');
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.download = `whyrating-tip-${tipNumber}-square-${Date.now()}.png`;
|
||||
link.href = url;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Download failed:', error);
|
||||
alert('Download failed. Please try again.');
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-stone-900 rounded-xl border border-slate-100 shadow-sm dark:border-stone-700 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-stone-50">Tip Post (Square)</h3>
|
||||
<p className="text-sm text-slate-500 dark:text-stone-500">1080 × 1080px • Instagram, Facebook</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={downloadPng}
|
||||
disabled={isDownloading}
|
||||
className="px-4 py-2 bg-[var(--ui-primary)] text-white rounded-lg text-sm font-medium hover:bg-[var(--ui-primary-hover)] transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isDownloading ? 'Generating...' : 'Download PNG'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Preview */}
|
||||
<PreviewScaler width={540} height={540}>
|
||||
<div
|
||||
className="rounded-xl p-12 flex flex-col justify-between relative overflow-hidden bg-gradient-to-br from-slate-800 to-slate-900"
|
||||
style={{ width: '540px', height: '540px' }}
|
||||
>
|
||||
{/* Decorative elements */}
|
||||
<div className="absolute top-6 left-6 w-24 h-24 rounded-full bg-[#FBBC05] opacity-5"></div>
|
||||
<div className="absolute bottom-32 right-8 w-16 h-16 rounded-full bg-[#FBBC05] opacity-5"></div>
|
||||
<div className="absolute top-1/2 right-6 text-[#FBBC05] text-3xl opacity-15">★</div>
|
||||
<div className="absolute bottom-16 left-12 text-[#FBBC05] text-xl opacity-10">★</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 flex flex-col justify-center z-10">
|
||||
<div className="text-[#FBBC05] text-xl font-bold tracking-widest uppercase mb-4">TIP #{tipNumber}</div>
|
||||
<div className="text-white text-3xl font-bold leading-tight mb-6 max-w-lg">{tipTitle}</div>
|
||||
<div className="text-slate-300 text-lg leading-relaxed max-w-md">{tipBody}</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-end justify-end">
|
||||
<WhyMyRatingLogo size={80} variant="horizontal-v2" colorScheme="dark" />
|
||||
</div>
|
||||
</div>
|
||||
</PreviewScaler>
|
||||
|
||||
{/* Editor */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-stone-300 mb-1">Tip Number</label>
|
||||
<input
|
||||
type="text"
|
||||
value={tipNumber}
|
||||
onChange={(e) => setTipNumber(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 dark:border-stone-600 rounded-lg bg-white dark:bg-stone-800 text-slate-900 dark:text-stone-50 focus:ring-2 focus:ring-[var(--ui-primary)] focus:border-transparent"
|
||||
placeholder="1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-stone-300 mb-1">Tip Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={tipTitle}
|
||||
onChange={(e) => setTipTitle(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 dark:border-stone-600 rounded-lg bg-white dark:bg-stone-800 text-slate-900 dark:text-stone-50 focus:ring-2 focus:ring-[var(--ui-primary)] focus:border-transparent"
|
||||
placeholder="Respond to negative reviews within 24 hours"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-stone-300 mb-1">Tip Body</label>
|
||||
<textarea
|
||||
value={tipBody}
|
||||
onChange={(e) => setTipBody(e.target.value)}
|
||||
rows={4}
|
||||
className="w-full px-3 py-2 border border-slate-300 dark:border-stone-600 rounded-lg bg-white dark:bg-stone-800 text-slate-900 dark:text-stone-50 focus:ring-2 focus:ring-[var(--ui-primary)] focus:border-transparent resize-none"
|
||||
placeholder="Quick responses show customers you care..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
9
whyrating-templates/src/components/templates/index.ts
Normal file
9
whyrating-templates/src/components/templates/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export { StatTemplateSquare } from './StatTemplateSquare';
|
||||
export { StatTemplateLandscape } from './StatTemplateLandscape';
|
||||
export { TipTemplateSquare } from './TipTemplateSquare';
|
||||
export { TipTemplateLandscape } from './TipTemplateLandscape';
|
||||
export { CTATemplateSquare } from './CTATemplateSquare';
|
||||
export { CTATemplateLandscape } from './CTATemplateLandscape';
|
||||
export { EmailSignature } from './EmailSignature';
|
||||
export { EmailHeader } from './EmailHeader';
|
||||
export { EmailHeaderMinimal } from './EmailHeaderMinimal';
|
||||
31
whyrating-templates/src/components/ui/CopyButton.tsx
Normal file
31
whyrating-templates/src/components/ui/CopyButton.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
interface CopyButtonProps {
|
||||
text: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export function CopyButton({ text, label }: CopyButtonProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const copy = () => {
|
||||
navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={copy}
|
||||
className={`px-2 py-1 text-xs font-mono rounded border transition-all cursor-pointer
|
||||
${copied
|
||||
? 'bg-green-500 border-green-500 text-white'
|
||||
: 'bg-white dark:bg-stone-800 border-slate-100 dark:border-stone-700 text-slate-600 dark:text-stone-400 hover:border-slate-300 dark:hover:border-stone-600'
|
||||
}`}
|
||||
>
|
||||
{copied ? 'Copied!' : label || text}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
74
whyrating-templates/src/components/ui/DownloadDropdown.tsx
Normal file
74
whyrating-templates/src/components/ui/DownloadDropdown.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
|
||||
export interface DownloadOption {
|
||||
label: string;
|
||||
sublabel?: string;
|
||||
action: () => void;
|
||||
divider?: false;
|
||||
}
|
||||
|
||||
export interface DownloadDivider {
|
||||
divider: true;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export type DownloadOptionOrDivider = DownloadOption | DownloadDivider;
|
||||
|
||||
interface DownloadDropdownProps {
|
||||
options: DownloadOptionOrDivider[];
|
||||
buttonText?: string;
|
||||
buttonClass?: string;
|
||||
}
|
||||
|
||||
export function DownloadDropdown({
|
||||
options,
|
||||
buttonText = "Download",
|
||||
buttonClass = ""
|
||||
}: DownloadDropdownProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={`px-3 py-2 bg-[var(--ui-primary)] text-white rounded-lg text-xs font-medium hover:bg-[var(--ui-primary-hover)] transition-colors cursor-pointer flex items-center gap-1 ${buttonClass}`}
|
||||
>
|
||||
{buttonText}
|
||||
<span className={`text-[10px] transition-transform ${isOpen ? 'rotate-180' : ''}`}>▼</span>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="absolute right-0 top-full mt-1 bg-white dark:bg-stone-800 border border-slate-100 dark:border-stone-700 rounded-lg shadow-lg py-1 min-w-48 z-50 max-h-80 overflow-y-auto">
|
||||
{options.map((option, i) => (
|
||||
option.divider ? (
|
||||
<div key={i} className="px-3 py-1 text-[10px] font-semibold text-slate-400 dark:text-stone-500 uppercase tracking-wider border-t border-slate-100 dark:border-stone-700 mt-1 pt-2">
|
||||
{option.label}
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => { option.action(); setIsOpen(false); }}
|
||||
className="w-full px-3 py-2 text-left text-sm text-slate-700 dark:text-stone-300 hover:bg-slate-50 dark:hover:bg-stone-700/70 transition-colors cursor-pointer border-none bg-transparent flex flex-col"
|
||||
>
|
||||
<span>{option.label}</span>
|
||||
{option.sublabel && <span className="text-[10px] text-slate-400 dark:text-stone-500">{option.sublabel}</span>}
|
||||
</button>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
19
whyrating-templates/src/components/ui/Section.tsx
Normal file
19
whyrating-templates/src/components/ui/Section.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface SectionProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Section({ title, description, children }: SectionProps) {
|
||||
return (
|
||||
<section className="bg-white dark:bg-stone-900 rounded-2xl p-8 shadow-sm mb-6 border border-slate-100 dark:border-stone-700/50">
|
||||
{title && <h2 className="text-xl font-semibold text-slate-900 dark:text-stone-50 mb-1">{title}</h2>}
|
||||
{description && <p className="text-sm text-slate-600 dark:text-stone-400 mb-6">{description}</p>}
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
4
whyrating-templates/src/components/ui/index.ts
Normal file
4
whyrating-templates/src/components/ui/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { DownloadDropdown } from './DownloadDropdown';
|
||||
export type { DownloadOption, DownloadDivider, DownloadOptionOrDivider } from './DownloadDropdown';
|
||||
export { Section } from './Section';
|
||||
export { CopyButton } from './CopyButton';
|
||||
47
whyrating-templates/src/lib/BrandContext.tsx
Normal file
47
whyrating-templates/src/lib/BrandContext.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import { BrandConfig, getBrandConfig, saveBrandConfig, resetBrandConfig, defaultBrandConfig } from './brand-config';
|
||||
|
||||
interface BrandContextType {
|
||||
config: BrandConfig;
|
||||
updateConfig: (updates: Partial<BrandConfig>) => void;
|
||||
resetConfig: () => void;
|
||||
isLoaded: boolean;
|
||||
}
|
||||
|
||||
const BrandContext = createContext<BrandContextType | undefined>(undefined);
|
||||
|
||||
export function BrandProvider({ children }: { children: ReactNode }) {
|
||||
const [config, setConfig] = useState<BrandConfig>(defaultBrandConfig);
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setConfig(getBrandConfig());
|
||||
setIsLoaded(true);
|
||||
}, []);
|
||||
|
||||
const updateConfig = (updates: Partial<BrandConfig>) => {
|
||||
const newConfig = saveBrandConfig(updates);
|
||||
setConfig(newConfig);
|
||||
};
|
||||
|
||||
const resetConfigHandler = () => {
|
||||
const newConfig = resetBrandConfig();
|
||||
setConfig(newConfig);
|
||||
};
|
||||
|
||||
return (
|
||||
<BrandContext.Provider value={{ config, updateConfig, resetConfig: resetConfigHandler, isLoaded }}>
|
||||
{children}
|
||||
</BrandContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useBrand() {
|
||||
const context = useContext(BrandContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useBrand must be used within a BrandProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
83
whyrating-templates/src/lib/brand-config.ts
Normal file
83
whyrating-templates/src/lib/brand-config.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
// Central brand configuration - edit these values to rebrand the entire site
|
||||
|
||||
export interface BrandConfig {
|
||||
// Core identity
|
||||
domain: string;
|
||||
domainTLD: string;
|
||||
fullDomain: string;
|
||||
tagline: string;
|
||||
|
||||
// Messaging
|
||||
valueProposition: string;
|
||||
ctaDefault: string;
|
||||
hookDefault: string;
|
||||
|
||||
// Contact
|
||||
supportEmail: string;
|
||||
|
||||
// Social
|
||||
twitterHandle: string;
|
||||
linkedInUrl: string;
|
||||
}
|
||||
|
||||
// Default brand configuration
|
||||
export const defaultBrandConfig: BrandConfig = {
|
||||
domain: 'whyrating',
|
||||
domainTLD: '.com',
|
||||
fullDomain: 'whyrating.com',
|
||||
tagline: 'The story behind your stars',
|
||||
|
||||
valueProposition: 'Get AI-powered insights in 45 seconds',
|
||||
ctaDefault: 'Try it free at whyrating.com',
|
||||
hookDefault: 'Still reading reviews one by one?',
|
||||
|
||||
supportEmail: 'support@whyrating.com',
|
||||
|
||||
twitterHandle: '@whyrating',
|
||||
linkedInUrl: 'https://linkedin.com/company/whyrating',
|
||||
};
|
||||
|
||||
// Local storage key for persisted config
|
||||
const STORAGE_KEY = 'brand-config';
|
||||
|
||||
// Get config from localStorage or return defaults
|
||||
export function getBrandConfig(): BrandConfig {
|
||||
if (typeof window === 'undefined') {
|
||||
return defaultBrandConfig;
|
||||
}
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
return { ...defaultBrandConfig, ...JSON.parse(stored) };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load brand config:', e);
|
||||
}
|
||||
|
||||
return defaultBrandConfig;
|
||||
}
|
||||
|
||||
// Save config to localStorage
|
||||
export function saveBrandConfig(config: Partial<BrandConfig>): BrandConfig {
|
||||
const newConfig = { ...getBrandConfig(), ...config };
|
||||
|
||||
// Auto-generate derived values
|
||||
newConfig.fullDomain = `${newConfig.domain}${newConfig.domainTLD}`;
|
||||
newConfig.ctaDefault = `Try it free at ${newConfig.fullDomain}`;
|
||||
newConfig.supportEmail = `support@${newConfig.fullDomain}`;
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(newConfig));
|
||||
}
|
||||
|
||||
return newConfig;
|
||||
}
|
||||
|
||||
// Reset to defaults
|
||||
export function resetBrandConfig(): BrandConfig {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
return defaultBrandConfig;
|
||||
}
|
||||
50
whyrating-templates/src/lib/constants.ts
Normal file
50
whyrating-templates/src/lib/constants.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// Brand color definitions and typography specs
|
||||
|
||||
export const logoColors = {
|
||||
star: { hex: '#FBBC05', name: 'Google Yellow', rgb: '251, 188, 5', element: 'Star' },
|
||||
magnifier: { hex: '#1E293B', name: 'Dark Slate', rgb: '30, 41, 59', element: 'Magnifier' },
|
||||
lens: { hex: '#FEF3C7', name: 'Amber 100', rgb: '254, 243, 199', element: 'Lens (interior)' },
|
||||
barLight: { hex: '#86EFAC', name: 'Green 300', rgb: '134, 239, 172', element: 'Bar Light' },
|
||||
barMid: { hex: '#22C55E', name: 'Green 500', rgb: '34, 197, 94', element: 'Bar Mid' },
|
||||
barDark: { hex: '#15803D', name: 'Green 700', rgb: '21, 128, 61', element: 'Bar Dark' },
|
||||
};
|
||||
|
||||
export const uiColors = {
|
||||
primary: { hex: '#4285F4', name: 'Google Blue', rgb: '66, 133, 244', usage: 'CTAs, links, primary actions' },
|
||||
secondary: { hex: '#1E40AF', name: 'Blue 800', rgb: '30, 64, 175', usage: 'Hover states, depth' },
|
||||
accent: { hex: '#F59E0B', name: 'Amber 500', rgb: '245, 158, 11', usage: 'Highlights, .com accent' },
|
||||
success: { hex: '#34A853', name: 'Google Green', rgb: '52, 168, 83', usage: 'Success states, positive' },
|
||||
error: { hex: '#EA4335', name: 'Google Red', rgb: '234, 67, 53', usage: 'Errors, warnings' },
|
||||
dark: { hex: '#1E293B', name: 'Slate 800', rgb: '30, 41, 59', usage: 'Primary text' },
|
||||
slate: { hex: '#64748B', name: 'Slate 500', rgb: '100, 116, 139', usage: 'Secondary text' },
|
||||
light: { hex: '#F8FAFC', name: 'Slate 50', rgb: '248, 250, 252', usage: 'Backgrounds' },
|
||||
darkBg: { hex: '#1C1917', name: 'Stone 900', rgb: '28, 25, 23', usage: 'Dark mode background' },
|
||||
};
|
||||
|
||||
export const typography = {
|
||||
wordmark: { font: 'Nunito', weight: 700, size: '36px', usage: 'Wordmark only' },
|
||||
h1: { font: 'Inter', weight: 700, size: '36px', usage: 'Page titles' },
|
||||
h2: { font: 'Inter', weight: 600, size: '30px', usage: 'Section headings' },
|
||||
h3: { font: 'Inter', weight: 600, size: '24px', usage: 'Subsection headings' },
|
||||
h4: { font: 'Inter', weight: 500, size: '20px', usage: 'Card titles' },
|
||||
body: { font: 'Inter', weight: 400, size: '16px', usage: 'Paragraphs' },
|
||||
small: { font: 'Inter', weight: 400, size: '14px', usage: 'Secondary info' },
|
||||
caption: { font: 'Inter', weight: 400, size: '12px', usage: 'Labels, metadata' },
|
||||
};
|
||||
|
||||
// Tab configuration
|
||||
export const tabs = [
|
||||
{ id: 'overview', label: 'Overview' },
|
||||
{ id: 'logo', label: 'Logo' },
|
||||
{ id: 'colors', label: 'Colors' },
|
||||
{ id: 'typography', label: 'Typography' },
|
||||
{ id: 'voice', label: 'Voice' },
|
||||
{ id: 'downloads', label: 'Downloads' },
|
||||
{ id: 'rationale', label: 'Rationale' },
|
||||
{ id: 'proportions', label: 'Proportions' },
|
||||
{ id: 'qa', label: 'QA' },
|
||||
{ id: 'guidelines', label: 'Guidelines' },
|
||||
{ id: 'settings', label: 'Settings' },
|
||||
] as const;
|
||||
|
||||
export type TabId = typeof tabs[number]['id'];
|
||||
Reference in New Issue
Block a user