Files
NextEdu/src/app/(dashboard)/profile/page.tsx
SpecialX 1a9377222c feat(app): add error/loading boundaries and update dashboard routes
- Add error.tsx and loading.tsx boundaries for admin, parent, student, teacher routes

- Add dashboard-error-fallback and dashboard-loading-skeleton components

- Add student/learning page, parent/leave routes, teacher textbook components

- Update existing app routes across auth, dashboard, and API endpoints

- Update proxy middleware and next-auth type declarations
2026-06-23 17:38:28 +08:00

160 lines
6.7 KiB
TypeScript

import Link from "next/link"
import { redirect } from "next/navigation"
import { Suspense, type ReactElement } from "react"
import { getTranslations } from "next-intl/server"
import { User, Mail, Phone, MapPin, Calendar, Clock, Shield } from "lucide-react"
import { requireAuth } from "@/shared/lib/auth-guard"
import { getUserProfile } from "@/modules/users/data-access"
import { AvatarUpload } from "@/modules/settings/components/avatar-upload"
import { ProfileStudentOverview, ProfileStudentOverviewSkeleton } from "@/modules/settings/components/profile-student-overview"
import { ProfileTeacherOverview, ProfileTeacherOverviewSkeleton } from "@/modules/settings/components/profile-teacher-overview"
import { SettingsSectionErrorBoundary } from "@/modules/settings/components/settings-section-error-boundary"
import { Badge } from "@/shared/components/ui/badge"
import { Button } from "@/shared/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/shared/components/ui/card"
import { PageHeader } from "@/shared/components/ui/page-header"
import { formatDate } from "@/shared/lib/utils"
export const dynamic = "force-dynamic"
export async function generateMetadata() {
const t = await getTranslations("settings.profilePage")
return { title: t("title") }
}
export default async function ProfilePage(): Promise<ReactElement> {
const ctx = await requireAuth()
const userId = ctx.userId
const userProfile = await getUserProfile(userId)
if (!userProfile) {
redirect("/login")
}
const roles = ctx.roles
const isStudent = roles.includes("student")
const isTeacher = roles.includes("teacher")
const t = await getTranslations("settings.profilePage")
return (
<div className="flex h-full flex-col gap-8 p-8">
<PageHeader
title={t("title")}
description={t("description")}
actions={
<Button asChild variant="outline">
<Link href="/settings">{t("editProfile")}</Link>
</Button>
}
/>
<AvatarUpload
currentImage={userProfile.image}
name={userProfile.name}
email={userProfile.email}
/>
<div className="grid gap-6 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<User className="h-5 w-5" />
{t("personalInfo.title")}
</CardTitle>
<CardDescription>{t("personalInfo.description")}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-1">
<div className="text-sm font-medium text-muted-foreground">{t("personalInfo.fullName")}</div>
<div className="text-sm font-medium">{userProfile.name ?? "-"}</div>
</div>
<div className="space-y-1">
<div className="text-sm font-medium text-muted-foreground">{t("personalInfo.gender")}</div>
<div className="text-sm capitalize">{userProfile.gender ?? "-"}</div>
</div>
<div className="space-y-1">
<div className="text-sm font-medium text-muted-foreground">{t("personalInfo.age")}</div>
<div className="text-sm">{userProfile.age ?? "-"}</div>
</div>
<div className="space-y-1">
<div className="text-sm font-medium text-muted-foreground">{t("personalInfo.phone")}</div>
<div className="flex items-center gap-2 text-sm">
{userProfile.phone ? <Phone className="h-3 w-3 text-muted-foreground" /> : null}
{userProfile.phone ?? "-"}
</div>
</div>
<div className="col-span-1 sm:col-span-2 space-y-1">
<div className="text-sm font-medium text-muted-foreground">{t("personalInfo.address")}</div>
<div className="flex items-center gap-2 text-sm">
{userProfile.address ? <MapPin className="h-3 w-3 text-muted-foreground" /> : null}
{userProfile.address ?? "-"}
</div>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="h-5 w-5" />
{t("accountInfo.title")}
</CardTitle>
<CardDescription>{t("accountInfo.description")}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="col-span-1 sm:col-span-2 space-y-1">
<div className="text-sm font-medium text-muted-foreground">{t("accountInfo.email")}</div>
<div className="flex items-center gap-2 text-sm">
<Mail className="h-3 w-3 text-muted-foreground" />
{userProfile.email}
</div>
</div>
<div className="space-y-1">
<div className="text-sm font-medium text-muted-foreground">{t("accountInfo.role")}</div>
<Badge variant="secondary" className="capitalize">
{userProfile.role}
</Badge>
</div>
<div className="space-y-1">
<div className="text-sm font-medium text-muted-foreground">{t("accountInfo.memberSince")}</div>
<div className="flex items-center gap-2 text-sm">
<Calendar className="h-3 w-3 text-muted-foreground" />
{formatDate(userProfile.createdAt)}
</div>
</div>
<div className="space-y-1">
<div className="text-sm font-medium text-muted-foreground">{t("accountInfo.onboardedAt")}</div>
<div className="flex items-center gap-2 text-sm">
<Clock className="h-3 w-3 text-muted-foreground" />
{userProfile.onboardedAt ? formatDate(userProfile.onboardedAt) : "-"}
</div>
</div>
</div>
</CardContent>
</Card>
</div>
{isStudent ? (
<SettingsSectionErrorBoundary>
<Suspense fallback={<ProfileStudentOverviewSkeleton />}>
<ProfileStudentOverview userId={userId} />
</Suspense>
</SettingsSectionErrorBoundary>
) : null}
{isTeacher ? (
<SettingsSectionErrorBoundary>
<Suspense fallback={<ProfileTeacherOverviewSkeleton />}>
<ProfileTeacherOverview />
</Suspense>
</SettingsSectionErrorBoundary>
) : null}
</div>
)
}