This commit is contained in:
2026-05-03 17:01:46 +03:30
parent b5ad5420b2
commit 9c30295b4b
76 changed files with 7891 additions and 461 deletions

View File

@@ -2,7 +2,7 @@
flagImage?: string | null;
flagEmoji?: string | null;
countryName: string;
size?: 'sm' | 'md' | 'lg';
size?: 'sm' | 'md' | 'lg' | 'xl';
}
export default function CountryFlag({
@@ -15,6 +15,7 @@ export default function CountryFlag({
sm: 'text-lg',
md: 'text-2xl',
lg: 'text-4xl',
xl: 'text-6xl',
};
return (

View File

@@ -18,6 +18,8 @@ export default function Navbar() {
{session ? (
<>
<Link href="/team" className="hover:text-green-300 transition">تیم من</Link>
<Link href="/quiz" className="hover:text-green-300 transition">کوییز</Link>
<Link href="/golden-cards" className="hover:text-green-300 transition">کارت ویژه</Link>
<Link href="/shop" className="hover:text-green-300 transition">فروشگاه</Link>
<Link href="/profile" className="hover:text-green-300 transition">پروفایل</Link>
{(session.user as any).role === "ADMIN" && (

View File

@@ -0,0 +1,166 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import DatePicker from "react-multi-date-picker";
import DateObject from "react-date-object";
import persian from "react-date-object/calendars/persian";
import gregorian from "react-date-object/calendars/gregorian";
import persian_fa from "react-date-object/locales/persian_fa";
import gregorian_en from "react-date-object/locales/gregorian_en";
import {
dateValueToJalali,
formatPersianDateTime,
getDateFromJalaliDateTime,
jalaliDateTimeToUtcIso,
jalaliDateToGregorianString,
type JalaliDateParts,
} from "@/lib/persianDate";
type PersianDateFieldProps = {
label: string;
value: string;
onChange: (value: string) => void;
mode?: "date" | "datetime";
required?: boolean;
placeholder?: string;
};
function createDateObject(parts: JalaliDateParts | null) {
if (!parts) return null;
return new DateObject({
calendar: persian,
locale: persian_fa,
year: parts.year,
month: parts.month,
day: parts.day,
});
}
function getInitialTime(value: string, mode: "date" | "datetime") {
const parsedValue = dateValueToJalali(value, mode);
return parsedValue?.time || "12:00";
}
export default function PersianDateField({
label,
value,
onChange,
mode = "date",
required = false,
placeholder = "انتخاب تاریخ شمسی",
}: PersianDateFieldProps) {
const parsedValue = useMemo(() => dateValueToJalali(value, mode), [mode, value]);
const pickerValue = useMemo(() => createDateObject(parsedValue), [parsedValue]);
const [time, setTime] = useState(getInitialTime(value, mode));
useEffect(() => {
setTime(getInitialTime(value, mode));
}, [mode, value]);
function emitChange(nextDate: DateObject | null, nextTime: string) {
if (!nextDate) {
onChange("");
return;
}
const year = Number(nextDate.year);
const month = Number(nextDate.month.number);
const day = Number(nextDate.day);
if (mode === "date") {
onChange(jalaliDateToGregorianString(year, month, day));
return;
}
onChange(jalaliDateTimeToUtcIso(year, month, day, nextTime));
}
const preview = useMemo(() => {
if (!parsedValue) return "";
if (mode === "date") return "";
return formatPersianDateTime(
getDateFromJalaliDateTime(parsedValue.year, parsedValue.month, parsedValue.day, time)
);
}, [mode, parsedValue, time]);
return (
<div className="space-y-2">
<label className="block text-sm font-medium text-slate-700">{label}</label>
<DatePicker
value={pickerValue}
onChange={(selected) => emitChange((selected as DateObject | null) ?? null, time)}
calendar={persian}
locale={persian_fa}
format="YYYY/MM/DD"
calendarPosition="bottom-right"
editable={false}
required={required}
placeholder={placeholder}
inputClass="w-full rounded-2xl border border-slate-200 bg-white px-4 py-3 text-right text-slate-900 shadow-sm outline-none transition focus:border-emerald-400 focus:ring-2 focus:ring-emerald-200"
containerClassName="block w-full"
weekDays={[
["شنبه", "ش"],
["یکشنبه", "ی"],
["دوشنبه", "د"],
["سه‌شنبه", "س"],
["چهارشنبه", "چ"],
["پنجشنبه", "پ"],
["جمعه", "ج"],
]}
/>
{pickerValue && (
<div className="rounded-xl bg-slate-50 px-3 py-2 text-xs text-slate-500">
{mode === "date"
? `تاریخ انتخاب‌شده: ${pickerValue.format("dddd DD MMMM YYYY")}`
: preview}
</div>
)}
{mode === "datetime" && (
<div className="grid grid-cols-2 gap-3">
<div>
<label className="mb-1 block text-xs font-medium text-slate-600">ساعت</label>
<select
value={time.split(":")[0]}
onChange={(event) => {
const nextTime = `${event.target.value}:${time.split(":")[1]}`;
setTime(nextTime);
emitChange(pickerValue, nextTime);
}}
className="w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-slate-900 outline-none transition focus:border-emerald-400 focus:ring-2 focus:ring-emerald-200"
>
{Array.from({ length: 24 }, (_, index) => String(index).padStart(2, "0")).map((hour) => (
<option key={hour} value={hour}>
{new Intl.NumberFormat("fa-IR").format(Number(hour))}
</option>
))}
</select>
</div>
<div>
<label className="mb-1 block text-xs font-medium text-slate-600">دقیقه</label>
<select
value={time.split(":")[1]}
onChange={(event) => {
const nextTime = `${time.split(":")[0]}:${event.target.value}`;
setTime(nextTime);
emitChange(pickerValue, nextTime);
}}
className="w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-slate-900 outline-none transition focus:border-emerald-400 focus:ring-2 focus:ring-emerald-200"
>
{Array.from({ length: 60 }, (_, index) => String(index).padStart(2, "0")).map((minute) => (
<option key={minute} value={minute}>
{new Intl.NumberFormat("fa-IR").format(Number(minute))}
</option>
))}
</select>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,66 @@
"use client";
type PersianTimeFieldProps = {
label: string;
value: string;
onChange: (value: string) => void;
required?: boolean;
};
function pad(value: number) {
return String(value).padStart(2, "0");
}
function toPersianNumber(value: string) {
return new Intl.NumberFormat("fa-IR").format(Number(value));
}
export default function PersianTimeField({
label,
value,
onChange,
required = false,
}: PersianTimeFieldProps) {
const [hour = "12", minute = "00"] = value ? value.split(":") : ["12", "00"];
return (
<div className="space-y-2">
<label className="block text-sm font-medium text-slate-700">{label}</label>
<input
className="pointer-events-none absolute opacity-0"
value={value}
onChange={() => undefined}
required={required}
tabIndex={-1}
/>
<div className="grid grid-cols-[1fr_auto_1fr] items-center gap-2 rounded-2xl border border-slate-200 bg-white px-3 py-3 shadow-sm">
<select
value={hour}
onChange={(event) => onChange(`${event.target.value}:${minute}`)}
className="w-full rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-slate-900 outline-none transition focus:border-cyan-400 focus:ring-2 focus:ring-cyan-200"
>
{Array.from({ length: 24 }, (_, index) => pad(index)).map((item) => (
<option key={item} value={item}>
{toPersianNumber(item)}
</option>
))}
</select>
<span className="text-lg font-bold text-slate-400">:</span>
<select
value={minute}
onChange={(event) => onChange(`${hour}:${event.target.value}`)}
className="w-full rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-slate-900 outline-none transition focus:border-cyan-400 focus:ring-2 focus:ring-cyan-200"
>
{Array.from({ length: 60 }, (_, index) => pad(index)).map((item) => (
<option key={item} value={item}>
{toPersianNumber(item)}
</option>
))}
</select>
</div>
</div>
);
}