feat(CC-090): add Pitch Builder page (7-section pitch form, startup/grant templates, TXT export)

This commit is contained in:
admin-valentin 2026-08-02 18:00:43 +00:00
parent 724460b3fe
commit 17907f0052

View file

@ -0,0 +1,134 @@
'use client';
import { useState } from 'react';
import { useSession } from '../../../components/session-provider';
interface PitchSection { id: string; label: string; placeholder: string; hint: string; }
const SECTIONS: PitchSection[] = [
{ id: 'problem', label: '1. Problema', placeholder: 'Ce problemă reală rezolvi?', hint: 'Specific, cuantificabil, cu cine se confruntă' },
{ id: 'solution', label: '2. Soluția', placeholder: 'Cum o rezolvi?', hint: 'Propunerea ta de valoare în 2 propoziții' },
{ id: 'market', label: '3. Piața', placeholder: 'Cât de mare e piața? TAM / SAM / SOM', hint: 'Date concrete, surse credibile' },
{ id: 'traction', label: '4. Tracțiune', placeholder: 'Ce ai demonstrat deja?', hint: 'Clienți, venituri, utilizatori, partnership-uri' },
{ id: 'model', label: '5. Model de business',placeholder: 'Cum faci bani?', hint: 'Prețuri, canale, unit economics' },
{ id: 'team', label: '6. Echipa', placeholder: 'De ce voi sunteți cei potriviți?', hint: 'Experiență relevantă, unfair advantage' },
{ id: 'ask', label: '7. Cererea (Ask)', placeholder: 'Ce cauți? Investiție, parteneriat, pilot?', hint: 'Sumă / termen / use of funds' },
];
const TEMPLATES: Record<string, Partial<Record<string, string>>> = {
startup: {
problem: '',
solution: '',
market: 'TAM: €_M | SAM: €_M | SOM: €_M (an 1)',
traction: '',
model: 'SaaS - €_ / lună per utilizator. ARR țintă: €_M',
ask: 'Runda seed: €_ pentru 18 luni runway',
},
grant: {
problem: '',
solution: '',
market: 'Beneficiari direcți: _ persoane. Impact regional / național.',
traction: '',
model: 'Grant nerambursabil. Cofinanțare: _%. Durată proiect: _ luni.',
ask: 'Suma solicitată: €_ din programul _',
},
};
export default function PitchPage() {
const { activeTenant } = useSession();
const [sections, setSections] = useState<Record<string, string>>({});
const [pitchName, setPitchName] = useState('');
const [template, setTemplate] = useState('blank');
const [exported, setExported] = useState(false);
function applyTemplate(t: string) {
setTemplate(t);
if (t !== 'blank') setSections((p) => ({ ...p, ...TEMPLATES[t] }));
}
function wordCount(s: string) { return s.trim().split(/\s+/).filter(Boolean).length; }
const totalWords = Object.values(sections).reduce((sum, s) => sum + wordCount(s), 0);
const filledSections = SECTIONS.filter((s) => (sections[s.id] ?? '').trim().length > 0).length;
function exportPitch() {
const lines = [
`PITCH: ${pitchName || 'Fără titlu'}`,
`Generat: ${new Date().toLocaleDateString('ro-RO')}`,
'',
...SECTIONS.map((s) => [
`## ${s.label}`,
sections[s.id]?.trim() || '(necompletat)',
'',
]).flat(),
];
const blob = new Blob([lines.join('\n')], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `pitch-${(pitchName || 'ceo-os').replace(/\s+/g, '-').toLowerCase()}.txt`;
a.click();
URL.revokeObjectURL(url);
setExported(true);
setTimeout(() => setExported(false), 3000);
}
return (
<div className="max-w-3xl space-y-6 p-6">
<div className="flex items-start justify-between flex-wrap gap-3">
<div>
<h1 className="font-display text-2xl font-semibold text-ink">Pitch Builder</h1>
<p className="text-sm text-ink-faint mt-1">{filledSections}/{SECTIONS.length} secțiuni · {totalWords} cuvinte</p>
</div>
<button onClick={exportPitch}
className={`rounded-lg px-4 py-2 text-sm font-medium transition-colors ${exported ? 'bg-signal-ok text-white' : 'bg-primary text-white hover:bg-primary/90'}`}>
{exported ? '✓ Exportat!' : '⬇ Export TXT'}
</button>
</div>
{/* Meta */}
<div className="card p-4 space-y-3">
<input placeholder="Numele pitch-ului (ex: Pitch FiscalAI pentru Angel Round)" value={pitchName}
onChange={(e) => setPitchName(e.target.value)}
className="w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
<div className="flex gap-2 flex-wrap">
<p className="text-xs text-ink-faint self-center">Template:</p>
{(['blank', 'startup', 'grant'] as const).map((t) => (
<button key={t} onClick={() => applyTemplate(t)}
className={`rounded-full px-3 py-1 text-xs border capitalize ${template === t ? 'bg-primary text-white border-primary' : 'border-border text-ink-faint hover:border-primary/40'}`}>
{t === 'blank' ? 'Blank' : t === 'startup' ? '🚀 Startup' : '📋 Grant'}
</button>
))}
</div>
</div>
{/* Progress bar */}
<div className="h-1.5 rounded-full bg-muted overflow-hidden">
<div className="h-full rounded-full bg-primary transition-all" style={{ width: `${(filledSections / SECTIONS.length) * 100}%` }} />
</div>
{/* Sections */}
<div className="space-y-4">
{SECTIONS.map((s) => {
const val = sections[s.id] ?? '';
const wc = wordCount(val);
return (
<div key={s.id} className={`card p-4 space-y-2 transition-colors ${val.trim() ? 'border-primary/20' : ''}`}>
<div className="flex items-center justify-between">
<label className="text-sm font-semibold text-ink">{s.label}</label>
<span className="text-[10px] text-ink-faint">{wc} cuvinte</span>
</div>
<p className="text-[10px] text-ink-faint">{s.hint}</p>
<textarea placeholder={s.placeholder} value={val}
onChange={(e) => setSections((p) => ({ ...p, [s.id]: e.target.value }))}
rows={3} className="w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink resize-none focus:outline-none focus:ring-2 focus:ring-ring" />
</div>
);
})}
</div>
<p className="text-[10px] text-ink-faint text-center">
Pitch-ul este local nu se trimite nicăieri automat. Exportă și folosește cum dorești.
</p>
</div>
);
}