improverd dashboard page layout

This commit is contained in:
Dmitri 2026-06-12 08:51:17 +02:00
parent 017cdfd3d5
commit 4b45ce30f5
8 changed files with 137 additions and 41 deletions

View File

@ -2,7 +2,7 @@ import type { ReactNode } from 'react';
import { Suspense } from 'react'; import { Suspense } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { useShellOutletContext } from '@/app/shellOutletContext'; import { useShellOutletContext } from '@/app/shellOutletContext';
import { StatePanel } from '@/components/ui/state-panel'; import { PageSkeleton } from '@/components/ui/page-skeleton';
import { canUserRoleAccessModuleRoute } from '@/shared/constants/moduleRoutes'; import { canUserRoleAccessModuleRoute } from '@/shared/constants/moduleRoutes';
import NotFound from '@/pages/NotFound'; import NotFound from '@/pages/NotFound';
@ -20,13 +20,7 @@ export function ModuleRouteGuard({ children }: ModuleRouteGuardProps) {
} }
return ( return (
<Suspense <Suspense fallback={<PageSkeleton />}>
fallback={(
<StatePanel tone="violet" alignment="center" loading className="my-20">
Loading module...
</StatePanel>
)}
>
{children} {children}
</Suspense> </Suspense>
); );

View File

