feat(parent,auth,onboarding,files,notifications,adaptive-practice,ai): add module updates

parent:

- Add parent-student-attendance-detail component

auth:

- Add actions, data-access, schema, services, types

onboarding:

- Add parent-children-form and hooks directory

files:

- Add actions, schema, hooks directory

notifications:

- Add schema and schema test

adaptive-practice:

- Add answer-input, answer-result, practice-result-view, practice-starter-with-nav

- Add question-card, question-content, lib and services directories

ai:

- Add context/create-ai-client-service, hooks/use-drag-position, hooks/use-position-persistence
This commit is contained in:
SpecialX
2026-07-03 10:26:12 +08:00
parent f3c223d914
commit e9a5264fe7
84 changed files with 6060 additions and 2530 deletions

View File

@@ -9,6 +9,7 @@ import {
Mail,
Stethoscope,
} from "lucide-react"
import { useTranslations } from "next-intl"
import { Button } from "@/shared/components/ui/button"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/shared/components/ui/tabs"
@@ -26,7 +27,7 @@ export type ChildDetailTab = "overview" | "homework" | "grades" | "exams" | "sch
const VALID_TABS: ChildDetailTab[] = ["overview", "homework", "grades", "exams", "schedule", "attendance", "diagnostic"]
const isTab = (v: string | undefined | null): v is ChildDetailTab =>
typeof v === "string" && (VALID_TABS as string[]).includes(v)
typeof v === "string" && VALID_TABS.some((tab) => tab === v)
const resolveTab = (v: string | undefined | null): ChildDetailTab =>
isTab(v) ? v : "overview"
@@ -40,40 +41,47 @@ export function ChildDetailPanel({
initialTab?: string
siblingSwitcher?: React.ReactNode
}) {
const t = useTranslations("parent")
const { basicInfo, todaySchedule, weeklySchedule, homeworkSummary, gradeTrend, examResults } = child
const childName = basicInfo.name ?? "Child"
const childName = basicInfo.name ?? t("childDetail.defaultName")
const [tab, setTab] = useState<ChildDetailTab>(resolveTab(initialTab))
const tabs = useMemo(
() => [
{ id: "overview" as const, label: "Overview", icon: ClipboardList },
{ id: "homework" as const, label: "Homework", icon: ClipboardList },
{ id: "grades" as const, label: "Grades", icon: BarChart3 },
{ id: "exams" as const, label: "Exams", icon: GraduationCap },
{ id: "schedule" as const, label: "Schedule", icon: CalendarDays },
{ id: "attendance" as const, label: "Attendance", icon: CalendarDays },
{ id: "diagnostic" as const, label: "Diagnostic", icon: Stethoscope },
{ id: "overview" as const, label: t("childDetail.tabs.overview"), icon: ClipboardList },
{ id: "homework" as const, label: t("childDetail.tabs.homework"), icon: ClipboardList },
{ id: "grades" as const, label: t("childDetail.tabs.grades"), icon: BarChart3 },
{ id: "exams" as const, label: t("childDetail.tabs.exams"), icon: GraduationCap },
{ id: "schedule" as const, label: t("childDetail.tabs.schedule"), icon: CalendarDays },
{ id: "attendance" as const, label: t("childDetail.tabs.attendance"), icon: CalendarDays },
{ id: "diagnostic" as const, label: t("childDetail.tabs.diagnostic"), icon: Stethoscope },
],
[],
[t],
)
return (
<div className="space-y-6">
{siblingSwitcher}
<Tabs value={tab} onValueChange={(v) => setTab(v as ChildDetailTab)} className="w-full">
<Tabs
value={tab}
onValueChange={(v) => {
if (isTab(v)) setTab(v)
}}
className="w-full"
>
<div className="overflow-x-auto">
<TabsList className="w-full justify-start">
{tabs.map((t) => (
{tabs.map((tab) => (
<TabsTrigger
key={t.id}
value={t.id}
key={tab.id}
value={tab.id}
className="gap-1.5"
aria-label={`${t.label} tab`}
aria-label={t("childDetail.tabAriaLabel", { label: tab.label })}
>
<t.icon className="h-3.5 w-3.5" />
{t.label}
<tab.icon className="h-3.5 w-3.5" />
{tab.label}
</TabsTrigger>
))}
</TabsList>
@@ -108,7 +116,7 @@ export function ChildDetailPanel({
<ChildGradeSummary grades={gradeTrend} childId={basicInfo.id} childName={childName} />
<div>
<h3 className="text-sm font-medium uppercase text-muted-foreground mb-3">
Subject Analysis
{t("childDetail.subjectAnalysis")}
</h3>
<ChildGradeDetail grades={gradeTrend} />
</div>
@@ -134,14 +142,16 @@ export function ChildDetailPanel({
<TabsContent value="attendance" className="mt-6">
<div className="rounded-md border bg-muted/30 p-6 text-center">
<p className="text-sm text-muted-foreground">
Attendance details are available on the{" "}
<a
href="/parent/attendance"
className="font-medium text-foreground underline-offset-4 hover:underline"
>
Attendance page
</a>
.
{t.rich("childDetail.attendanceHint", {
link: (chunks) => (
<a
href="/parent/attendance"
className="font-medium text-foreground underline-offset-4 hover:underline"
>
{chunks}
</a>
),
})}
</p>
</div>
</TabsContent>
@@ -149,7 +159,7 @@ export function ChildDetailPanel({
<TabsContent value="diagnostic" className="mt-6">
<div className="rounded-md border bg-muted/30 p-6 text-center">
<p className="text-sm text-muted-foreground">
Diagnostic reports will be available here once published by the school.
{t("childDetail.diagnosticHint")}
</p>
</div>
</TabsContent>
@@ -157,9 +167,12 @@ export function ChildDetailPanel({
<div className="flex justify-end">
<Button asChild variant="ghost" size="sm" className="gap-2">
<a href={`/messages?studentId=${basicInfo.id}`} aria-label={`Contact teacher about ${childName}`}>
<a
href={`/messages?studentId=${basicInfo.id}`}
aria-label={t("childDetail.contactTeacherAria", { name: childName })}
>
<Mail className="h-4 w-4" />
Contact Teacher
{t("childDetail.contactTeacher")}
</a>
</Button>
</div>
@@ -175,21 +188,24 @@ export function SiblingSwitcher({
current: { id: string; name: string | null }
siblings: Array<{ id: string; name: string | null }>
}) {
const t = useTranslations("parent")
if (siblings.length <= 1) return null
return (
<div className="flex flex-wrap items-center gap-2 rounded-md border bg-muted/30 p-2">
<span className="px-2 text-xs font-medium uppercase text-muted-foreground">Switch child</span>
<span className="px-2 text-xs font-medium uppercase text-muted-foreground">
{t("childDetail.switchChild")}
</span>
<div className="flex flex-wrap gap-1">
{siblings.map((s) => {
const isActive = s.id === current.id
const label = s.name ?? "Child"
const label = s.name ?? t("childDetail.defaultName")
return (
<a
key={s.id}
href={`/parent/children/${s.id}`}
aria-current={isActive ? "page" : undefined}
aria-label={`View ${label}'s details`}
aria-label={t("childDetail.viewDetailsAria", { name: label })}
className={cn(
"inline-flex min-h-[40px] items-center rounded-md px-3 text-sm font-medium transition-colors",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",