← back to Norma
components/CypressAwardsTab.tsx
461 lines
'use client';
import { useState, useCallback } from 'react';
import { motion } from 'framer-motion';
import {
Trophy,
Star,
ExternalLink,
Newspaper,
Users,
Globe,
Calendar,
Send,
ChevronDown,
ChevronUp,
Sparkles,
Award,
Heart,
} from 'lucide-react';
import { getOutletFaviconUrl } from '@/lib/outlet-logo';
/* ─── Types ──────────────────────────────────────────────────────────────── */
interface AwardCategory {
id: string;
name: string;
description: string;
icon: React.ElementType;
color: string;
criteria: string[];
pastWinners?: string[];
}
interface NominationIdea {
name: string;
outlet: string;
reason: string;
linkedinSearch: string;
}
/* ─── Award Categories ───────────────────────────────────────────────────── */
const CATEGORIES: AwardCategory[] = [
{
id: 'investigative',
name: 'Investigative Reporting',
description: 'Excellence in investigative journalism exposing systemic policy failures, misconduct, or institutional practices harming communities.',
icon: Newspaper,
color: '#ef4444',
criteria: [
'In-depth reporting on systemic policy issues',
'Exposed systemic issues affecting communities',
'Led to policy changes or public awareness shifts',
'Published in recognized media outlets',
],
pastWinners: ['Stacy Cowley (NY Times)', 'Cory Turner (NPR)', 'Annie Nova (CNBC)'],
},
{
id: 'advocacy',
name: 'Advocacy Journalism',
description: 'Journalists who amplify community voices and drive the policy reform conversation forward.',
icon: Users,
color: '#6366f1',
criteria: [
'Consistent coverage of advocacy and policy issues',
'Centering affected community stories and experiences',
'Challenging misleading narratives about key policy issues',
'Building public understanding of systemic challenges',
],
pastWinners: ['Elissa Nadworny (NPR)', 'Jillian Berman (MarketWatch)'],
},
{
id: 'digital',
name: 'Digital & Social Impact',
description: 'Content creators and digital journalists who reach new audiences with advocacy stories.',
icon: Globe,
color: '#a855f7',
criteria: [
'Viral or high-engagement content about policy reform',
'Creative use of social media to educate',
'Reaching underserved or younger audiences',
'Data visualization or multimedia storytelling',
],
},
{
id: 'emerging',
name: 'Emerging Voice',
description: 'Up-and-coming journalists or content creators making their mark on advocacy coverage.',
icon: Star,
color: '#f59e0b',
criteria: [
'New voice in advocacy journalism (under 3 years covering the beat)',
'Fresh perspective or innovative approach',
'Demonstrated commitment to ongoing coverage',
'Published at least 3 significant pieces on relevant policy topics',
],
},
];
/* ─── Nomination Ideas ───────────────────────────────────────────────────── */
const NOMINATION_IDEAS: NominationIdea[] = [
{ name: 'Stacy Cowley', outlet: 'The New York Times', reason: 'Extensive policy accountability and reform reporting', linkedinSearch: 'https://www.linkedin.com/search/results/people/?keywords=Stacy%20Cowley%20New%20York%20Times' },
{ name: 'Cory Turner', outlet: 'NPR', reason: 'Authoritative education beat coverage including policy reform programs', linkedinSearch: 'https://www.linkedin.com/search/results/people/?keywords=Cory%20Turner%20NPR%20education' },
{ name: 'Annie Nova', outlet: 'CNBC', reason: 'Comprehensive personal finance coverage of economic relief programs', linkedinSearch: 'https://www.linkedin.com/search/results/people/?keywords=Annie%20Nova%20CNBC' },
{ name: 'Elissa Nadworny', outlet: 'NPR', reason: 'Deep reporting on higher education costs and community experiences', linkedinSearch: 'https://www.linkedin.com/search/results/people/?keywords=Elissa%20Nadworny%20NPR' },
{ name: 'Jillian Berman', outlet: 'MarketWatch', reason: 'Longstanding policy beat reporter covering economic impact on communities', linkedinSearch: 'https://www.linkedin.com/search/results/people/?keywords=Jillian%20Berman%20MarketWatch' },
{ name: 'Adam Looney', outlet: 'Brookings Institution', reason: 'Data-driven policy analysis on education economics and reform', linkedinSearch: 'https://www.linkedin.com/search/results/people/?keywords=Adam%20Looney%20Brookings' },
{ name: 'Michael Stratford', outlet: 'Politico', reason: 'Inside-the-Beltway coverage of education policy developments', linkedinSearch: 'https://www.linkedin.com/search/results/people/?keywords=Michael%20Stratford%20Politico' },
{ name: 'Danielle Douglas-Gabriel', outlet: 'Washington Post', reason: 'Education and economics reporting covering policy reform trends', linkedinSearch: 'https://www.linkedin.com/search/results/people/?keywords=Danielle%20Douglas-Gabriel%20Washington%20Post' },
];
/* ─── Award Category Card ────────────────────────────────────────────────── */
function CategoryCard({ category, index, onJournalistClick }: { category: AwardCategory; index: number; onJournalistClick?: (name: string) => void }) {
const [expanded, setExpanded] = useState(false);
const { icon: Icon, name, description, color, criteria, pastWinners } = category;
return (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2, delay: index * 0.06 }}
className="card"
style={{ cursor: 'pointer', borderColor: `${color}30` }}
onClick={() => setExpanded((v) => !v)}
>
<div className="flex items-start gap-3">
<div
style={{
width: 40, height: 40, borderRadius: 12,
background: `linear-gradient(135deg, ${color}20, ${color}10)`,
border: `1px solid ${color}30`,
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
}}
>
<Icon size={20} style={{ color }} />
</div>
<div className="flex-1 min-w-0">
<h3 className="font-bold text-sm" style={{ color: 'var(--color-text)' }}>{name}</h3>
<p className="text-xs mt-0.5 leading-relaxed" style={{ color: 'var(--color-text-secondary)' }}>{description}</p>
{expanded && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="mt-3">
<p className="text-xs font-semibold mb-2" style={{ color: 'var(--color-text-muted)' }}>Selection Criteria:</p>
<ul className="flex flex-col gap-1 mb-3">
{criteria.map((c, i) => (
<li key={i} className="flex items-start gap-2 text-xs" style={{ color: 'var(--color-text-secondary)' }}>
<Star size={10} style={{ color, marginTop: 3, flexShrink: 0 }} />
{c}
</li>
))}
</ul>
{pastWinners && pastWinners.length > 0 && (
<div>
<p className="text-xs font-semibold mb-1" style={{ color: 'var(--color-text-muted)' }}>Suggested Nominees:</p>
<div className="flex flex-wrap gap-1">
{pastWinners.map((w) => (
<button
key={w}
className="badge"
style={{ color, backgroundColor: `${color}12`, border: `1px solid ${color}20`, fontSize: '0.65rem', cursor: onJournalistClick ? 'pointer' : 'default', textDecoration: onJournalistClick ? 'underline' : 'none' }}
onClick={(e) => {
e.stopPropagation();
if (onJournalistClick) {
const nameOnly = w.replace(/\s*\(.*\)\s*$/, '');
onJournalistClick(nameOnly);
}
}}
>
{w}
</button>
))}
</div>
</div>
)}
</motion.div>
)}
</div>
<div style={{ flexShrink: 0 }}>
{expanded ? <ChevronUp size={14} style={{ color: 'var(--color-text-muted)' }} /> : <ChevronDown size={14} style={{ color: 'var(--color-text-muted)' }} />}
</div>
</div>
</motion.div>
);
}
/* ─── Nominee Card ───────────────────────────────────────────────────────── */
function NomineeCard({ nominee, index, onJournalistClick }: { nominee: NominationIdea; index: number; onJournalistClick?: (name: string) => void }) {
const [showLetter, setShowLetter] = useState(false);
const draftLetter = `Dear ${nominee.name},
I'm writing on behalf of our organization to express our deep appreciation for your reporting on the issues we care about.
Your work at ${nominee.outlet} — particularly ${nominee.reason.toLowerCase()} — has been instrumental in bringing public attention to critical issues affecting communities across the country.
As a past Cypress Press Award recipient ourselves, we deeply value journalism that advances public understanding of these important topics.
We'd love to discuss how we might collaborate to amplify community stories and support your continued coverage of these critical issues.
Would you be open to a brief conversation?
Warm regards,
[Your Name]
[Your Title], [Your Organization]
[org-email]
[org-website]`;
return (
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.15, delay: index * 0.04 }}
className="card"
>
<div className="flex items-start gap-3">
<div
style={{
width: 36, height: 36, borderRadius: '50%',
background: 'linear-gradient(135deg, rgba(16,185,129,0.2), rgba(147,51,234,0.2))',
border: '1px solid rgba(16,185,129,0.3)',
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
fontSize: 14, fontWeight: 700, color: '#34d399',
}}
>
{nominee.name.charAt(0)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<button
className="font-semibold text-sm"
style={{ color: onJournalistClick ? 'var(--color-primary)' : 'var(--color-text)', cursor: onJournalistClick ? 'pointer' : 'default', textDecoration: onJournalistClick ? 'underline' : 'none', background: 'none', border: 'none', padding: 0 }}
onClick={(e) => {
e.stopPropagation();
if (onJournalistClick) onJournalistClick(nominee.name);
}}
>
{nominee.name}
</button>
<span className="text-xs font-medium" style={{ color: '#34d399' }}>
{nominee.outlet && (
<img
src={getOutletFaviconUrl(nominee.outlet, 24) || ''}
alt=""
width={14}
height={14}
style={{ borderRadius: 3, verticalAlign: 'middle', marginRight: 4, display: 'inline-block' }}
onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = 'none'; }}
/>
)}
{nominee.outlet}
</span>
</div>
<p className="text-xs mt-0.5" style={{ color: 'var(--color-text-secondary)' }}>{nominee.reason}</p>
<div className="flex items-center gap-2 mt-2">
<a
href={nominee.linkedinSearch}
target="_blank"
rel="noopener noreferrer"
className="btn btn-ghost btn-sm"
style={{ fontSize: '0.7rem', padding: '3px 8px' }}
onClick={(e) => e.stopPropagation()}
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="#0a66c2"><path d="M20.5 2h-17A1.5 1.5 0 002 3.5v17A1.5 1.5 0 003.5 22h17a1.5 1.5 0 001.5-1.5v-17A1.5 1.5 0 0020.5 2zM8 19H5v-9h3zM6.5 8.25A1.75 1.75 0 118.3 6.5a1.78 1.78 0 01-1.8 1.75zM19 19h-3v-4.74c0-1.42-.6-1.93-1.38-1.93A1.74 1.74 0 0013 14.19a.66.66 0 000 .14V19h-3v-9h2.9v1.3a3.11 3.11 0 012.7-1.4c1.55 0 3.36.86 3.36 3.66z"/></svg>
LinkedIn
</a>
<button
type="button"
className="btn btn-ghost btn-sm"
style={{ fontSize: '0.7rem', padding: '3px 8px' }}
onClick={() => setShowLetter((v) => !v)}
>
<Send size={10} />
{showLetter ? 'Hide Letter' : 'Draft Letter'}
</button>
</div>
{showLetter && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="mt-3">
<pre
className="text-xs leading-relaxed p-3 rounded-lg overflow-x-auto"
style={{
backgroundColor: 'var(--color-surface-el)',
border: '1px solid var(--color-border)',
color: 'var(--color-text-secondary)',
whiteSpace: 'pre-wrap',
fontFamily: 'inherit',
}}
>
{draftLetter}
</pre>
<button
type="button"
className="btn btn-secondary btn-sm mt-2"
style={{ fontSize: '0.7rem' }}
onClick={() => navigator.clipboard.writeText(draftLetter)}
>
Copy to Clipboard
</button>
</motion.div>
)}
</div>
</div>
</motion.div>
);
}
/* ═══════════════════════════════════════════════════════════════════════════
CYPRESS AWARDS TAB
═══════════════════════════════════════════════════════════════════════════ */
export default function CypressAwardsTab({ onNavigateToJournalist }: { onNavigateToJournalist?: (name: string) => void } = {}) {
return (
<div className="p-4 sm:p-6 max-w-4xl mx-auto">
{/* ── Hero Header ──────────────────────────────────────────────────── */}
<div
className="rounded-xl p-6 mb-6"
style={{
background: 'linear-gradient(135deg, rgba(234,179,8,0.12), rgba(245,158,11,0.08), rgba(16,185,129,0.08))',
border: '1px solid rgba(234,179,8,0.25)',
}}
>
<div className="flex items-center gap-3 mb-3">
<div
style={{
width: 48, height: 48, borderRadius: 14,
background: 'linear-gradient(135deg, #fbbf24, #f59e0b)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
boxShadow: '0 4px 12px rgba(245,158,11,0.3)',
}}
>
<Trophy size={24} style={{ color: '#fff' }} />
</div>
<div>
<h1 className="text-xl font-bold" style={{ color: 'var(--color-text)' }}>
Cypress Press Awards
</h1>
<p className="text-sm" style={{ color: 'var(--color-text-secondary)' }}>
Tracking journalists who cover the issues we care about
</p>
</div>
</div>
<p className="text-xs leading-relaxed" style={{ color: 'var(--color-text-muted)' }}>
The Cypress Press Awards recognize exceptional journalism that advances public understanding of critical
policy issues. As a past Cypress Award recipient, we use this space to track and celebrate
reporters, columnists, and content creators who give voice to communities affected by systemic challenges.
</p>
<div className="flex items-center gap-4 mt-4 flex-wrap">
<div className="flex items-center gap-1.5">
<Calendar size={13} style={{ color: '#fbbf24' }} />
<span className="text-xs font-medium" style={{ color: 'var(--color-text-secondary)' }}>Annual Award Cycle</span>
</div>
<div className="flex items-center gap-1.5">
<Award size={13} style={{ color: '#fbbf24' }} />
<span className="text-xs font-medium" style={{ color: 'var(--color-text-secondary)' }}>4 Award Categories</span>
</div>
<div className="flex items-center gap-1.5">
<Heart size={13} style={{ color: '#fbbf24' }} />
<span className="text-xs font-medium" style={{ color: 'var(--color-text-secondary)' }}>Building Press Relationships</span>
</div>
</div>
</div>
{/* ── Why This Matters (for Your Organization) ──────────────────────────────────── */}
<div
className="rounded-xl p-4 mb-6"
style={{
backgroundColor: 'rgba(16,185,129,0.06)',
border: '1px solid rgba(16,185,129,0.15)',
}}
>
<div className="flex items-center gap-2 mb-2">
<Sparkles size={14} style={{ color: '#34d399' }} />
<h2 className="text-sm font-semibold" style={{ color: 'var(--color-text)' }}>Why This Matters for Your Organization</h2>
</div>
<div className="grid gap-3" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))' }}>
{[
{ title: 'Press Relationships', desc: 'Build direct connections with journalists covering your beat' },
{ title: 'Earned Media', desc: 'Award announcements generate free press coverage for Your Organization' },
{ title: 'Source Authority', desc: 'Become the go-to source when reporters need policy expertise' },
{ title: 'Donor Appeal', desc: 'Awards program shows organizational sophistication to funders' },
].map((item) => (
<div key={item.title} className="flex items-start gap-2">
<Star size={12} style={{ color: '#fbbf24', marginTop: 2, flexShrink: 0 }} />
<div>
<p className="text-xs font-semibold" style={{ color: 'var(--color-text)' }}>{item.title}</p>
<p className="text-xs" style={{ color: 'var(--color-text-muted)' }}>{item.desc}</p>
</div>
</div>
))}
</div>
</div>
{/* ── Award Categories ─────────────────────────────────────────────── */}
<section className="mb-8">
<h2 className="text-sm font-semibold mb-3 flex items-center gap-2" style={{ color: 'var(--color-text)' }}>
<Award size={16} style={{ color: '#fbbf24' }} />
Award Categories
</h2>
<div className="flex flex-col gap-3">
{CATEGORIES.map((cat, i) => (
<CategoryCard key={cat.id} category={cat} index={i} onJournalistClick={onNavigateToJournalist} />
))}
</div>
</section>
{/* ── Journalist Nominees & Outreach ────────────────────────────────── */}
<section className="mb-8">
<div className="flex items-center gap-2 mb-3">
<Users size={16} style={{ color: 'var(--color-text-muted)' }} />
<h2 className="text-sm font-semibold" style={{ color: 'var(--color-text)' }}>
Suggested Nominees & Outreach
</h2>
<span className="badge" style={{ color: 'var(--color-text-muted)', backgroundColor: 'var(--color-surface-el)', border: '1px solid var(--color-border)' }}>
{NOMINATION_IDEAS.length}
</span>
</div>
<p className="text-xs mb-4" style={{ color: 'var(--color-text-muted)' }}>
Top journalists covering policy reform. Click "Draft Letter" to see a personalized outreach message you can send.
</p>
<div className="flex flex-col gap-2">
{NOMINATION_IDEAS.map((nominee, i) => (
<NomineeCard key={nominee.name} nominee={nominee} index={i} onJournalistClick={onNavigateToJournalist} />
))}
</div>
</section>
{/* ── Launch Checklist ──────────────────────────────────────────────── */}
<section>
<div
className="rounded-xl p-4"
style={{
backgroundColor: 'var(--color-surface)',
border: '1px dashed rgba(234,179,8,0.3)',
}}
>
<h3 className="text-sm font-semibold mb-3 flex items-center gap-2" style={{ color: 'var(--color-text)' }}>
<Sparkles size={14} style={{ color: '#fbbf24' }} />
Launch Checklist
</h3>
<div className="flex flex-col gap-2">
{[
'Create public awards page on your-org-website.org',
'Design award badge/logo (Cypress tree + press motif)',
'Draft press release announcing the awards program',
'Set up nomination form (Google Forms or Typeform)',
'Recruit 3-5 advisory board members (academics, former journalists)',
'Send personalized outreach to nominees (use draft letters above)',
'Plan announcement timing around a key advocacy awareness day',
'Prepare social media campaign for your organization',
].map((item, i) => (
<label key={i} className="flex items-start gap-2 text-xs cursor-pointer" style={{ color: 'var(--color-text-secondary)' }}>
<input type="checkbox" className="mt-0.5" style={{ accentColor: '#fbbf24' }} />
{item}
</label>
))}
</div>
</div>
</section>
</div>
);
}