io
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -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" && (
|
||||
|
||||
166
components/PersianDateField.tsx
Normal file
166
components/PersianDateField.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
66
components/PersianTimeField.tsx
Normal file
66
components/PersianTimeField.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user