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:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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="وضعیت دوره">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user