Spaces:
Running
Running
| import React, { useState } from 'react'; | |
| import { Outlet } from 'react-router-dom'; | |
| import Sidebar from './Sidebar'; | |
| import PricingModal from './PricingModal'; | |
| import { motion, AnimatePresence } from 'framer-motion'; | |
| import { useQuery } from '@tanstack/react-query'; | |
| import { useAuth } from '@clerk/clerk-react'; | |
| import { getProject } from '../../api/client'; | |
| import { useMe } from '../../hooks/useMe'; | |
| import { AlertTriangle, ArrowRight, ShieldAlert, PanelLeftClose, PanelLeftOpen } from 'lucide-react'; | |
| import { useLocation } from 'react-router-dom'; | |
| import { OnboardingWelcomeModal } from './OnboardingWelcomeModal'; | |
| const Layout: React.FC = () => { | |
| const [showPricing, setShowPricing] = useState(false); | |
| const [, setWelcomeComplete] = useState(false); | |
| const [isSidebarOpen, setIsSidebarOpen] = useState(true); | |
| const { isLoaded, isSignedIn } = useAuth(); | |
| const location = useLocation(); | |
| const projectId = location.pathname.split('/projects/')[1]?.split('/')[0]; | |
| // Shared gated /api/me — no request until Clerk signed in | |
| const { data: sub } = useMe(); | |
| useQuery({ | |
| queryKey: ['project', projectId], | |
| queryFn: () => getProject(projectId as string), | |
| enabled: Boolean(projectId && isLoaded && isSignedIn), | |
| refetchInterval: 10000 | |
| }); | |
| // Only show limit banner from real API data — never fabricate usage stats | |
| const activeSub = sub && typeof sub === 'object' && sub.limits ? sub : null; | |
| const iterPercent = activeSub | |
| ? (activeSub.wizard_iterations_today / activeSub.limits.max_wizard_iterations) * 100 | |
| : 0; | |
| const showLimitBanner = !!activeSub && activeSub.tier !== 'business' && iterPercent > 80; | |
| const isCriticalLimit = iterPercent > 90; | |
| return ( | |
| <div className="dashboard-layout"> | |
| {/* Kolumna 1: Lewy Panel Nawigacyjny */} | |
| <aside | |
| className="dashboard-sidebar-left" | |
| style={{ | |
| width: isSidebarOpen ? '320px' : '0px', | |
| padding: 0, | |
| overflow: 'hidden', | |
| transition: 'width 0.3s cubic-bezier(0.4, 0, 0.2, 1)' | |
| }} | |
| > | |
| <div style={{ width: '320px', height: '100%', overflowY: 'auto' }}> | |
| <Sidebar /> | |
| </div> | |
| </aside> | |
| {/* Kolumna 2: Główna Treść + Outlet */} | |
| <main className="dashboard-main" style={{ display: 'flex', flexDirection: 'column', position: 'relative' }}> | |
| <button | |
| onClick={() => setIsSidebarOpen(!isSidebarOpen)} | |
| className="btn btn-secondary hover-bg" | |
| style={{ | |
| position: 'fixed', | |
| bottom: '1.5rem', | |
| left: isSidebarOpen ? '335px' : '1rem', | |
| zIndex: 9999, | |
| padding: '0.6rem', | |
| borderRadius: '50%', | |
| background: 'rgba(255,255,255,0.1)', | |
| backdropFilter: 'blur(10px)', | |
| transition: 'left 0.3s cubic-bezier(0.4, 0, 0.2, 1)', | |
| border: '1px solid rgba(255,255,255,0.1)', | |
| boxShadow: '0 4px 15px rgba(0,0,0,0.5)' | |
| }} | |
| title={isSidebarOpen ? "Ukryj główne menu" : "Pokaż główne menu"} | |
| > | |
| {isSidebarOpen ? <PanelLeftClose size={20} /> : <PanelLeftOpen size={20} />} | |
| </button> | |
| <AnimatePresence> | |
| {showLimitBanner && ( | |
| <motion.div | |
| initial={{ height: 0, opacity: 0 }} | |
| animate={ | |
| isCriticalLimit | |
| ? { | |
| height: 'auto', | |
| opacity: 1, | |
| boxShadow: ['0 4px 30px rgba(239,68,68,0.2)', '0 4px 50px rgba(239,68,68,0.5)', '0 4px 30px rgba(239,68,68,0.2)'] | |
| } | |
| : { height: 'auto', opacity: 1 } | |
| } | |
| transition={isCriticalLimit ? { boxShadow: { repeat: Infinity, duration: 2, ease: 'easeInOut' } } : {}} | |
| exit={{ height: 0, opacity: 0 }} | |
| style={{ | |
| background: isCriticalLimit ? 'linear-gradient(90deg, rgba(239, 68, 68, 0.25), rgba(220, 38, 38, 0.15))' : 'linear-gradient(90deg, rgba(245, 158, 11, 0.1), rgba(239, 68, 68, 0.1))', | |
| borderBottom: isCriticalLimit ? '2px solid rgba(239, 68, 68, 0.6)' : '1px solid rgba(245, 158, 11, 0.3)', | |
| overflow: 'hidden' | |
| }} | |
| > | |
| <div style={{ padding: '0.8rem 2rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '0.8rem', color: isCriticalLimit ? '#EF4444' : '#F59E0B', fontWeight: 600 }}> | |
| {isCriticalLimit ? <ShieldAlert size={20} /> : <AlertTriangle size={18} />} | |
| <span> | |
| {isCriticalLimit | |
| ? `KRYTYCZNY LIMIT: Pozostało tylko ${Math.max(0, (activeSub?.limits.max_wizard_iterations ?? 0) - (activeSub?.wizard_iterations_today ?? 0))} iteracji! Wykup wyższy plan by nie stracić dostępu do AI.` | |
| : `Uwaga: Wykorzystałeś ${iterPercent.toFixed(0)}% dziennych iteracji Kreatora. Twój plan zaraz zablokuje działanie AI.` | |
| } | |
| </span> | |
| </div> | |
| <motion.button | |
| animate={isCriticalLimit ? { scale: [1, 1.05, 1], boxShadow: ['0 0 0px transparent', '0 0 15px rgba(239,68,68,0.5)', '0 0 0px transparent'] } : {}} | |
| transition={isCriticalLimit ? { repeat: Infinity, duration: 1.5, ease: 'easeInOut' } : {}} | |
| whileHover={{ scale: 1.05 }} | |
| className="btn" | |
| style={{ | |
| background: isCriticalLimit ? 'rgba(239, 68, 68, 0.25)' : 'rgba(245, 158, 11, 0.2)', | |
| color: isCriticalLimit ? '#EF4444' : '#F59E0B', | |
| fontSize: '0.85rem', padding: '0.4rem 1rem', display: 'flex', alignItems: 'center', gap: '0.5rem', fontWeight: 800, | |
| border: isCriticalLimit ? '1px solid rgba(239, 68, 68, 0.6)' : '1px solid rgba(245, 158, 11, 0.4)' | |
| }} | |
| onClick={() => setShowPricing(true)} | |
| > | |
| {isCriticalLimit ? 'Upgrade Natychmiastowy' : 'Odblokuj Limity'} <ArrowRight size={14} /> | |
| </motion.button> | |
| </div> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| <motion.div | |
| initial={{ opacity: 0, y: 15 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ duration: 0.5 }} | |
| style={{ display: 'flex', flexDirection: 'column', flex: 1, overflowY: 'auto', overflowX: 'hidden' }} | |
| > | |
| <Outlet /> | |
| </motion.div> | |
| </main> | |
| {showPricing && <PricingModal onClose={() => setShowPricing(false)} />} | |
| <OnboardingWelcomeModal onComplete={() => setWelcomeComplete(true)} /> | |
| </div> | |
| ); | |
| }; | |
| export default Layout; | |