Fix minimum age 12 and ensure assignment catalog seeds on startup.

Persian age validation messages, clearer homework entry points, and seed assignment definitions so «تکالیف من» is not empty after deploy.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ar.badiei
2026-09-22 07:24:40 +03:30
co-authored by Cursor
parent c6243caa35
commit 8c37c15697
12 changed files with 115 additions and 29 deletions
+13
View File
@@ -91,12 +91,25 @@ export function getLoginErrorMessage(error: unknown): string {
function localizeValidationMessage(message: string | undefined): string | null {
if (!message) return null;
const lower = message.toLowerCase();
if (/حداقل سن|سن باید|سن الزامی/.test(message)) {
return message;
}
if (/age must be at least\s*(\d+)/i.test(message)) {
const min = message.match(/age must be at least\s*(\d+)/i)?.[1] ?? '12';
return `حداقل سن ${min} سال است`;
}
if (/age must be an integer|age is required/i.test(message)) {
return 'سن باید یک عدد صحیح باشد';
}
if (lower.includes('email')) {
return 'فرمت ایمیل نامعتبر است.';
}
if (lower.includes('password')) {
return 'رمز عبور نامعتبر است.';
}
if (/^validation failed$/i.test(message.trim())) {
return 'اطلاعات واردشده نامعتبر است.';
}
return localizeKnownEnglishMessage(message);
}
+16 -1
View File
@@ -6,12 +6,13 @@ import { PROGRAM_SESSION_COUNT } from '@/data/program/sessions';
import { getProgramProgress } from '@/services/programService';
import { Progress } from '@/components/ui/progress';
import { Navigate, useNavigate } from 'react-router-dom';
import { Check, ChevronLeft, Lock } from 'lucide-react';
import { Check, ChevronLeft, Lock, NotebookPen } from 'lucide-react';
import { cn } from '@/lib/utils';
import { StaggeredList, StaggerItem, AnimationWrapper } from '@/components/motion';
import { motion } from 'framer-motion';
import { fastTransition } from '@/lib/motion';
import { CourseStatusSkeleton } from '@/components/skeletons';
import { Button } from '@/components/ui/button';
type StepState = 'open' | 'locked' | 'done';
@@ -85,6 +86,20 @@ export default function AccountPage() {
</span>
</p>
)}
{!isGuest ? (
<Button
type="button"
variant="outline"
className="mt-3 w-full justify-between"
onClick={() => navigate('/assignments')}
>
<span className="inline-flex items-center gap-2">
<NotebookPen className="h-4 w-4 text-sky-600 dark:text-sky-400" />
تکالیف من
</span>
<ChevronLeft className="h-4 w-4 text-muted-foreground" />
</Button>
) : null}
</AnimationWrapper>
<StaggeredList itemCount={3} className="space-y-3" role="region" aria-label="وضعیت دوره">
+15 -8
View File
@@ -8,6 +8,7 @@ import {
fetchAssignmentCatalog,
type AssignmentCatalogItem,
} from '@/services/assignmentService';
import { getBackendErrorMessage } from '@/lib/backendError';
import { AnimationWrapper, StaggerItem, StaggeredList } from '@/components/motion';
import { PageSkeleton } from '@/components/skeletons';
import { Button } from '@/components/ui/button';
@@ -40,7 +41,7 @@ export default function AssignmentsPage() {
const rows = await fetchAssignmentCatalog();
setItems(rows);
} catch (err) {
setError(err instanceof Error ? err.message : 'بارگذاری تکالیف ناموفق بود');
setError(getBackendErrorMessage(err, 'بارگذاری تکالیف ناموفق بود'));
setItems([]);
} finally {
setLoading(false);
@@ -109,14 +110,20 @@ export default function AssignmentsPage() {
</Button>
</div>
) : items.length === 0 ? (
<p className="rounded-xl border border-dashed border-slate-300 p-6 text-center text-sm text-muted-foreground dark:border-slate-600">
هنوز تکلیفی تعریف نشده است.
</p>
<div className="rounded-xl border border-dashed border-slate-300 p-6 text-center text-sm text-muted-foreground dark:border-slate-600">
<p>فهرست تکالیف هنوز از سرور بارگذاری نشده است.</p>
<p className="mt-2 text-xs">
پس از تکمیل هر یک از جلسات ۲ تا ۱۰، تکلیف همان جلسه اینجا باز می‌شود.
</p>
<Button variant="link" className="mt-2 px-0" onClick={() => void load()}>
تلاش مجدد
</Button>
</div>
) : (
<StaggeredList itemCount={items.length} className="space-y-3">
{items.map((item) => {
const ua = item.userAssignment;
const unlocked = item.unlocked && ua;
const isOpen = item.unlocked && Boolean(ua);
const completed = ua?.status === 'COMPLETED';
const content = (
@@ -130,7 +137,7 @@ export default function AssignmentsPage() {
{item.definition.title}
</h2>
</div>
{unlocked ? (
{isOpen ? (
<ChevronLeft className="mt-1 h-4 w-4 shrink-0 text-muted-foreground" />
) : (
<Lock className="mt-1 h-4 w-4 shrink-0 text-muted-foreground" />
@@ -143,7 +150,7 @@ export default function AssignmentsPage() {
<span className="rounded-md bg-slate-100 px-2 py-0.5 dark:bg-slate-700">
{assignmentTypeLabel(item.definition.assignmentType)}
</span>
{unlocked && ua ? (
{isOpen && ua ? (
<>
<span
className={cn(
@@ -169,7 +176,7 @@ export default function AssignmentsPage() {
return (
<StaggerItem key={item.definition.id}>
{unlocked ? (
{isOpen && ua ? (
<Link
to={`/assignments/${ua.id}`}
className="block rounded-xl border border-slate-200/80 bg-white p-4 shadow-sm transition hover:border-sky-300 dark:border-slate-700 dark:bg-slate-800 dark:hover:border-sky-600"
+10 -2
View File
@@ -144,7 +144,7 @@ export default function ProfileCompletionPage() {
id="profile-age"
type="text"
inputMode="numeric"
placeholder="سن"
placeholder={`حداقل ${PROFILE_AGE_MIN} سال`}
value={age}
onChange={(event) => {
setAge(event.target.value);
@@ -152,9 +152,17 @@ export default function ProfileCompletionPage() {
}}
disabled={isLoading}
autoComplete="off"
min={PROFILE_AGE_MIN}
aria-invalid={Boolean(fieldError.age)}
aria-describedby="profile-age-hint"
/>
{fieldError.age ? <p className="text-xs text-red-400">{fieldError.age}</p> : null}
{fieldError.age ? (
<p className="text-xs text-red-400">{fieldError.age}</p>
) : (
<p id="profile-age-hint" className="text-xs text-muted-foreground">
حداقل سن ورود {PROFILE_AGE_MIN} سال است
</p>
)}
</div>
<Button type="submit" variant="neon" className="w-full" disabled={isLoading}>