@ -4,12 +4,11 @@ import { useAuth } from '@/contexts/useAuth';
import { useIsMobile } from '@/hooks/use-mobile'; import { useIsMobile } from '@/hooks/use-mobile';
import { useAppShell } from '@/business/app-shell/hooks'; import { useAppShell } from '@/business/app-shell/hooks';
import { AppFooter } from '@/components/app-shell/AppFooter'; import { AppFooter } from '@/components/app-shell/AppFooter';
import { AppShellSkeleton } from '@/components/ui/app-shell-skeleton';
import Sidebar from '@/components/frameworks/Sidebar'; import Sidebar from '@/components/frameworks/Sidebar';
import TopBar from '@/components/frameworks/TopBar'; import TopBar from '@/components/frameworks/TopBar';
import { Loader2 } from 'lucide-react';
const AppLayout: React.FC = () => { const AppLayout: React.FC = () => {
const { profile, loading: authLoading } = useAuth(); const { profile, loading: authLoading } = useAuth();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
@ -22,19 +21,8 @@ const AppLayout: React.FC = () => {
setMobileSidebarOpen, setMobileSidebarOpen,
} = useAppShell({ profile, isMobile }); } = useAppShell({ profile, isMobile });
// Loading state
if (authLoading) { if (authLoading) {
return ( return <AppShellSkeleton />;
<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>
);
} }
return ( return (

View File

@ -1,8 +1,22 @@
import { useMemo } from 'react';
import { Sparkles } from 'lucide-react'; import { Sparkles } from 'lucide-react';
import type { FrameEntryViewModel } from '@/business/frame/types'; import type { FrameEntryViewModel } from '@/business/frame/types';
import { HERO_IMAGE } from '@/shared/constants/appData'; import { HERO_IMAGE } from '@/shared/constants/appData';
function getCurrentWeekMonday(): string {
const today = new Date();
const dayOfWeek = today.getDay();
const mondayOffset = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
const monday = new Date(today);
monday.setDate(today.getDate() + mondayOffset);
return monday.toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
});
}
interface DashboardHeroProps { interface DashboardHeroProps {
readonly greeting: string; readonly greeting: string;
readonly userName: string; readonly userName: string;
@ -12,8 +26,8 @@ interface DashboardHeroProps {
export function DashboardHero({ export function DashboardHero({
greeting, greeting,
userName, userName,
latestFrame,
}: DashboardHeroProps) { }: DashboardHeroProps) {
const currentWeekMonday = useMemo(() => getCurrentWeekMonday(), []);
return ( return (
<section className="relative overflow-hidden rounded-2xl h-52 md:h-60"> <section className="relative overflow-hidden rounded-2xl h-52 md:h-60">
<img src={HERO_IMAGE} alt="Classroom" className="w-full h-full object-cover" /> <img src={HERO_IMAGE} alt="Classroom" className="w-full h-full object-cover" />
@ -23,7 +37,7 @@ export function DashboardHero({
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<Sparkles size={18} className="text-amber-400" /> <Sparkles size={18} className="text-amber-400" />
<span className="text-amber-400 text-sm font-medium"> <span className="text-amber-400 text-sm font-medium">
{latestFrame ? `Week of ${latestFrame.weekOf}` : 'No weekly focus posted'} Week of {currentWeekMonday}
</span> </span>
</div> </div>
<h1 className="text-2xl md:text-4xl font-bold text-white mb-1"> <h1 className="text-2xl md:text-4xl font-bold text-white mb-1">

View File

@ -4,7 +4,7 @@ interface SidebarBrandProps {
export function SidebarBrand({ collapsed }: SidebarBrandProps) { export function SidebarBrand({ collapsed }: SidebarBrandProps) {
return ( return (
<div className="p-4 border-b border-slate-700/50 flex items-center gap-3"> <div className="py-2.5 px-4 border-b border-slate-700/50 flex items-center gap-3">
{collapsed ? ( {collapsed ? (
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-violet-500 to-amber-400 flex items-center justify-center mx-auto shadow-lg shadow-violet-500/30"> <div className="w-9 h-9 rounded-xl bg-gradient-to-br from-violet-500 to-amber-400 flex items-center justify-center mx-auto shadow-lg shadow-violet-500/30">
<span className="text-white font-bold text-sm">F</span> <span className="text-white font-bold text-sm">F</span>

View File

@ -2,9 +2,7 @@ import { ChevronLeft, ChevronRight } from 'lucide-react';
import type { SidebarPage } from '@/business/app-shell/types'; import type { SidebarPage } from '@/business/app-shell/types';
import { SidebarBrand } from '@/components/sidebar/SidebarBrand'; import { SidebarBrand } from '@/components/sidebar/SidebarBrand';
import { SidebarCampusRolePanel } from '@/components/sidebar/SidebarCampusRolePanel';
import { SidebarNavigation } from '@/components/sidebar/SidebarNavigation'; import { SidebarNavigation } from '@/components/sidebar/SidebarNavigation';
import { Button } from '@/components/ui/button';
interface SidebarViewProps { interface SidebarViewProps {
readonly page: SidebarPage; readonly page: SidebarPage;
@ -15,16 +13,14 @@ export function SidebarView({ page }: SidebarViewProps) {
<aside className={`${page.collapsed ? 'w-16' : 'w-64'} bg-gradient-to-b from-slate-900 via-slate-900 to-slate-800 h-full flex flex-col transition-all duration-300 shadow-2xl shadow-black/20 relative`}> <aside className={`${page.collapsed ? 'w-16' : 'w-64'} bg-gradient-to-b from-slate-900 via-slate-900 to-slate-800 h-full flex flex-col transition-all duration-300 shadow-2xl shadow-black/20 relative`}>
<SidebarBrand collapsed={page.collapsed} /> <SidebarBrand collapsed={page.collapsed} />
<Button <button
type="button" type="button"
variant="ghost"
size="icon"
onClick={page.toggleCollapsed} onClick={page.toggleCollapsed}
aria-label={page.collapsed ? 'Expand sidebar' : 'Collapse sidebar'} aria-label={page.collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
className="absolute top-4 -right-3 w-6 h-6 bg-gradient-to-r from-violet-500 to-violet-600 text-white rounded-full flex items-center justify-center shadow-lg shadow-violet-500/40 hover:shadow-violet-500/60 transition-all z-10" className="absolute -right-3 top-[10px] w-3 h-9 bg-violet-500 hover:bg-violet-400 rounded-r flex items-center justify-center cursor-pointer z-50"
> >
{page.collapsed ? <ChevronRight size={14} /> : <ChevronLeft size={14} />} {page.collapsed ? <ChevronRight size={12} strokeWidth={3} className="text-white" /> : <ChevronLeft size={12} strokeWidth={3} className="text-white" />}
</Button> </button>
<SidebarNavigation <SidebarNavigation
modules={page.modules} modules={page.modules}
@ -32,13 +28,6 @@ export function SidebarView({ page }: SidebarViewProps) {
collapsed={page.collapsed} collapsed={page.collapsed}
onSelectModule={page.setCurrentModule} onSelectModule={page.setCurrentModule}
/> />
<SidebarCampusRolePanel
collapsed={page.collapsed}
campusInfo={page.campusInfo}
campusInitial={page.campusInitial}
roleLabel={page.roleLabel}
/>
</aside> </aside>
); );
} }

View File

@ -16,7 +16,7 @@ interface TopBarViewProps {
export function TopBarView({ page }: TopBarViewProps) { export function TopBarView({ page }: TopBarViewProps) {
return ( return (
<header className="h-16 bg-slate-900/95 backdrop-blur-xl border-b border-slate-700/50 flex items-center justify-between px-4 md:px-6 relative z-20"> <header className="h-14 bg-slate-900/95 backdrop-blur-xl border-b border-slate-700/50 flex items-center justify-between px-4 md:px-6 relative z-20">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Button <Button
type="button" type="button"

View File

@ -0,0 +1,69 @@
import { Skeleton } from '@/components/ui/skeleton';
/**
* Full app shell skeleton shown during initial auth loading.
* Mimics the sidebar + topbar + content layout.
*/
export function AppShellSkeleton() {
return (
<div className="h-screen w-full flex bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 overflow-hidden">
{/* Sidebar skeleton */}
<div className="hidden md:flex w-60 flex-col border-r border-slate-800/50 bg-slate-900/30 p-4">
{/* Logo */}
<div className="flex items-center gap-3 mb-8">
<Skeleton className="h-10 w-10 rounded-xl bg-slate-800" />
<div className="space-y-1.5">
<Skeleton className="h-4 w-24 bg-slate-800" />
<Skeleton className="h-3 w-16 bg-slate-800" />
</div>
</div>
{/* Nav items */}
<div className="space-y-2">
{[1, 2, 3, 4, 5, 6, 7, 8].map((i) => (
<Skeleton key={i} className="h-10 w-full rounded-lg bg-slate-800" />
))}
</div>
{/* Bottom section */}
<div className="mt-auto space-y-2">
<Skeleton className="h-3 w-16 bg-slate-800" />
<Skeleton className="h-10 w-full rounded-lg bg-slate-800" />
</div>
</div>
{/* Main content area */}
<div className="flex-1 flex flex-col min-w-0">
{/* TopBar skeleton */}
<div className="h-16 border-b border-slate-800/50 bg-slate-900/30 flex items-center justify-between px-4">
<Skeleton className="h-10 w-64 rounded-lg bg-slate-800" />
<div className="flex items-center gap-4">
<Skeleton className="h-8 w-24 rounded-full bg-slate-800" />
<Skeleton className="h-8 w-8 rounded-full bg-slate-800" />
<Skeleton className="h-10 w-10 rounded-full bg-slate-800" />
</div>
</div>
{/* Content skeleton */}
<div className="flex-1 p-6 space-y-6">
{/* Page header */}
<div className="space-y-2">
<Skeleton className="h-8 w-48 bg-slate-800" />
<Skeleton className="h-4 w-72 bg-slate-800" />
</div>
{/* Content cards */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{[1, 2, 3].map((i) => (
<div key={i} className="rounded-lg border border-slate-800/50 bg-slate-900/30 p-4 space-y-3">
<Skeleton className="h-5 w-3/4 bg-slate-800" />
<Skeleton className="h-4 w-full bg-slate-800" />
<Skeleton className="h-4 w-5/6 bg-slate-800" />
</div>
))}
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,42 @@
import { Skeleton } from '@/components/ui/skeleton';
/**
* Generic page loading skeleton. Shows a content-shaped placeholder
* while the page component is being lazy-loaded.
*/
export function PageSkeleton() {
return (
<div className="space-y-6 p-6">
{/* Page header */}
<div className="flex items-center justify-between">
<div className="space-y-2">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-4 w-72" />
</div>
<Skeleton className="h-10 w-32" />
</div>
{/* Content cards grid */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{[1, 2, 3].map((i) => (
<div key={i} className="rounded-lg border bg-card p-4 space-y-3">
<Skeleton className="h-5 w-3/4" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-5/6" />
<Skeleton className="h-4 w-2/3" />
</div>
))}
</div>
{/* Additional content area */}
<div className="rounded-lg border bg-card p-4 space-y-3">
<Skeleton className="h-6 w-40" />
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</div>
</div>
</div>
);
}