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 { getSubscriptionStatus, getProject } from '../../api/client'; | |
| 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 location = useLocation(); | |
| const projectId = location.pathname.split('/projects/')[1]?.split('/')[0]; | |
| const { data: sub } = useQuery({ | |
| queryKey: ['subscription'], | |
| queryFn: getSubscriptionStatus, | |
| refetchInterval: 60000, | |
| retry: false | |
| }); | |
| useQuery({ | |
| queryKey: ['project', projectId], | |
| queryFn: () => getProject(projectId as string), | |
| enabled: !!projectId, | |
| refetchInterval: 10000 | |
| }); | |
| // Uzywamy bezpiecznego dostepu do limitow, lub fallback | |
| const activeSub = sub && typeof sub === 'object' && sub.limits ? sub : { | |
| tier: 'free', | |
| wizard_iterations_today: 18, | |
| tokens_used_month: 24500, | |
| limits: { max_wizard_iterations: 25, max_tokens_monthly: 50000 } | |
| }; | |
| const iterPercent = (activeSub.wizard_iterations_today / activeSub.limits.max_wizard_iterations) * 100; | |
| const showLimitBanner = 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 - activeSub.wizard_iterations_today)} 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; | |