Spaces:
Running
Running
File size: 7,224 Bytes
afd56bc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | 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;
|