GrantForge Bot
Deploy to Hugging Face
afd56bc
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;