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 { useLocation } from 'react-router-dom';
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 NotFound from '@/pages/NotFound';
@ -20,13 +20,7 @@ export function ModuleRouteGuard({ children }: ModuleRouteGuardProps) {
}
return (
<Suspense
fallback={(
<StatePanel tone="violet" alignment="center" loading className="my-20">
Loading module...
</StatePanel>
)}
>
<Suspense fallback={<PageSkeleton />}>
{children}
</Suspense>
);

View File

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

View File

@ -1,8 +1,22 @@
import { useMemo } from 'react';
import { Sparkles } from 'lucide-react';
import type { FrameEntryViewModel } from '@/business/frame/types';
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 {
readonly greeting: string;
readonly userName: string;
@ -12,8 +26,8 @@ interface DashboardHeroProps {
export function DashboardHero({
greeting,
userName,
latestFrame,
}: DashboardHeroProps) {
const currentWeekMonday = useMemo(() => getCurrentWeekMonday(), []);
return (
<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" />
@ -23,7 +37,7 @@ export function DashboardHero({
<div className="flex items-center gap-2 mb-2">
<Sparkles size={18} className="text-amber-400" />
<span className="text-amber-400 text-sm font-medium">
{latestFrame ? `Week of ${latestFrame.weekOf}` : 'No weekly focus posted'}
Week of {currentWeekMonday}
</span>
</div>
<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) {
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 ? (
<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>

View File

@ -2,9 +2,7 @@ import { ChevronLeft, ChevronRight } from 'lucide-react';
import type { SidebarPage } from '@/business/app-shell/types';
import { SidebarBrand } from '@/components/sidebar/SidebarBrand';
import { SidebarCampusRolePanel } from '@/components/sidebar/SidebarCampusRolePanel';
import { SidebarNavigation } from '@/components/sidebar/SidebarNavigation';
import { Button } from '@/components/ui/button';
interface SidebarViewProps {
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`}>
<SidebarBrand collapsed={page.collapsed} />
<Button
<button
type="button"
variant="ghost"
size="icon"
onClick={page.toggleCollapsed}
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} />}
</Button>
{page.collapsed ? <ChevronRight size={12} strokeWidth={3} className="text-white" /> : <ChevronLeft size={12} strokeWidth={3} className="text-white" />}
</button>
<SidebarNavigation
modules={page.modules}
@ -32,13 +28,6 @@ export function SidebarView({ page }: SidebarViewProps) {
collapsed={page.collapsed}
onSelectModule={page.setCurrentModule}
/>
<SidebarCampusRolePanel
collapsed={page.collapsed}
campusInfo={page.campusInfo}
campusInitial={page.campusInitial}
roleLabel={page.roleLabel}
/>
</aside>
);
}

View File

@ -16,7 +16,7 @@ interface TopBarViewProps {
export function TopBarView({ page }: TopBarViewProps) {
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">
<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>
);
}