GrantForge Bot
Deploy to Hugging Face
3b7f713
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useUser } from '@clerk/clerk-react';
import toast from 'react-hot-toast';
import { analytics } from '../utils/analytics';
import './Pricing.css';
// ─── Typy ────────────────────────────────────────────────────────────────────
interface PlanFeature {
text: string;
available: boolean;
highlight?: boolean;
}
interface Plan {
id: 'free' | 'pro' | 'enterprise';
name: string;
price: string;
priceSuffix: string;
description: string;
badge?: string;
cta: string;
ctaVariant: 'secondary' | 'primary' | 'enterprise';
features: PlanFeature[];
stripePriceId?: string;
}
// ─── Dane planów ─────────────────────────────────────────────────────────────
const PLANS: Plan[] = [
{
id: 'free',
name: 'Free',
price: '0 zł',
priceSuffix: '/miesiąc',
description: 'Idealne do testowania i małych projektów',
cta: 'Zacznij za darmo',
ctaVariant: 'secondary',
features: [
{ text: '3 aktywne projekty', available: true },
{ text: '3 dokumenty PDF / projekt', available: true },
{ text: 'Generator wniosków AI (Gemini)', available: true },
{ text: 'Audytor wniosku (podstawowy)', available: true },
{ text: 'Wyszukiwarka naborów GUS', available: true },
{ text: 'Export DOCX', available: true },
{ text: 'Analiza RAG (Pinecone)', available: false },
{ text: 'Priorytetowe wsparcie', available: false },
{ text: 'Custom branding', available: false },
{ text: 'API access', available: false },
],
},
{
id: 'pro',
name: 'Pro',
price: '299 zł',
priceSuffix: '/miesiąc',
description: 'Dla firm aktywnie pozyskujących dofinansowanie',
badge: 'Najpopularniejszy',
cta: 'Wybierz Pro',
ctaVariant: 'primary',
stripePriceId: import.meta.env.VITE_STRIPE_PRICE_ID_PRO,
features: [
{ text: 'Nieograniczone projekty', available: true, highlight: true },
{ text: '50 dokumentów PDF / projekt', available: true, highlight: true },
{ text: 'Generator wniosków AI (Gemini Pro)', available: true },
{ text: 'Audytor wniosku (zaawansowany + DNSH)', available: true },
{ text: 'Wyszukiwarka naborów GUS', available: true },
{ text: 'Export DOCX + PDF', available: true },
{ text: 'Analiza RAG (Pinecone wektoryzacja)', available: true, highlight: true },
{ text: 'Priorytetowe wsparcie (48h)', available: true },
{ text: 'Custom branding', available: false },
{ text: 'API access', available: false },
],
},
{
id: 'enterprise',
name: 'Enterprise',
price: 'Wycena',
priceSuffix: 'indywidualna',
description: 'Dla biur rachunkowych i agencji doradczych',
cta: 'Skontaktuj się',
ctaVariant: 'enterprise',
features: [
{ text: 'Nieograniczone projekty & dokumenty', available: true, highlight: true },
{ text: 'Dedykowane środowisko Pinecone', available: true, highlight: true },
{ text: 'Modele Bielik (polskie prawo)', available: true, highlight: true },
{ text: 'Multi-tenant — wiele spółek', available: true, highlight: true },
{ text: 'Audytor CrewAI (pełny pipeline)', available: true },
{ text: 'Custom domain + white-label', available: true },
{ text: 'SLA 99.9% uptime', available: true },
{ text: 'API access + webhooks', available: true },
{ text: 'Onboarding + szkolenie (4h)', available: true },
{ text: 'Dedykowany opiekun klienta', available: true },
],
},
];
// ─── FAQ ─────────────────────────────────────────────────────────────────────
const FAQ_ITEMS = [
{
q: 'Czy mogę zmienić plan w dowolnym momencie?',
a: 'Tak. Upgrade następuje natychmiastowo, a opłata jest proporcjonalna do pozostałego okresu w danym miesiącu. Downgrade skutkuje zmianą od następnego okresu rozliczeniowego.',
},
{
q: 'Jak działa analiza RAG (Pinecone)?',
a: 'Twoje dokumenty PDF są indeksowane wektorowo w Pinecone. Przy generowaniu wniosków AI automatycznie wyszukuje i cytuje odpowiednie fragmenty (regulaminy, wytyczne MFiPR), co eliminuje halucynacje i zwiększa wierność treści.',
},
{
q: 'Co się stanie z moimi danymi po anulowaniu subskrypcji?',
a: 'Twoje projekty i dokumenty pozostają dostępne przez 30 dni po anulowaniu. Możesz je wyeksportować w tym czasie. Po 30 dniach dane są trwale usuwane.',
},
{
q: 'Czy plan Enterprise obejmuje integrację z GUS REGON?',
a: 'Tak. Enterprise zawiera pełną integrację z GUS REGON API do automatycznego pobierania danych firmy, a także integrację z BDO i e-Zamówienia.',
},
{
q: 'Czy mogę przetestować plan Pro za darmo?',
a: 'Oferujemy 14-dniowy trial Pro bez karty kredytowej. Skontaktuj się z nami na kontakt@grantforge.pl lub kliknij Wybierz Pro.',
},
];
// ─── Komponenty ──────────────────────────────────────────────────────────────
const CheckIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="8" fill="rgba(16,185,129,0.15)" />
<path d="M5 8l2 2 4-4" stroke="#10B981" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
const XIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="8" fill="rgba(255,255,255,0.04)" />
<path d="M5.5 5.5l5 5m0-5l-5 5" stroke="rgba(255,255,255,0.2)" strokeWidth="1.5" strokeLinecap="round" />
</svg>
);
const PlanCard: React.FC<{
plan: Plan;
isPopular: boolean;
onSelect: (plan: Plan) => void;
loading: boolean;
}> = ({ plan, isPopular, onSelect, loading }) => {
return (
<div className={`pricing-card ${isPopular ? 'pricing-card--popular' : ''} pricing-card--${plan.id}`}>
{plan.badge && (
<div className="pricing-badge">{plan.badge}</div>
)}
<div className="pricing-card-header">
<div className="pricing-plan-name">{plan.name}</div>
<div className="pricing-price-wrap">
<span className="pricing-price">{plan.price}</span>
<span className="pricing-price-suffix">{plan.priceSuffix}</span>
</div>
<p className="pricing-description">{plan.description}</p>
</div>
<button
className={`pricing-cta pricing-cta--${plan.ctaVariant}`}
onClick={() => onSelect(plan)}
disabled={loading}
id={`pricing-cta-${plan.id}`}
>
{loading ? (
<span className="pricing-spinner" />
) : plan.ctaVariant === 'enterprise' ? (
<>📩 {plan.cta}</>
) : (
plan.cta
)}
</button>
<div className="pricing-features">
{plan.features.map((f, i) => (
<div key={i} className={`pricing-feature ${!f.available ? 'pricing-feature--disabled' : ''} ${f.highlight ? 'pricing-feature--highlight' : ''}`}>
{f.available ? <CheckIcon /> : <XIcon />}
<span>{f.text}</span>
</div>
))}
</div>
</div>
);
};
const FAQItem: React.FC<{ q: string; a: string; index: number }> = ({ q, a, index }) => {
const [open, setOpen] = useState(false);
return (
<div className={`faq-item ${open ? 'faq-item--open' : ''}`} id={`faq-${index}`}>
<button className="faq-question" onClick={() => setOpen(!open)}>
<span>{q}</span>
<svg className="faq-chevron" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M4 6l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
{open && <div className="faq-answer">{a}</div>}
</div>
);
};
// ─── Strona główna ────────────────────────────────────────────────────────────
const Pricing: React.FC = () => {
const navigate = useNavigate();
const { user } = useUser();
const [loading, setLoading] = useState<string | null>(null);
const [billingAnnual, setBillingAnnual] = useState(false);
const [visible, setVisible] = useState(false);
useEffect(() => {
const t = setTimeout(() => setVisible(true), 50);
return () => clearTimeout(t);
}, []);
const handleSelectPlan = async (plan: Plan) => {
if (plan.id === 'free') {
navigate('/projects');
return;
}
if (plan.id === 'enterprise') {
window.location.href = 'mailto:kontakt@grantforge.pl?subject=Enterprise%20-%20Zapytanie%20ofertowe';
return;
}
// Pro — Stripe checkout
if (!user) {
navigate('/sign-in');
return;
}
setLoading(plan.id);
try {
analytics.checkoutStarted(plan.id);
const token = await (window as any).Clerk?.session?.getToken();
const res = await fetch('/api/subscription/checkout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({ plan: plan.id }),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.detail || 'Błąd serwera');
}
const data = await res.json();
if (data.checkout_url) {
window.location.href = data.checkout_url;
}
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : 'Błąd płatności';
toast.error(`Nie udało się otworzyć płatności: ${msg}`);
} finally {
setLoading(null);
}
};
const annualDiscount = 0.2; // 20% rabatu rocznego
const getPrice = (plan: Plan) => {
if (plan.id !== 'pro' || !billingAnnual) return plan.price;
return `${Math.round(299 * (1 - annualDiscount))} zł`;
};
return (
<div className={`pricing-page ${visible ? 'pricing-page--visible' : ''}`}>
{/* Hero */}
<div className="pricing-hero">
<div className="pricing-hero-badge">💡 Transparentne ceny bez ukrytych kosztów</div>
<h1 className="pricing-title">
Wybierz plan<br />
<span className="pricing-title-gradient">dopasowany do skali</span>
</h1>
<p className="pricing-subtitle">
Od pierwszego wniosku testowego do kompleksowej obsługi portfela projektów.<br />
Każdy plan zawiera pełny generator AI i audytor wniosków.
</p>
{/* Billing toggle */}
<div className="pricing-billing-toggle">
<span className={!billingAnnual ? 'active' : ''}>Miesięcznie</span>
<button
className={`pricing-toggle-btn ${billingAnnual ? 'pricing-toggle-btn--on' : ''}`}
onClick={() => setBillingAnnual(!billingAnnual)}
id="pricing-billing-toggle"
aria-label="Przełącz billing roczny"
>
<span className="pricing-toggle-knob" />
</button>
<span className={billingAnnual ? 'active' : ''}>
Rocznie <span className="pricing-discount-badge">-20%</span>
</span>
</div>
</div>
{/* Karty planów */}
<div className="pricing-cards">
{PLANS.map((plan) => (
<PlanCard
key={plan.id}
plan={{ ...plan, price: getPrice(plan) }}
isPopular={plan.id === 'pro'}
onSelect={handleSelectPlan}
loading={loading === plan.id}
/>
))}
</div>
{/* Trust signals */}
<div className="pricing-trust">
<div className="pricing-trust-item">🔒 Płatności Stripe — PCI DSS Level 1</div>
<div className="pricing-trust-item">🇵🇱 Dane w Polsce (EU-West3)</div>
<div className="pricing-trust-item">📄 Faktura VAT</div>
<div className="pricing-trust-item">↩️ Zwrot w 14 dni</div>
</div>
{/* FAQ */}
<div className="pricing-faq">
<h2 className="pricing-faq-title">Najczęstsze pytania</h2>
<div className="pricing-faq-list">
{FAQ_ITEMS.map((item, i) => (
<FAQItem key={i} index={i} q={item.q} a={item.a} />
))}
</div>
</div>
{/* CTA Bottom */}
<div className="pricing-bottom-cta">
<h2>Masz pytania? Napisz do nas.</h2>
<p>Pomożemy dobrać plan i skonfigurować GrantForge AI dla Twojej organizacji.</p>
<a
href="mailto:kontakt@grantforge.pl"
className="btn btn-primary"
id="pricing-contact-cta"
>
📩 kontakt@grantforge.pl
</a>
</div>
</div>
);
};
export default Pricing;