fix(ui): design-system consistency — typography, borders, admin IA, search removal

- typography: font-semibold/font-medium → font-bold throughout (card, dialog, empty-state, notification-bell, quiz-experience, questions-panel, profile-manage)
- borders: border → border-2 for content containers (leaderboard error, notifications page, quiz pass screen, quiz option tiles, learning-panel error)
- admin IA: remove Card-in-SectionPanel nesting from announce-panel, challenges-panel, physical-orders-panel; SectionPanel gains optional actions slot
- announce-panel: flat composer + live preview; history as flat list; cancel-edit button
- lockout: raw textarea → Textarea design-system component
- global-search: delete component and all references (feature removed)
- progress.tsx: dir="ltr" for correct RTL fill direction; remove rtl:me-auto
This commit is contained in:
ahp
2026-10-20 17:51:23 +03:30
parent 7716c1677c
commit 96b10c9148
15 changed files with 299 additions and 373 deletions
+3 -2
View File
@@ -8,6 +8,7 @@ import { api, http, toUserError } from "@/lib/api";
import { MAX_HEARTS, clampHearts, hasInfiniteHearts } from "@/lib/hearts"; import { MAX_HEARTS, clampHearts, hasInfiniteHearts } from "@/lib/hearts";
import type { Mentor, User } from "@/lib/types"; import type { Mentor, User } from "@/lib/types";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { toast } from "@/components/providers"; import { toast } from "@/components/providers";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -116,14 +117,14 @@ export default function LockoutPage() {
</Card> </Card>
<div className="mt-5 w-full space-y-2"> <div className="mt-5 w-full space-y-2">
<textarea <Textarea
value={note} value={note}
onChange={(e) => setNote(e.target.value)} onChange={(e) => setNote(e.target.value)}
maxLength={500} maxLength={500}
rows={3} rows={3}
dir="rtl" dir="rtl"
placeholder="اختیاری: چه آزمونی گیر کردید؟ چه کمکی لازم دارید؟" placeholder="اختیاری: چه آزمونی گیر کردید؟ چه کمکی لازم دارید؟"
className="w-full rounded-xl border-2 border-border bg-background px-3 py-2 text-sm outline-none focus-visible:border-primary/50" className="min-h-0"
/> />
<div className="flex flex-col gap-2 sm:flex-row"> <div className="flex flex-col gap-2 sm:flex-row">
<Button className="flex-1" disabled={busy || requested} onClick={() => void requestUnlock()}> <Button className="flex-1" disabled={busy || requested} onClick={() => void requestUnlock()}>
+1 -1
View File
@@ -48,7 +48,7 @@ export default function NotificationsPage() {
<Skeleton className="h-8 w-44" /> <Skeleton className="h-8 w-44" />
<Skeleton className="h-4 w-72" /> <Skeleton className="h-4 w-72" />
</div> </div>
<div className="rounded-2xl border border-border/70 bg-card p-6 shadow-soft"> <div className="rounded-2xl border-2 border-border/70 bg-card p-6 shadow-soft">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Skeleton className="h-10 w-10 rounded-xl" /> <Skeleton className="h-10 w-10 rounded-xl" />
<div className="flex-1 space-y-1.5"> <div className="flex-1 space-y-1.5">
@@ -19,9 +19,9 @@ import {
} from "lucide-react"; } from "lucide-react";
import { http, toUserError } from "@/lib/api"; import { http, toUserError } from "@/lib/api";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -158,16 +158,10 @@ export function AnnouncePanel() {
return ( return (
<> <>
<div className="flex flex-col gap-6"> {/* Composer + Live Preview */}
<div data-announce-compose className="grid items-start gap-4 lg:grid-cols-[minmax(0,1fr)_20rem]"> <div className="grid items-start gap-6 lg:grid-cols-[minmax(0,1fr)_18rem]">
<Card> {/* Form */}
<CardHeader className="space-y-1"> <div className="space-y-5">
<CardTitle className="text-base">نگارش اعلان</CardTitle>
<p className="text-xs text-muted-foreground">
به همه هنرجویان فعال می‌رسد و در مرکز اعلان‌ها دیده می‌شود.
</p>
</CardHeader>
<CardContent className="space-y-5">
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-sm font-bold">عنوان</Label> <Label className="text-sm font-bold">عنوان</Label>
<Input <Input
@@ -184,15 +178,12 @@ export function AnnouncePanel() {
<Label className="text-sm font-bold">پیام</Label> <Label className="text-sm font-bold">پیام</Label>
<span className="text-[11px] tabular-nums text-muted-foreground">{body.length}/۴۰۰۰</span> <span className="text-[11px] tabular-nums text-muted-foreground">{body.length}/۴۰۰۰</span>
</div> </div>
<textarea <Textarea
value={body} value={body}
onChange={(e) => setBody(e.target.value.slice(0, 4000))} onChange={(e) => setBody(e.target.value.slice(0, 4000))}
rows={5} rows={5}
maxLength={4000} maxLength={4000}
className={cn( className={cn("resize-y", !bodyOk && body.length > 0 ? "border-destructive/50" : "")}
"w-full resize-y rounded-xl border bg-background px-4 py-3 text-sm leading-relaxed focus:outline-none focus:ring-2 focus:ring-primary",
!bodyOk && body.length > 0 ? "border-destructive/50" : "border-input"
)}
placeholder="یک جملهٔ واضح و کوتاه — لینک یا زمان را حتماً بگویید…" placeholder="یک جملهٔ واضح و کوتاه — لینک یا زمان را حتماً بگویید…"
/> />
{body.trim().length > 0 && body.trim().length < 8 && ( {body.trim().length > 0 && body.trim().length < 8 && (
@@ -236,9 +227,9 @@ export function AnnouncePanel() {
<div className="space-y-2"> <div className="space-y-2">
<Label className="flex h-5 items-center gap-1.5 text-sm font-bold"> <Label className="flex h-5 items-center gap-1.5 text-sm font-bold">
<Route className="h-3.5 w-3.5" /> با زدن اعلان این صفحه باز می‌شود <Route className="h-3.5 w-3.5" /> صفحه‌ای که با کلیک باز می‌شود
</Label> </Label>
<p className="text-[11px] text-muted-foreground">خالی بگذارید تا اعلان فقط خوانده شود و صفحه‌ای باز نکند.</p> <p className="text-[11px] text-muted-foreground">خالی بگذارید تا اعلان فقط خوانده شود.</p>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{ROUTES.map((r) => ( {ROUTES.map((r) => (
<button <button
@@ -277,68 +268,65 @@ export function AnnouncePanel() {
onClick={() => setConfirmOpen(true)} onClick={() => setConfirmOpen(true)}
> >
<Eye className="h-4 w-4" /> <Eye className="h-4 w-4" />
پیش‌نمایش و ارسال {editingId ? "پیش‌نمایش و ذخیره" : "پیش‌نمایش و ارسال"}
</Button> </Button>
{editingId && (
<Button
size="lg"
variant="outline"
onClick={() => { setEditingId(null); setTitle(""); setBody(""); setPinned(false); }}
>
انصراف از ویرایش
</Button>
)}
{!valid && ( {!valid && (
<p className="text-xs text-muted-foreground">عنوان و پیام را کامل کنید تا ادامه فعال شود.</p> <p className="text-xs text-muted-foreground">عنوان و پیام را کامل کنید تا ادامه فعال شود.</p>
)} )}
</div> </div>
</CardContent> </div>
</Card>
<div className="space-y-4"> {/* Sidebar: live preview + last sent */}
<Card className="border bg-muted/20"> <div className="space-y-4 lg:sticky lg:top-24">
<CardHeader className="space-y-1 pb-3"> <div className="space-y-1.5">
<CardTitle className="flex h-6 items-center gap-2 text-sm"> <p className="flex items-center gap-1.5 text-xs font-bold text-muted-foreground">
<Eye className="h-3.5 w-3.5" /> پیش‌نمایش زنده <Eye className="h-3.5 w-3.5" /> پیش‌نمایش زنده
</CardTitle> </p>
<p className="text-[11px] text-muted-foreground">هم‌زمان با تایپ به‌روز می‌شود.</p>
</CardHeader>
<CardContent>
<PreviewCard title={title} body={body} category={category} route={route} /> <PreviewCard title={title} body={body} category={category} route={route} />
</CardContent> </div>
</Card>
{lastSent && ( {lastSent && (
<Card className="border border-success/40 bg-success/5"> <div className="rounded-xl border border-success/40 bg-success/5 px-4 py-3 text-sm">
<CardContent className="space-y-1 pt-5 text-sm">
<p className="font-bold text-success">آخرین ارسال موفق</p> <p className="font-bold text-success">آخرین ارسال موفق</p>
<p className="truncate font-extrabold">{lastSent.title}</p> <p className="mt-0.5 truncate font-extrabold">{lastSent.title}</p>
<p className="text-xs text-muted-foreground">{lastSent.recipients} گیرنده</p> <p className="text-xs text-muted-foreground">{lastSent.recipients} گیرنده</p>
</CardContent> </div>
</Card>
)} )}
</div> </div>
</div> </div>
<Card data-announce-list className="rounded-2xl border border-border"> {/* History */}
<CardHeader> <div className="mt-8 space-y-3 border-t border-border/70 pt-6">
<CardTitle className="text-base">اطلاعیه‌های ثبت‌شده</CardTitle> <h3 className="text-sm font-black text-muted-foreground">اطلاعیه‌های ثبت‌شده</h3>
</CardHeader>
<CardContent>
{(listQ.data?.announcements ?? []).length > 0 ? ( {(listQ.data?.announcements ?? []).length > 0 ? (
<ul className="flex list-none flex-col gap-3 p-0"> <ul className="flex list-none flex-col gap-2.5 p-0">
{(listQ.data?.announcements ?? []).map((a) => { {(listQ.data?.announcements ?? []).map((a) => {
const cat = CATEGORIES.find((c) => c.id === a.category); const cat = CATEGORIES.find((c) => c.id === a.category);
const excerpt = announcementExcerpt(a.body); const excerpt = announcementExcerpt(a.body);
return ( return (
<li key={a.id} className="rounded-2xl border border-border bg-background p-4"> <li key={a.id} className="flex flex-col gap-3 rounded-2xl border border-border bg-background p-4 sm:flex-row sm:items-start sm:justify-between">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="min-w-0 flex-1 space-y-2"> <div className="min-w-0 flex-1 space-y-2">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<p className="min-w-0 text-sm font-bold leading-snug">{a.title}</p> <p className="min-w-0 text-sm font-bold leading-snug">{a.title}</p>
<Badge variant="secondary" className="text-[10px]"> <Badge variant="secondary" className="text-[10px]">
{cat?.label ?? a.category} {cat?.label ?? a.category}
</Badge> </Badge>
{a.pinned ? ( {a.pinned && (
<Badge variant="gold" className="text-[10px]"> <Badge variant="gold" className="text-[10px]">سنجاق</Badge>
سنجاق )}
</Badge>
) : null}
</div> </div>
{excerpt ? ( {excerpt && (
<p className="line-clamp-2 text-xs leading-relaxed text-muted-foreground">{excerpt}</p> <p className="line-clamp-2 text-xs leading-relaxed text-muted-foreground">{excerpt}</p>
) : null} )}
</div> </div>
<div className="flex shrink-0 items-center gap-2"> <div className="flex shrink-0 items-center gap-2">
<Button <Button
@@ -352,6 +340,7 @@ export function AnnouncePanel() {
setCategory(a.category); setCategory(a.category);
setRoute(a.route); setRoute(a.route);
setPinned(a.pinned); setPinned(a.pinned);
window.scrollTo({ top: 0, behavior: "smooth" });
}} }}
> >
ویرایش ویرایش
@@ -371,7 +360,6 @@ export function AnnouncePanel() {
<Trash2 className="h-3.5 w-3.5" /> <Trash2 className="h-3.5 w-3.5" />
</Button> </Button>
</div> </div>
</div>
</li> </li>
); );
})} })}
@@ -381,8 +369,6 @@ export function AnnouncePanel() {
هنوز اطلاعیه‌ای ثبت نشده است. هنوز اطلاعیه‌ای ثبت نشده است.
</p> </p>
) : null} ) : null}
</CardContent>
</Card>
</div> </div>
<Dialog open={confirmOpen} onOpenChange={setConfirmOpen}> <Dialog open={confirmOpen} onOpenChange={setConfirmOpen}>
@@ -5,9 +5,9 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Plus, Trash2, Trophy } from "lucide-react"; import { Plus, Trash2, Trophy } from "lucide-react";
import { http, toUserError } from "@/lib/api"; import { http, toUserError } from "@/lib/api";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
@@ -86,20 +86,7 @@ export function ChallengesPanel() {
minute: "2-digit", minute: "2-digit",
}).format(new Date(iso)); }).format(new Date(iso));
return ( const newChallenge = () =>
<>
{dialog}
<Card>
<CardHeader className="flex-row items-center justify-between gap-4">
<div>
<CardTitle className="text-base">چالش‌های امتیاز</CardTitle>
<p className="mt-1 text-xs text-muted-foreground">
چالش ماهانه با هدف امتیاز؛ پیشرفت هنرجو با تکمیل آزمون‌ها به‌روز می‌شود.
</p>
</div>
<Button
className="gap-1.5"
onClick={() =>
setModal({ setModal({
id: 0, id: 0,
title: "", title: "",
@@ -109,13 +96,21 @@ export function ChallengesPanel() {
endsAt: "", endsAt: "",
isActive: true, isActive: true,
createdAt: "", createdAt: "",
}) });
}
> return (
<>
{dialog}
<div className="space-y-4">
<div className="flex items-center justify-between gap-4">
<p className="text-xs text-muted-foreground">
پیشرفت هنرجو با تکمیل آزمون‌ها به‌روز می‌شود.
</p>
<Button className="shrink-0 gap-1.5" onClick={newChallenge}>
<Plus className="h-4 w-4" /> چالش جدید <Plus className="h-4 w-4" /> چالش جدید
</Button> </Button>
</CardHeader> </div>
<CardContent>
{isLoading && ( {isLoading && (
<div className="space-y-3"> <div className="space-y-3">
{[0, 1].map((i) => ( {[0, 1].map((i) => (
@@ -124,8 +119,8 @@ export function ChallengesPanel() {
</div> </div>
)} )}
{isError && ( {isError && (
<div className="rounded-md border border-destructive/30 bg-destructive/5 p-4 text-sm"> <div className="rounded-md border-2 border-destructive/30 bg-destructive/5 p-4 text-sm">
<p className="font-medium text-destructive">{toUserError(error)}</p> <p className="font-bold text-destructive">{toUserError(error)}</p>
<Button size="sm" variant="outline" className="mt-2" onClick={() => refetch()}> <Button size="sm" variant="outline" className="mt-2" onClick={() => refetch()}>
تلاش دوباره تلاش دوباره
</Button> </Button>
@@ -133,7 +128,7 @@ export function ChallengesPanel() {
)} )}
<div className="grid gap-3 sm:grid-cols-2"> <div className="grid gap-3 sm:grid-cols-2">
{(data?.challenges ?? []).map((c) => ( {(data?.challenges ?? []).map((c) => (
<div key={c.id} className="rounded-md border border-border p-4"> <div key={c.id} className="rounded-xl border border-border p-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Trophy className="h-4 w-4 text-gold" /> <Trophy className="h-4 w-4 text-gold" />
<Badge variant={c.isActive ? "success" : "secondary"}>{c.isActive ? "فعال" : "خاموش"}</Badge> <Badge variant={c.isActive ? "success" : "secondary"}>{c.isActive ? "فعال" : "خاموش"}</Badge>
@@ -147,7 +142,7 @@ export function ChallengesPanel() {
</Button> </Button>
</div> </div>
</div> </div>
<p className="mt-2 font-medium">{c.title}</p> <p className="mt-2 font-bold">{c.title}</p>
<p className="line-clamp-2 text-xs text-muted-foreground">{c.description || "—"}</p> <p className="line-clamp-2 text-xs text-muted-foreground">{c.description || "—"}</p>
<p className="mt-1.5 text-xs text-muted-foreground"> <p className="mt-1.5 text-xs text-muted-foreground">
{fmt(c.startsAt)} تا {fmt(c.endsAt)} {fmt(c.startsAt)} تا {fmt(c.endsAt)}
@@ -156,11 +151,12 @@ export function ChallengesPanel() {
))} ))}
</div> </div>
{!isLoading && !isError && (data?.challenges ?? []).length === 0 && ( {!isLoading && !isError && (data?.challenges ?? []).length === 0 && (
<p className="py-8 text-center text-sm text-muted-foreground">هنوز چالشی نیست.</p> <p className="rounded-2xl border border-dashed border-border px-4 py-6 text-center text-sm text-muted-foreground">
هنوز چالشی نیست.
</p>
)} )}
</CardContent> </div>
<ChallengeDialog challenge={modal} onClose={() => setModal(null)} onSave={save} /> <ChallengeDialog challenge={modal} onClose={() => setModal(null)} onSave={save} />
</Card>
</> </>
); );
} }
@@ -199,11 +195,11 @@ function ChallengeDialog({
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label>توضیحات</Label> <Label>توضیحات</Label>
<textarea <Textarea
value={draft.description} value={draft.description}
onChange={(e) => set({ description: e.target.value })} onChange={(e) => set({ description: e.target.value })}
rows={3} rows={3}
className="w-full resize-y rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary" className="resize-y"
placeholder="هدف و انگیزهٔ چالش…" placeholder="هدف و انگیزهٔ چالش…"
/> />
</div> </div>
@@ -6,7 +6,6 @@ import { Loader2 } from "lucide-react";
import { http, toUserError } from "@/lib/api"; import { http, toUserError } from "@/lib/api";
import type { CertificatePhysicalOrder, PhysicalCertSettings, PhysicalOrderStatus } from "@/lib/types"; import type { CertificatePhysicalOrder, PhysicalCertSettings, PhysicalOrderStatus } from "@/lib/types";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { toast } from "@/components/providers"; import { toast } from "@/components/providers";
@@ -75,16 +74,16 @@ export function PhysicalOrdersPanel() {
}; };
return ( return (
<Card> <div className="space-y-4">
<CardHeader className="flex flex-row flex-wrap items-center justify-between gap-3 space-y-0"> <div className="flex flex-wrap items-center justify-between gap-3">
<CardTitle className="text-base">فهرست سفارش‌ها</CardTitle> <p className="text-xs text-muted-foreground">پیگیری درخواست و ارسال گواهینامه فیزیکی.</p>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<select <select
className="h-10 rounded-lg border border-input bg-card px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring" className="h-10 rounded-lg border border-input bg-card px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
value={statusFilter} value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)} onChange={(e) => setStatusFilter(e.target.value)}
> >
<option value="">همه</option> <option value="">همه وضعیت‌ها</option>
<option value="requested">درخواست‌شده</option> <option value="requested">درخواست‌شده</option>
<option value="paid">پرداخت‌شده</option> <option value="paid">پرداخت‌شده</option>
<option value="shipped">ارسال‌شده</option> <option value="shipped">ارسال‌شده</option>
@@ -97,15 +96,15 @@ export function PhysicalOrdersPanel() {
</span> </span>
)} )}
</div> </div>
</CardHeader> </div>
<CardContent className="space-y-2"> <div className="space-y-2">
{isLoading && ( {isLoading && (
<div className="flex justify-center py-8"> <div className="flex justify-center py-8">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" /> <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
</div> </div>
)} )}
{(data?.orders ?? []).map((o) => ( {(data?.orders ?? []).map((o) => (
<div key={o.id} className="space-y-2 rounded-md border border-border px-3 py-3"> <div key={o.id} className="space-y-2 rounded-xl border border-border px-3 py-3">
<div className="flex flex-wrap items-center justify-between gap-2"> <div className="flex flex-wrap items-center justify-between gap-2">
<div> <div>
<p className="text-sm font-medium">{o.userName ?? `کاربر ${o.userId}`}</p> <p className="text-sm font-medium">{o.userName ?? `کاربر ${o.userId}`}</p>
@@ -167,7 +166,9 @@ export function PhysicalOrdersPanel() {
</div> </div>
))} ))}
{!isLoading && (data?.orders ?? []).length === 0 && ( {!isLoading && (data?.orders ?? []).length === 0 && (
<p className="py-8 text-center text-sm text-muted-foreground">سفارشی نیست.</p> <p className="rounded-2xl border border-dashed border-border px-4 py-6 text-center text-sm text-muted-foreground">
سفارشی نیست.
</p>
)} )}
<ListPagination <ListPagination
page={page} page={page}
@@ -175,7 +176,7 @@ export function PhysicalOrdersPanel() {
total={data?.total ?? 0} total={data?.total ?? 0}
onPageChange={setPage} onPageChange={setPage}
/> />
</CardContent> </div>
</Card> </div>
); );
} }
@@ -238,7 +238,7 @@ function AccountFact({
{icon} {icon}
{label} {label}
</dt> </dt>
<dd className={cn("mt-1.5 text-sm font-semibold leading-relaxed [overflow-wrap:anywhere]", ltr && "font-latin")} dir={ltr ? "ltr" : undefined}> <dd className={cn("mt-1.5 text-sm font-bold leading-relaxed [overflow-wrap:anywhere]", ltr && "font-latin")} dir={ltr ? "ltr" : undefined}>
{value} {value}
</dd> </dd>
</div> </div>
@@ -10,6 +10,7 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { import {
Dialog, Dialog,
@@ -338,15 +339,15 @@ function QuestionDialog({
<div className="space-y-5"> <div className="space-y-5">
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-sm font-bold">متن سوال</Label> <Label className="text-sm font-bold">متن سوال</Label>
<textarea <Textarea
value={draft.question} value={draft.question}
onChange={(e) => set({ question: e.target.value })} onChange={(e) => set({ question: e.target.value })}
rows={3} rows={3}
autoFocus autoFocus
placeholder="مثلاً نتیجهٔ اجرای git status چیست؟" placeholder="مثلاً نتیجهٔ اجرای git status چیست؟"
className={cn( className={cn(
"w-full resize-y rounded-xl border bg-background px-4 py-3 text-base font-medium leading-relaxed focus:outline-none focus:ring-2 focus:ring-primary", "resize-y",
questionOk ? "border-input" : "border-destructive/40" questionOk ? "" : "border-destructive/40"
)} )}
/> />
{!questionOk && ( {!questionOk && (
@@ -485,7 +486,7 @@ function QuestionDialog({
> >
<span <span
className={cn( className={cn(
"flex h-7 min-w-7 shrink-0 items-center justify-center rounded-md border px-1.5 text-[11px] font-semibold", "flex h-7 min-w-7 shrink-0 items-center justify-center rounded-md border px-1.5 text-[11px] font-bold",
isAns isAns
? "border-primary bg-primary text-primary-foreground" ? "border-primary bg-primary text-primary-foreground"
: "border-border text-muted-foreground" : "border-border text-muted-foreground"
+1 -1
View File
@@ -27,7 +27,7 @@ export function EmptyState({
</span> </span>
)} )}
<div className="space-y-1 px-6"> <div className="space-y-1 px-6">
<p className="text-sm font-semibold">{title}</p> <p className="text-sm font-bold">{title}</p>
{description && <p className="text-xs leading-relaxed text-muted-foreground sm:text-sm">{description}</p>} {description && <p className="text-xs leading-relaxed text-muted-foreground sm:text-sm">{description}</p>}
</div> </div>
{action && <div className="mt-1">{action}</div>} {action && <div className="mt-1">{action}</div>}
-65
View File
@@ -1,65 +0,0 @@
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { Search } from "lucide-react";
import { http } from "@/lib/api";
type Hit = { kind: string; title: string; href: string };
export function GlobalSearch() {
const router = useRouter();
const [q, setQ] = useState("");
const [hits, setHits] = useState<Hit[]>([]);
const [open, setOpen] = useState(false);
useEffect(() => {
const query = q.trim();
if (query.length < 2) {
setHits([]);
return;
}
const t = setTimeout(() => {
void http
.get<{ results: Hit[] }>(`/api/search?q=${encodeURIComponent(query)}`)
.then((res) => setHits(res.results ?? []))
.catch(() => setHits([]));
}, 250);
return () => clearTimeout(t);
}, [q]);
return (
<div className="relative min-w-0 w-28 sm:w-56">
<Search className="pointer-events-none absolute start-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
<input
value={q}
onChange={(e) => {
setQ(e.target.value);
setOpen(true);
}}
onFocus={() => setOpen(true)}
placeholder="جستجو"
className="h-9 w-full rounded-full border border-border bg-background/70 ps-8 pe-3 text-sm outline-none focus:border-primary"
/>
{open && hits.length > 0 && (
<div className="absolute z-50 mt-1 w-full overflow-hidden rounded-xl border bg-popover shadow-lg">
{hits.map((h) => (
<button
key={h.kind + h.href + h.title}
type="button"
className="block w-full px-3 py-2 text-start text-sm hover:bg-muted"
onClick={() => {
setOpen(false);
setQ("");
router.push(h.href);
}}
>
<span className="text-[10px] text-muted-foreground">{h.kind}</span>
<span className="block truncate font-bold">{h.title}</span>
</button>
))}
</div>
)}
</div>
);
}
@@ -64,7 +64,7 @@ export function NotificationBell() {
<div dir="rtl" className="text-start"> <div dir="rtl" className="text-start">
<div className="flex items-center justify-between gap-3 border-b border-border px-3 py-2.5"> <div className="flex items-center justify-between gap-3 border-b border-border px-3 py-2.5">
<div className="min-w-0"> <div className="min-w-0">
<p className="text-sm font-semibold">اعلان‌ها</p> <p className="text-sm font-bold">اعلان‌ها</p>
<p className="text-[11px] text-muted-foreground"> <p className="text-[11px] text-muted-foreground">
{unread > 0 ? `${unread} خوانده‌نشده` : "همه خوانده شده"} {unread > 0 ? `${unread} خوانده‌نشده` : "همه خوانده شده"}
</p> </p>
@@ -133,7 +133,7 @@ export function NotificationBell() {
<button <button
type="button" type="button"
onClick={() => router.push("/notifications")} onClick={() => router.push("/notifications")}
className="w-full border-t border-border bg-muted/40 py-2.5 text-xs font-semibold text-primary hover:bg-muted" className="w-full border-t border-border bg-muted/40 py-2.5 text-xs font-bold text-primary hover:bg-muted"
> >
مشاهده همه اعلان‌ها مشاهده همه اعلان‌ها
</button> </button>
+5 -5
View File
@@ -119,7 +119,7 @@ export function QuizExperience({ lessonId }: { lessonId: number }) {
return ( return (
<Card className="mx-auto max-w-md p-8 text-center"> <Card className="mx-auto max-w-md p-8 text-center">
<Lock className="mx-auto mb-3 h-10 w-10 text-destructive" /> <Lock className="mx-auto mb-3 h-10 w-10 text-destructive" />
<h2 className="mb-2 text-lg font-semibold">آزمون در دسترس نیست</h2> <h2 className="mb-2 text-lg font-bold">آزمون در دسترس نیست</h2>
<p className="mb-4 text-sm text-muted-foreground">{msg}</p> <p className="mb-4 text-sm text-muted-foreground">{msg}</p>
<Button onClick={() => router.push(`/player/${lessonId}`)}>ابتدا درس را تماشا کنید</Button> <Button onClick={() => router.push(`/player/${lessonId}`)}>ابتدا درس را تماشا کنید</Button>
</Card> </Card>
@@ -188,7 +188,7 @@ export function QuizExperience({ lessonId }: { lessonId: number }) {
<Badge variant="secondary" className="h-6 w-6 justify-center rounded-md p-0"> <Badge variant="secondary" className="h-6 w-6 justify-center rounded-md p-0">
{qi + 1} {qi + 1}
</Badge> </Badge>
<p className="font-medium leading-snug">{q.question}</p> <p className="font-bold leading-snug">{q.question}</p>
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
{q.options.map((opt, oi) => { {q.options.map((opt, oi) => {
@@ -198,7 +198,7 @@ export function QuizExperience({ lessonId }: { lessonId: number }) {
key={oi} key={oi}
onClick={() => select(q.id, oi)} onClick={() => select(q.id, oi)}
className={cn( className={cn(
"flex items-center gap-3 rounded-md border px-4 py-3 text-right text-sm transition-colors", "flex items-center gap-3 rounded-md border-2 px-4 py-3 text-right text-sm transition-colors",
active active
? "border-primary bg-primary/10 ring-1 ring-primary" ? "border-primary bg-primary/10 ring-1 ring-primary"
: "border-border hover:bg-muted/50" : "border-border hover:bg-muted/50"
@@ -206,7 +206,7 @@ export function QuizExperience({ lessonId }: { lessonId: number }) {
> >
<span <span
className={cn( className={cn(
"flex h-7 min-w-7 shrink-0 items-center justify-center rounded-md border px-1.5 text-[11px] font-semibold", "flex h-7 min-w-7 shrink-0 items-center justify-center rounded-md border px-1.5 text-[11px] font-bold",
active ? "border-primary bg-primary text-primary-foreground" : "border-border text-muted-foreground" active ? "border-primary bg-primary text-primary-foreground" : "border-border text-muted-foreground"
)} )}
> >
@@ -305,7 +305,7 @@ function HeartsMeterInline({ hearts, infinite }: { hearts: number; infinite?: bo
function PassScreen({ result, onNext, onLeagues }: { result: QuizResult; onNext: () => void; onLeagues: () => void }) { function PassScreen({ result, onNext, onLeagues }: { result: QuizResult; onNext: () => void; onLeagues: () => void }) {
return ( return (
<div className="mx-auto max-w-md rounded-lg border border-border bg-card p-10 text-center"> <div className="mx-auto max-w-md rounded-2xl border-2 border-border bg-card p-10 text-center">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-lg bg-primary"> <div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-lg bg-primary">
<PartyPopper className="h-9 w-9 text-primary-foreground" /> <PartyPopper className="h-9 w-9 text-primary-foreground" />
</div> </div>
+9 -4
View File
@@ -103,21 +103,26 @@ export function SectionPanel({
titleId, titleId,
title, title,
description, description,
actions,
children, children,
}: { }: {
id?: string; id?: string;
titleId: string; titleId: string;
title: string; title: string;
description: string; description: string;
actions?: ReactNode;
children: ReactNode; children: ReactNode;
}) { }) {
return ( return (
<section id={id} aria-labelledby={titleId} className="min-w-0 rounded-2xl border-2 border-border bg-card"> <section id={id} aria-labelledby={titleId} className="min-w-0 rounded-2xl border-2 border-border bg-card">
<header className="border-b border-border/70 px-5 py-4 sm:px-6"> <header className="border-b border-border/70 px-5 py-4 sm:px-6">
<h2 id={titleId} className="text-lg font-black"> <div className="flex flex-wrap items-start justify-between gap-3">
{title} <div className="min-w-0">
</h2> <h2 id={titleId} className="text-lg font-black">{title}</h2>
<p className="mt-1 text-sm text-muted-foreground">{description}</p> {description && <p className="mt-1 text-sm text-muted-foreground">{description}</p>}
</div>
{actions && <div className="shrink-0">{actions}</div>}
</div>
</header> </header>
<div className="p-5 sm:p-6">{children}</div> <div className="p-5 sm:p-6">{children}</div>
</section> </section>
+1 -1
View File
@@ -24,7 +24,7 @@ CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<div ref={ref} className={cn("text-xl font-semibold leading-none", className)} {...props} /> <div ref={ref} className={cn("text-xl font-bold leading-none", className)} {...props} />
) )
); );
CardTitle.displayName = "CardTitle"; CardTitle.displayName = "CardTitle";
+1 -1
View File
@@ -78,7 +78,7 @@ const DialogTitle = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Title <DialogPrimitive.Title
ref={ref} ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight", className)} className={cn("text-lg font-bold leading-none tracking-tight", className)}
{...props} {...props}
/> />
)); ));
+2 -1
View File
@@ -16,12 +16,13 @@ const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
return ( return (
<div <div
ref={ref} ref={ref}
dir="ltr"
className={cn("relative h-2.5 w-full overflow-hidden rounded-full bg-muted", className)} className={cn("relative h-2.5 w-full overflow-hidden rounded-full bg-muted", className)}
{...props} {...props}
> >
<div <div
className={cn( className={cn(
"h-full rounded-full will-change-[width] rtl:me-auto", "h-full rounded-full will-change-[width]",
gradient ? "bg-primary" : "bg-primary", gradient ? "bg-primary" : "bg-primary",
smooth smooth
? "transition-[width] duration-1000 ease-[cubic-bezier(0.22,1,0.36,1)]" ? "transition-[width] duration-1000 ease-[cubic-bezier(0.22,1,0.36,1)]"