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:
Alejandro Gutiérrez
2026-02-18 15:17:42 +01:00
parent 9a0881e852
commit ea5775da25
68 changed files with 15159 additions and 0 deletions

View 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 }
);
}
}

View 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 }
);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View 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;
}

View 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>
);
}

View 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>
);
}

View File

@@ -0,0 +1,7 @@
'use client';
import { BrandProvider } from '@/lib/BrandContext';
export function Providers({ children }: { children: React.ReactNode }) {
return <BrandProvider>{children}</BrandProvider>;
}

View File

@@ -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>
);
}

View 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>
);
}

View 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>
);
}

View File

@@ -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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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&apos;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&apos;t know <em className="text-white">why</em> it&apos;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&apos;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&apos;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">
&ldquo;This is about your Google rating.&rdquo;
</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">
&ldquo;We see what Google doesn&apos;t show.&rdquo;
</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">&ldquo;We&apos;re part of Google&rdquo;</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">&ldquo;We look deeper&rdquo;</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">
&ldquo;You&apos;ll see improvement.&rdquo;
</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. &ldquo;Platform&rdquo;</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. &ldquo;Helper&rdquo;</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">&ldquo;Analyze my reviews&rdquo;</p>
<p className="text-2xl text-white mb-4">&ldquo;Fix the right problems first&rdquo;</p>
<p className="text-2xl text-white">&ldquo;Questions? Just reply.&rdquo;</p>
</div>
<div className="opacity-50">
<p className="text-red-400 text-lg mb-4"> We don&apos;t say</p>
<p className="text-2xl text-white mb-4">&ldquo;Leverage AI-powered analytics&rdquo;</p>
<p className="text-2xl text-white mb-4">&ldquo;Unlock synergies&rdquo;</p>
<p className="text-2xl text-white">&ldquo;Please submit a ticket&rdquo;</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>
);
}

View 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>
);
}

View 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;
}

View 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';

View 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>
);
}

View 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&apos;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">&#10003; {item.status}</span></div>
</div>
</div>
))}
</div>
</Section>
</div>
);
}

View 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)' }}>&#9733;</div>
<div className="absolute text-[var(--brand-star)]" style={{ top: '75%', left: '15%', fontSize: '3rem', opacity: 0.15, filter: 'blur(1px)' }}>&#9733;</div>
<div className="absolute text-[var(--brand-star)]" style={{ top: '12%', left: '10%', fontSize: '1.5rem', opacity: 0.1, filter: 'blur(0.5px)' }}>&#9733;</div>
<div className="absolute text-[var(--brand-star)]" style={{ top: '80%', left: '85%', fontSize: '1.8rem', opacity: 0.12, filter: 'blur(0.5px)' }}>&#9733;</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%)' }}>&#9733;</div>
<div className="absolute text-[var(--brand-star)]" style={{ top: '20%', left: '88%', fontSize: '2.2rem', opacity: 0.12, filter: 'blur(0.5px)' }}>&#9733;</div>
<div className="absolute text-[var(--brand-star)]" style={{ top: '65%', left: '95%', fontSize: '1.2rem', opacity: 0.1, filter: 'blur(0.5px)' }}>&#9733;</div>
<div className="absolute text-[var(--brand-star)]" style={{ top: '12%', left: '78%', fontSize: '1rem', opacity: 0.08, filter: 'blur(0.5px)' }}>&#9733;</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 &rarr;</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>
);
}

View 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>&#8595;</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>
);
}

View 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>
);
}

View 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&apos;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">&quot;The story behind your stars&quot;</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)]">&quot;Analyze my reviews&quot;</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&apos;t have time for complex tools
and don&apos;t want to pay $300/month for enterprise software.
</p>
</Section>
</div>
);
}

View 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">&#10007;</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">&#10003;</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">&#8595; top (y=30)</text>
<text x="268" y="108" fontSize="10" fill="#EF4444">&#8592; right</text>
<text x="32" y="108" fontSize="10" fill="#EF4444" textAnchor="end">left &#8594;</text>
<text x="150" y="240" fontSize="10" fill="#EF4444" textAnchor="middle">&#8593; 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 &#8776; 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'}>&#8226; 1 top (upper point)</text>
<text x="380" y="172" fontSize="9" fill={darkMode ? '#A8A29E' : '#64748B'}>&#8226; 1 left (left point)</text>
<text x="380" y="187" fontSize="9" fill={darkMode ? '#A8A29E' : '#64748B'}>&#8226; 1 right (right point)</text>
<text x="380" y="202" fontSize="9" fill={darkMode ? '#A8A29E' : '#64748B'}>&#8226; 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>&#11088;</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&apos;s bounding box to the left edge of the text&apos;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">&#9654;</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">&#9654;</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>
);
}

View 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>
);
}

View 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&apos;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>&ldquo;We&apos;re part of Google&rdquo;</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>&ldquo;We see what Google doesn&apos;t show you&rdquo;</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>&ldquo;We look deeper.&rdquo;</strong> It&apos;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>&ldquo;Success&rdquo; (binary)</li>
<li>Google&apos;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>&ldquo;Growth over time&rdquo; (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">&ldquo;Things get better.&rdquo;</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>&ldquo;Platform&rdquo;</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>&ldquo;Helper&rdquo;</li>
</ul>
</div>
</div>
<p className="text-sm text-slate-600 dark:text-stone-400">
Our brand voice is &ldquo;helpful expert, not salesy.&rdquo; Nunito&apos;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>
);
}

View 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>
);
}

View 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>
);
}

View 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>&#10003;</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>&#10005;</span> Don&apos;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">&#10003; Good</div>
<div className="text-sm text-green-700 dark:text-green-300 mb-2">&quot;Your June review summary is ready&quot;</div>
<div className="text-sm text-green-700 dark:text-green-300">&quot;3 things customers mentioned this week&quot;</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">&#10005; Bad</div>
<div className="text-sm text-red-700 dark:text-red-300 mb-2 line-through">&quot;URGENT: Don&apos;t miss your analytics!&quot;</div>
<div className="text-sm text-red-700 dark:text-red-300 line-through">&quot;You won&apos;t BELIEVE what customers said&quot;</div>
</div>
</div>
</Section>
</div>
);
}

View 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';

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View 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>
);
}

View File

@@ -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>
);
}

View 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 &ldquo;Copy HTML&rdquo; to copy the signature</li>
<li>Paste into your email client&apos;s signature settings</li>
</ol>
</div>
</div>
</div>
</div>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View 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';

View 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>
);
}

View 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>
);
}

View 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>
);
}

View File

@@ -0,0 +1,4 @@
export { DownloadDropdown } from './DownloadDropdown';
export type { DownloadOption, DownloadDivider, DownloadOptionOrDivider } from './DownloadDropdown';
export { Section } from './Section';
export { CopyButton } from './CopyButton';

View 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;
}

View 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;
}

View 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'];