337 lines
16 KiB
TypeScript
337 lines
16 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { useAppContext } from '@/contexts/AppContext';
|
|
import { useAuth } from '@/contexts/AuthContext';
|
|
import { useIsMobile } from '@/hooks/use-mobile';
|
|
import { UserRole, ModuleId } from '@/lib/types';
|
|
import { MODULES } from '@/lib/appData';
|
|
|
|
import Sidebar from '@/components/frameworks/Sidebar';
|
|
import TopBar from '@/components/frameworks/TopBar';
|
|
import Dashboard from '@/components/frameworks/Dashboard';
|
|
import FrameModule from '@/components/frameworks/FrameModule';
|
|
import ClassroomSupport from '@/components/frameworks/ClassroomSupport';
|
|
import ClassroomTimer from '@/components/frameworks/ClassroomTimer';
|
|
import QBSSafety from '@/components/frameworks/QBSSafety';
|
|
import EmotionalIntelligence from '@/components/frameworks/EmotionalIntelligence';
|
|
import ZonesOfRegulation from '@/components/frameworks/ZonesOfRegulation';
|
|
import SignLanguage from '@/components/frameworks/SignLanguage';
|
|
import DirectorDashboard from '@/components/frameworks/DirectorDashboard';
|
|
import HandbookPolicy from '@/components/frameworks/HandbookPolicy';
|
|
import SignInModal from '@/components/frameworks/SignInModal';
|
|
import CommunityService from '@/components/frameworks/CommunityService';
|
|
import VocationalOpportunities from '@/components/frameworks/VocationalOpportunities';
|
|
import ESAFunding from '@/components/frameworks/ESAFunding';
|
|
import WalkThroughCheckIn from '@/components/frameworks/WalkThroughCheckIn';
|
|
import CampusAttendance from '@/components/frameworks/CampusAttendance';
|
|
|
|
|
|
import {
|
|
ParentCommModule,
|
|
InternalCommModule,
|
|
SafetyProtocolsModule,
|
|
} from '@/components/frameworks/MoreModules';
|
|
|
|
import {
|
|
Loader2, LogIn, CheckCircle2, Eye, ChevronDown
|
|
} from 'lucide-react';
|
|
|
|
const AppLayout: React.FC = () => {
|
|
const { sidebarOpen, toggleSidebar } = useAppContext();
|
|
const { isAuthenticated, profile, loading: authLoading } = useAuth();
|
|
const isMobile = useIsMobile();
|
|
|
|
const [currentModule, setCurrentModule] = useState<ModuleId>('dashboard');
|
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
|
const [zoneCheckIn, setZoneCheckIn] = useState<string | null>(null);
|
|
const [showSignInModal, setShowSignInModal] = useState(false);
|
|
|
|
// Demo role switcher for unauthenticated users
|
|
const [demoRole, setDemoRole] = useState<UserRole>('teacher');
|
|
const [showDemoRolePicker, setShowDemoRolePicker] = useState(false);
|
|
|
|
// Use real role from profile when authenticated, otherwise use demo role
|
|
const userRole: UserRole = isAuthenticated && profile?.role ? profile.role : demoRole;
|
|
const userName = isAuthenticated && profile?.full_name ? profile.full_name : 'Guest User';
|
|
const userCampus = isAuthenticated && profile?.campus ? profile.campus : 'Tigers';
|
|
|
|
|
|
const hasAccess = MODULES.find(m => m.id === currentModule)?.roles.includes(userRole);
|
|
const activeModule = hasAccess ? currentModule : 'dashboard';
|
|
|
|
const handleSetModule = (id: ModuleId) => {
|
|
setCurrentModule(id);
|
|
if (isMobile) setMobileSidebarOpen(false);
|
|
};
|
|
|
|
// When demo role changes, check if current module is still accessible
|
|
useEffect(() => {
|
|
const mod = MODULES.find(m => m.id === currentModule);
|
|
if (mod && !mod.roles.includes(userRole)) {
|
|
setCurrentModule('dashboard');
|
|
}
|
|
}, [userRole, currentModule]);
|
|
|
|
const renderModule = () => {
|
|
switch (activeModule) {
|
|
case 'dashboard':
|
|
return <Dashboard userRole={userRole} userName={userName} setCurrentModule={handleSetModule} zoneCheckIn={zoneCheckIn} setZoneCheckIn={setZoneCheckIn} />;
|
|
case 'frame':
|
|
return <FrameModule userRole={userRole} />;
|
|
case 'classroom':
|
|
return <ClassroomSupport />;
|
|
case 'timer':
|
|
return <ClassroomTimer />;
|
|
case 'qbs':
|
|
return <QBSSafety userRole={userRole} />;
|
|
case 'ei':
|
|
return <EmotionalIntelligence userRole={userRole} userName={userName} userCampus={userCampus} />;
|
|
|
|
case 'zones':
|
|
return <ZonesOfRegulation />;
|
|
case 'signs':
|
|
return <SignLanguage />;
|
|
case 'attendance':
|
|
return <CampusAttendance userRole={userRole} userCampus={userCampus} userName={userName} />;
|
|
|
|
case 'parent-comm':
|
|
return <ParentCommModule />;
|
|
case 'internal-comm':
|
|
return <InternalCommModule userRole={userRole} />;
|
|
case 'safety':
|
|
return <SafetyProtocolsModule />;
|
|
case 'handbook':
|
|
return <HandbookPolicy userRole={userRole} />;
|
|
case 'director':
|
|
return <DirectorDashboard setCurrentModule={handleSetModule} />;
|
|
case 'community':
|
|
return <CommunityService />;
|
|
case 'vocational':
|
|
return <VocationalOpportunities />;
|
|
case 'esa':
|
|
return <ESAFunding />;
|
|
case 'walkthrough':
|
|
return <WalkThroughCheckIn />;
|
|
default:
|
|
|
|
return <Dashboard userRole={userRole} userName={userName} setCurrentModule={handleSetModule} zoneCheckIn={zoneCheckIn} setZoneCheckIn={setZoneCheckIn} />;
|
|
}
|
|
|
|
};
|
|
|
|
|
|
// Loading state
|
|
if (authLoading) {
|
|
return (
|
|
<div className="h-screen w-full flex items-center justify-center bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950">
|
|
<div className="text-center">
|
|
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-violet-500 to-amber-400 flex items-center justify-center mx-auto mb-4 shadow-lg shadow-violet-500/30 animate-pulse">
|
|
<span className="text-white font-bold text-2xl">F</span>
|
|
</div>
|
|
<Loader2 size={24} className="animate-spin text-violet-400 mx-auto mb-3" />
|
|
<p className="text-slate-400 text-sm">Loading FRAMEworks...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const demoRoles: { value: UserRole; label: string; color: string }[] = [
|
|
{ value: 'teacher', label: 'Teacher', color: 'text-emerald-400 bg-emerald-500/15 border-emerald-500/30' },
|
|
{ value: 'para', label: 'Support Staff', color: 'text-blue-400 bg-blue-500/15 border-blue-500/30' },
|
|
{ value: 'office', label: 'Office Manager', color: 'text-amber-400 bg-amber-500/15 border-amber-500/30' },
|
|
{ value: 'director', label: 'Director', color: 'text-purple-400 bg-purple-500/15 border-purple-500/30' },
|
|
{ value: 'superintendent', label: 'Superintendent', color: 'text-rose-400 bg-rose-500/15 border-rose-500/30' },
|
|
];
|
|
|
|
|
|
const currentDemoRole = demoRoles.find(r => r.value === demoRole)!;
|
|
|
|
// Always show the full app — authenticated or guest
|
|
return (
|
|
<div className="h-screen w-full flex bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 overflow-hidden">
|
|
{/* Mobile sidebar overlay */}
|
|
{mobileSidebarOpen && isMobile && (
|
|
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-30" onClick={() => setMobileSidebarOpen(false)} />
|
|
)}
|
|
|
|
{/* Sidebar */}
|
|
<div className={`${isMobile ? `fixed left-0 top-0 h-full z-40 transform transition-transform duration-300 ${mobileSidebarOpen ? 'translate-x-0' : '-translate-x-full'}` : 'relative'}`}>
|
|
<Sidebar currentModule={activeModule} setCurrentModule={handleSetModule} userRole={userRole} collapsed={isMobile ? false : sidebarCollapsed} setCollapsed={setSidebarCollapsed} userCampus={userCampus} />
|
|
</div>
|
|
|
|
|
|
{/* Main content area */}
|
|
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
|
<TopBar
|
|
userRole={userRole}
|
|
userName={userName}
|
|
userCampus={userCampus}
|
|
toggleSidebar={() => isMobile ? setMobileSidebarOpen(!mobileSidebarOpen) : setSidebarCollapsed(!sidebarCollapsed)}
|
|
/>
|
|
|
|
<main className="flex-1 overflow-y-auto p-4 md:p-6 lg:p-8">
|
|
{/* Guest Mode Banner — only when NOT authenticated */}
|
|
{!isAuthenticated && (
|
|
<div className="max-w-7xl mx-auto mb-5">
|
|
<div className="bg-gradient-to-r from-violet-600/10 via-amber-500/10 to-emerald-500/10 rounded-2xl border border-violet-500/20 px-4 py-3 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-violet-500/20 to-amber-500/20 border border-violet-500/30 flex items-center justify-center flex-shrink-0">
|
|
<Eye size={18} className="text-violet-400" />
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-semibold text-white">
|
|
You're browsing as a Guest
|
|
</p>
|
|
<p className="text-xs text-slate-400">
|
|
Explore all modules freely. Sign in to save your progress and get a personalized experience.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2 flex-shrink-0">
|
|
{/* Demo Role Picker */}
|
|
<div className="relative">
|
|
<button
|
|
onClick={() => setShowDemoRolePicker(!showDemoRolePicker)}
|
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-semibold border transition-all ${currentDemoRole.color}`}
|
|
>
|
|
<span>View as: {currentDemoRole.label}</span>
|
|
<ChevronDown size={12} />
|
|
</button>
|
|
{showDemoRolePicker && (
|
|
<>
|
|
<div className="fixed inset-0 z-30" onClick={() => setShowDemoRolePicker(false)} />
|
|
<div className="absolute right-0 top-full mt-1.5 w-48 bg-slate-800 rounded-xl shadow-2xl shadow-black/40 border border-slate-700/50 py-1.5 z-40">
|
|
<p className="px-3 py-1.5 text-[10px] text-slate-500 uppercase tracking-wider font-semibold">Switch Demo Role</p>
|
|
{demoRoles.map(r => (
|
|
<button
|
|
key={r.value}
|
|
onClick={() => {
|
|
setDemoRole(r.value);
|
|
setShowDemoRolePicker(false);
|
|
}}
|
|
className={`w-full text-left px-3 py-2 text-sm transition-colors flex items-center justify-between ${
|
|
demoRole === r.value
|
|
? 'bg-violet-500/10 text-white'
|
|
: 'text-slate-300 hover:bg-slate-700/50'
|
|
}`}
|
|
>
|
|
<span className="font-medium">{r.label}</span>
|
|
{demoRole === r.value && <CheckCircle2 size={14} className="text-violet-400" />}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => setShowSignInModal(true)}
|
|
className="flex items-center gap-1.5 px-4 py-1.5 bg-gradient-to-r from-violet-500 to-amber-500 hover:from-violet-600 hover:to-amber-600 text-white font-semibold rounded-xl transition-all duration-200 shadow-lg shadow-violet-500/25 hover:shadow-violet-500/40 text-xs"
|
|
>
|
|
<LogIn size={14} />
|
|
Sign In
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Module Content */}
|
|
<div className="max-w-7xl mx-auto">{renderModule()}</div>
|
|
|
|
{/* Footer */}
|
|
<footer className="mt-12 pb-6">
|
|
<div className="max-w-7xl mx-auto">
|
|
<div className="bg-slate-800/40 backdrop-blur-sm rounded-2xl border border-slate-700/40 p-6 md:p-8">
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
|
|
<div className="md:col-span-1">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-violet-500 to-amber-400 flex items-center justify-center shadow-lg shadow-violet-500/20">
|
|
<span className="text-white font-bold text-sm">F</span>
|
|
</div>
|
|
<span className="font-bold text-lg bg-gradient-to-r from-violet-400 to-amber-400 bg-clip-text text-transparent">FRAMEworks</span>
|
|
</div>
|
|
<p className="text-xs text-slate-500 leading-relaxed">A modular, role-based school operations platform designed for autism-focused educational environments.</p>
|
|
</div>
|
|
<div>
|
|
<h4 className="font-semibold text-sm text-slate-300 mb-3">Core Modules</h4>
|
|
<ul className="space-y-1.5">
|
|
{[
|
|
{ label: 'Classroom Support', mod: 'classroom' as ModuleId },
|
|
{ label: 'Classroom Timer', mod: 'timer' as ModuleId },
|
|
{ label: 'De-escalation Strategies', mod: 'qbs' as ModuleId },
|
|
{ label: 'Regulate your Zone', mod: 'zones' as ModuleId },
|
|
{ label: 'Sign Language', mod: 'signs' as ModuleId },
|
|
].map(item => (
|
|
<li key={item.label}>
|
|
<button
|
|
onClick={() => handleSetModule(item.mod)}
|
|
className="text-xs text-slate-500 hover:text-violet-400 transition-colors"
|
|
>
|
|
{item.label}
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
|
|
</div>
|
|
|
|
<div>
|
|
<h4 className="font-semibold text-sm text-slate-300 mb-3">Operations</h4>
|
|
<ul className="space-y-1.5">
|
|
{[
|
|
{ label: 'F.R.A.M.E. Weekly', mod: 'frame' as ModuleId },
|
|
{ label: 'Attendance', mod: 'attendance' as ModuleId },
|
|
{ label: 'Handbook & Policies', mod: 'handbook' as ModuleId },
|
|
{ label: 'Safety Protocols', mod: 'safety' as ModuleId },
|
|
{ label: 'Community & Partnerships', mod: 'community' as ModuleId },
|
|
{ label: 'Vocational Opportunities', mod: 'vocational' as ModuleId },
|
|
{ label: 'ESA Funding Info', mod: 'esa' as ModuleId },
|
|
].map(item => (
|
|
<li key={item.label}>
|
|
<button
|
|
onClick={() => handleSetModule(item.mod)}
|
|
className="text-xs text-slate-500 hover:text-violet-400 transition-colors"
|
|
>
|
|
{item.label}
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
|
|
<div>
|
|
<h4 className="font-semibold text-sm text-slate-300 mb-3">Platform</h4>
|
|
<ul className="space-y-1.5">
|
|
{['Role-Based Access', 'Secure Auth', 'ADP Integration', 'FERPA Compliant'].map(item => (
|
|
<li key={item} className="text-xs text-slate-500">{item}</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<div className="mt-6 pt-4 border-t border-slate-700/40 flex flex-col md:flex-row items-center justify-between gap-2">
|
|
<p className="text-[10px] text-slate-600">FRAMEworks © 2026 — Built for autism-focused school communities</p>
|
|
<div className="flex items-center gap-2">
|
|
<CheckCircle2 size={10} className={isAuthenticated ? 'text-emerald-500' : 'text-slate-600'} />
|
|
<span className="text-[10px] text-slate-600">
|
|
{isAuthenticated
|
|
? `Signed in as ${userName} (${userRole})`
|
|
: `Browsing as Guest (${currentDemoRole.label})`
|
|
}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
</main>
|
|
</div>
|
|
|
|
{/* Sign In Modal */}
|
|
<SignInModal isOpen={showSignInModal} onClose={() => setShowSignInModal(false)} />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default AppLayout;
|