feat(CC-090): add Pitch Builder page (7-section pitch form, startup/grant templates, TXT export)
This commit is contained in:
parent
724460b3fe
commit
17907f0052
1 changed files with 134 additions and 0 deletions
134
src/app/dashboard/pitch/page.tsx
Normal file
134
src/app/dashboard/pitch/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue