GrantForge Bot
Deploy sha-565ad85979610064f6d1c18ab3b6404357d61073 — source build (no GHCR)
ce8f04a
Raw
History Blame Contribute Delete
7.23 kB
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;