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