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