Spaces:
Running
Running
| 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; | |