improverd dashboard page layout
This commit is contained in:
parent
017cdfd3d5
commit
4b45ce30f5
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 (
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
69
frontend/src/components/ui/app-shell-skeleton.tsx
Normal file
69
frontend/src/components/ui/app-shell-skeleton.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
42
frontend/src/components/ui/page-skeleton.tsx
Normal file
42
frontend/src/components/ui/page-skeleton.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user