Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions app/(protected)/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"use client";

import { useEffect } from "react";
import { AlertTriangle, RefreshCw } from "lucide-react";

import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Link } from "@/lib/router";

export default function ProtectedError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
useEffect(() => {
console.error("[StudyOS] protected route failed", error);
}, [error]);

return (
<Card className="mx-auto max-w-xl border-destructive/20 shadow-card" role="alert">
<CardContent className="flex flex-col items-center px-6 py-12 text-center">
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-destructive/10">
<AlertTriangle className="h-5 w-5 text-destructive" />
</div>
<h1 className="mt-4 text-lg font-semibold text-foreground">This page could not be loaded</h1>
<p className="mt-1.5 max-w-md text-sm leading-6 text-muted-foreground">
Your data is safe. Try loading the page again, or return to the dashboard if the problem continues.
</p>
<div className="mt-5 flex flex-col gap-2 sm:flex-row">
<Button onClick={reset}>
<RefreshCw className="mr-2 h-4 w-4" />
Try again
</Button>
<Button asChild variant="outline">
<Link to="/">Return to dashboard</Link>
</Button>
</div>
</CardContent>
</Card>
);
}
22 changes: 22 additions & 0 deletions app/(protected)/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Skeleton } from "@/components/ui/skeleton";

export default function ProtectedLoading() {
return (
<div className="space-y-5" role="status" aria-label="Loading workspace">
<div className="rounded-2xl border border-border bg-card p-5 shadow-card">
<Skeleton className="h-5 w-36" />
<Skeleton className="mt-3 h-3 w-64 max-w-full" />
<div className="mt-5 grid grid-cols-2 gap-3 sm:grid-cols-4">
{Array.from({ length: 4 }).map((_, index) => (
<Skeleton key={index} className="h-20 rounded-xl" />
))}
</div>
</div>
<div className="grid gap-4 lg:grid-cols-2">
<Skeleton className="h-64 rounded-2xl" />
<Skeleton className="h-64 rounded-2xl" />
</div>
<span className="sr-only">Loading your StudyOS workspace…</span>
</div>
);
}
25 changes: 18 additions & 7 deletions app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import Link from "next/link";
import { ArrowLeft, SearchX } from "lucide-react";

export default function NotFound() {
return (
<div className="flex min-h-screen items-center justify-center bg-muted">
<div className="text-center">
<h1 className="mb-4 text-4xl font-bold">404</h1>
<p className="mb-4 text-xl text-muted-foreground">Oops! Page not found</p>
<Link href="/" className="text-primary underline hover:text-primary/90">
Return to Home
<main className="flex min-h-screen items-center justify-center bg-muted/30 px-4">
<div className="w-full max-w-md rounded-2xl border border-border bg-card p-8 text-center shadow-card">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10 text-primary">
<SearchX className="h-6 w-6" />
</div>
<p className="mt-5 text-xs font-semibold uppercase tracking-[0.18em] text-primary">404 error</p>
<h1 className="mt-2 text-2xl font-bold tracking-tight text-foreground">Page not found</h1>
<p className="mt-2 text-sm leading-6 text-muted-foreground">
The page may have moved, or the link may be incorrect.
</p>
<Link
href="/"
className="mt-6 inline-flex h-10 items-center justify-center gap-2 rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
<ArrowLeft className="h-4 w-4" />
Return to dashboard
</Link>
</div>
</div>
</main>
);
}
22 changes: 16 additions & 6 deletions src/components/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,30 @@ export function AppLayout({ children }: { children: ReactNode }) {
return (
<SidebarProvider>
<div className="flex min-h-screen w-full bg-background">
<a
href="#main-content"
className="fixed left-4 top-3 z-[100] -translate-y-20 rounded-lg bg-primary px-3 py-2 text-sm font-semibold text-primary-foreground shadow-lg transition-transform focus:translate-y-0"
>
Skip to main content
</a>
<AppSidebar />
<div className="flex flex-1 flex-col min-w-0">
<header className="sticky top-0 z-30 flex h-14 items-center justify-between border-b border-border bg-background/80 backdrop-blur-md px-4 lg:px-6">
<div className="flex items-center gap-3">
<header className="sticky top-0 z-30 flex h-14 items-center justify-between border-b border-border bg-background/90 px-3 backdrop-blur-md sm:px-4 lg:px-6">
<div className="flex min-w-0 items-center gap-2 sm:gap-3">
<SidebarTrigger className="text-muted-foreground hover:text-foreground transition-colors" />
<GlobalSearch />
</div>
<div className="flex items-center gap-2">
<NotificationBell />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="relative rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background cursor-pointer group">
<Avatar className="h-8 w-8 border border-border transition-all duration-200 group-hover:ring-2 group-hover:ring-primary/40 group-hover:scale-105 group-hover:brightness-110">
<AvatarImage src={user?.user_metadata?.avatar_url} />
<button
type="button"
aria-label={`Open account menu for ${displayName}`}
className="relative cursor-pointer rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background group"
>
<Avatar className="h-8 w-8 border border-border transition-all duration-200 group-hover:ring-2 group-hover:ring-primary/40 group-hover:brightness-110">
<AvatarImage src={user?.user_metadata?.avatar_url} alt="" />
<AvatarFallback className="text-xs font-semibold bg-muted text-muted-foreground">{initials}</AvatarFallback>
</Avatar>
<span className="absolute bottom-0 right-0 block h-2.5 w-2.5 rounded-full bg-green-500 ring-2 ring-background" />
Expand Down Expand Up @@ -75,7 +85,7 @@ export function AppLayout({ children }: { children: ReactNode }) {
</DropdownMenu>
</div>
</header>
<main className="flex-1 overflow-auto p-4 lg:p-6">
<main id="main-content" tabIndex={-1} className="flex-1 overflow-auto p-3 outline-none sm:p-4 lg:p-6">
<div className="mx-auto max-w-6xl">
{children}
</div>
Expand Down
31 changes: 25 additions & 6 deletions src/components/AppSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,17 @@ const gradientToColor: Record<string, string> = {
"gradient-sunset": "text-amber",
};

function NavSection({ label, items, collapsed }: { label: string; items: typeof mainNav; collapsed: boolean }) {
function NavSection({
label,
items,
collapsed,
onNavigate,
}: {
label: string;
items: typeof mainNav;
collapsed: boolean;
onNavigate: () => void;
}) {
return (
<SidebarGroup>
<SidebarGroupLabel className="px-3 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
Expand All @@ -70,6 +80,7 @@ function NavSection({ label, items, collapsed }: { label: string; items: typeof
<NavLink
to={item.url}
end={item.url === "/"}
onClick={onNavigate}
className="flex items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-sidebar-foreground transition-all duration-200 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
activeClassName="bg-sidebar-accent text-sidebar-accent-foreground font-semibold"
>
Expand All @@ -86,9 +97,12 @@ function NavSection({ label, items, collapsed }: { label: string; items: typeof
}

export function AppSidebar() {
const { state } = useSidebar();
const { state, isMobile, setOpenMobile } = useSidebar();
const collapsed = state === "collapsed";
const { signOut, user } = useAuth();
const closeMobileNavigation = () => {
if (isMobile) setOpenMobile(false);
};

const { data: courses = [] } = useQuery({
queryKey: ["courses", user?.id, "summary"],
Expand Down Expand Up @@ -117,9 +131,9 @@ export function AppSidebar() {
</SidebarHeader>

<SidebarContent className="px-3">
<NavSection label="Main" items={mainNav} collapsed={collapsed} />
<NavSection label="Learning" items={learningNav} collapsed={collapsed} />
<NavSection label="AI" items={aiNav} collapsed={collapsed} />
<NavSection label="Main" items={mainNav} collapsed={collapsed} onNavigate={closeMobileNavigation} />
<NavSection label="Learning" items={learningNav} collapsed={collapsed} onNavigate={closeMobileNavigation} />
<NavSection label="AI" items={aiNav} collapsed={collapsed} onNavigate={closeMobileNavigation} />

{/* Dynamic Courses */}
{courses.length > 0 && (
Expand All @@ -134,6 +148,7 @@ export function AppSidebar() {
<SidebarMenuButton asChild>
<NavLink
to={`/courses/${course.id}`}
onClick={closeMobileNavigation}
className="flex items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-sidebar-foreground transition-all duration-200 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
activeClassName="bg-sidebar-accent text-sidebar-accent-foreground font-semibold"
>
Expand All @@ -157,6 +172,7 @@ export function AppSidebar() {
<SidebarMenuButton asChild>
<NavLink
to="/settings"
onClick={closeMobileNavigation}
className="flex items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-sidebar-foreground transition-all duration-200 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
activeClassName="bg-sidebar-accent text-sidebar-accent-foreground font-semibold"
>
Expand All @@ -168,7 +184,10 @@ export function AppSidebar() {
<SidebarMenuItem>
<SidebarMenuButton asChild>
<button
onClick={signOut}
onClick={() => {
closeMobileNavigation();
void signOut();
}}
className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-muted-foreground transition-all duration-200 hover:bg-destructive/10 hover:text-destructive"
>
<LogOut className="h-[18px] w-[18px] shrink-0" />
Expand Down
Loading
Loading