io
This commit is contained in:
39
lib/cardTier.ts
Normal file
39
lib/cardTier.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { CardTier, DailyQuiz } from "@prisma/client";
|
||||
|
||||
export const CARD_TIER_LABELS: Record<CardTier, string> = {
|
||||
GOLD: "طلایی",
|
||||
SILVER: "نقره ای",
|
||||
BRONZE: "برنزی",
|
||||
};
|
||||
|
||||
export const CARD_TIER_ORDER: CardTier[] = ["GOLD", "SILVER", "BRONZE"];
|
||||
|
||||
export function resolveQuizRewardTier(
|
||||
quiz: Pick<DailyQuiz, "goldMinCorrect" | "silverMinCorrect" | "bronzeMinCorrect">,
|
||||
correctAnswers: number
|
||||
): CardTier | null {
|
||||
if (quiz.goldMinCorrect != null && correctAnswers >= quiz.goldMinCorrect) {
|
||||
return "GOLD";
|
||||
}
|
||||
|
||||
if (quiz.silverMinCorrect != null && correctAnswers >= quiz.silverMinCorrect) {
|
||||
return "SILVER";
|
||||
}
|
||||
|
||||
if (quiz.bronzeMinCorrect != null && correctAnswers >= quiz.bronzeMinCorrect) {
|
||||
return "BRONZE";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getCardTierBadgeClass(cardTier: CardTier) {
|
||||
switch (cardTier) {
|
||||
case "GOLD":
|
||||
return "bg-yellow-100 text-yellow-800";
|
||||
case "SILVER":
|
||||
return "bg-slate-200 text-slate-800";
|
||||
case "BRONZE":
|
||||
return "bg-amber-100 text-amber-800";
|
||||
}
|
||||
}
|
||||
30
lib/db.ts
30
lib/db.ts
@@ -4,10 +4,36 @@ const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined;
|
||||
};
|
||||
|
||||
function getPrismaDatabaseUrl() {
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
if (!databaseUrl) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const url = new URL(databaseUrl);
|
||||
|
||||
// In dev, Next can spin up multiple workers. Keep each Prisma pool small so
|
||||
// the database is not exhausted by parallel hot-reload processes.
|
||||
if (!url.searchParams.has("connection_limit")) {
|
||||
url.searchParams.set("connection_limit", "5");
|
||||
}
|
||||
|
||||
if (!url.searchParams.has("pool_timeout")) {
|
||||
url.searchParams.set("pool_timeout", "20");
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
export const db =
|
||||
globalForPrisma.prisma ??
|
||||
new PrismaClient({
|
||||
log: process.env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
|
||||
log: ["error"],
|
||||
datasources: {
|
||||
db: {
|
||||
url: getPrismaDatabaseUrl(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db;
|
||||
globalForPrisma.prisma = db;
|
||||
|
||||
1443
lib/openapi.ts
Normal file
1443
lib/openapi.ts
Normal file
File diff suppressed because it is too large
Load Diff
405
lib/persianDate.ts
Normal file
405
lib/persianDate.ts
Normal file
@@ -0,0 +1,405 @@
|
||||
import DateObject from "react-date-object";
|
||||
import persian from "react-date-object/calendars/persian";
|
||||
import gregorian from "react-date-object/calendars/gregorian";
|
||||
|
||||
export type JalaliDateParts = {
|
||||
year: number;
|
||||
month: number;
|
||||
day: number;
|
||||
};
|
||||
|
||||
export const TEHRAN_TIMEZONE = "Asia/Tehran";
|
||||
|
||||
const PERSIAN_MONTHS = [
|
||||
"فروردین",
|
||||
"اردیبهشت",
|
||||
"خرداد",
|
||||
"تیر",
|
||||
"مرداد",
|
||||
"شهریور",
|
||||
"مهر",
|
||||
"آبان",
|
||||
"آذر",
|
||||
"دی",
|
||||
"بهمن",
|
||||
"اسفند",
|
||||
];
|
||||
|
||||
const PERSIAN_WEEKDAYS = ["ش", "ی", "د", "س", "چ", "پ", "ج"];
|
||||
|
||||
function div(a: number, b: number) {
|
||||
return Math.floor(a / b);
|
||||
}
|
||||
|
||||
function mod(a: number, b: number) {
|
||||
return a - Math.floor(a / b) * b;
|
||||
}
|
||||
|
||||
function pad(value: number) {
|
||||
return String(value).padStart(2, "0");
|
||||
}
|
||||
|
||||
function createGregorianDateObject(year: number, month: number, day: number) {
|
||||
return new DateObject({
|
||||
calendar: gregorian,
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
});
|
||||
}
|
||||
|
||||
function createPersianDateObject(year: number, month: number, day: number) {
|
||||
return new DateObject({
|
||||
calendar: persian,
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
});
|
||||
}
|
||||
|
||||
function parseGmtOffset(offsetLabel: string) {
|
||||
const normalized = offsetLabel.replace("GMT", "");
|
||||
const sign = normalized.startsWith("-") ? -1 : 1;
|
||||
const [hoursPart, minutesPart = "0"] = normalized.replace(/[+-]/, "").split(":");
|
||||
return sign * (Number(hoursPart) * 60 + Number(minutesPart));
|
||||
}
|
||||
|
||||
function getTimeZoneOffsetMinutes(date: Date, timeZone: string) {
|
||||
const formatter = new Intl.DateTimeFormat("en-US", {
|
||||
timeZone,
|
||||
timeZoneName: "shortOffset",
|
||||
hour: "2-digit",
|
||||
});
|
||||
const offsetPart = formatter.formatToParts(date).find((part) => part.type === "timeZoneName")?.value ?? "GMT+0";
|
||||
return parseGmtOffset(offsetPart);
|
||||
}
|
||||
|
||||
function getTimeZoneDateParts(date: Date, timeZone: string) {
|
||||
const formatter = new Intl.DateTimeFormat("en-CA", {
|
||||
timeZone,
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hourCycle: "h23",
|
||||
});
|
||||
|
||||
const parts = formatter.formatToParts(date);
|
||||
const read = (type: string) => Number(parts.find((part) => part.type === type)?.value ?? "0");
|
||||
|
||||
return {
|
||||
year: read("year"),
|
||||
month: read("month"),
|
||||
day: read("day"),
|
||||
hour: read("hour"),
|
||||
minute: read("minute"),
|
||||
};
|
||||
}
|
||||
|
||||
function zonedDateTimeToUtcDate(
|
||||
year: number,
|
||||
month: number,
|
||||
day: number,
|
||||
hour: number,
|
||||
minute: number,
|
||||
timeZone = TEHRAN_TIMEZONE
|
||||
) {
|
||||
let utcDate = new Date(Date.UTC(year, month - 1, day, hour, minute, 0, 0));
|
||||
|
||||
for (let index = 0; index < 2; index += 1) {
|
||||
const offsetMinutes = getTimeZoneOffsetMinutes(utcDate, timeZone);
|
||||
utcDate = new Date(Date.UTC(year, month - 1, day, hour, minute, 0, 0) - offsetMinutes * 60 * 1000);
|
||||
}
|
||||
|
||||
return utcDate;
|
||||
}
|
||||
|
||||
function jalCal(jy: number) {
|
||||
const breaks = [-61, 9, 38, 199, 426, 686, 756, 818, 1111, 1181, 1210, 1635, 2060, 2097, 2192, 2262, 2324, 2394, 2456, 3178];
|
||||
const bl = breaks.length;
|
||||
|
||||
if (jy < breaks[0] || jy >= breaks[bl - 1]) {
|
||||
throw new Error("Invalid Jalali year");
|
||||
}
|
||||
|
||||
let jump = 0;
|
||||
let leapJ = -14;
|
||||
let jp = breaks[0];
|
||||
|
||||
for (let i = 1; i < bl; i += 1) {
|
||||
const jm = breaks[i];
|
||||
jump = jm - jp;
|
||||
if (jy < jm) {
|
||||
break;
|
||||
}
|
||||
leapJ += div(jump, 33) * 8 + div(mod(jump, 33), 4);
|
||||
jp = jm;
|
||||
}
|
||||
|
||||
let n = jy - jp;
|
||||
leapJ += div(n, 33) * 8 + div(mod(n, 33) + 3, 4);
|
||||
|
||||
if (mod(jump, 33) === 4 && jump - n === 4) {
|
||||
leapJ += 1;
|
||||
}
|
||||
|
||||
const gy = jy + 621;
|
||||
const leapG = div(gy, 4) - div((div(gy, 100) + 1) * 3, 4) - 150;
|
||||
const march = 20 + leapJ - leapG;
|
||||
|
||||
if (jump - n < 6) {
|
||||
n = n - jump + div(jump + 4, 33) * 33;
|
||||
}
|
||||
|
||||
let leap = mod(mod(n + 1, 33) - 1, 4);
|
||||
if (leap === -1) {
|
||||
leap = 4;
|
||||
}
|
||||
|
||||
return { leap, gy, march };
|
||||
}
|
||||
|
||||
function g2d(gy: number, gm: number, gd: number) {
|
||||
let d =
|
||||
div((gy + div(gm - 8, 6) + 100100) * 1461, 4) +
|
||||
div(153 * mod(gm + 9, 12) + 2, 5) +
|
||||
gd -
|
||||
34840408;
|
||||
d = d - div(div(gy + 100100 + div(gm - 8, 6), 100) * 3, 4) + 752;
|
||||
return d;
|
||||
}
|
||||
|
||||
function d2g(jdn: number) {
|
||||
let j = 4 * jdn + 139361631;
|
||||
j = j + div(div(4 * jdn + 183187720, 146097) * 3, 4) * 4 - 3908;
|
||||
const i = div(mod(j, 1461), 4) * 5 + 308;
|
||||
const gd = div(mod(i, 153), 5) + 1;
|
||||
const gm = mod(div(i, 153), 12) + 1;
|
||||
const gy = div(j, 1461) - 100100 + div(8 - gm, 6);
|
||||
return { year: gy, month: gm, day: gd };
|
||||
}
|
||||
|
||||
function j2d(jy: number, jm: number, jd: number) {
|
||||
const r = jalCal(jy);
|
||||
return g2d(r.gy, 3, r.march) + (jm - 1) * 31 - div(jm, 7) * (jm - 7) + jd - 1;
|
||||
}
|
||||
|
||||
function d2j(jdn: number): JalaliDateParts {
|
||||
const g = d2g(jdn);
|
||||
let jy = g.year - 621;
|
||||
const r = jalCal(jy);
|
||||
const jdn1f = g2d(g.year, 3, r.march);
|
||||
let k = jdn - jdn1f;
|
||||
|
||||
if (k >= 0) {
|
||||
if (k <= 185) {
|
||||
return {
|
||||
year: jy,
|
||||
month: 1 + div(k, 31),
|
||||
day: mod(k, 31) + 1,
|
||||
};
|
||||
}
|
||||
k -= 186;
|
||||
} else {
|
||||
jy -= 1;
|
||||
k += 179;
|
||||
if (r.leap === 1) {
|
||||
k += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
year: jy,
|
||||
month: 7 + div(k, 30),
|
||||
day: mod(k, 30) + 1,
|
||||
};
|
||||
}
|
||||
|
||||
export function toJalali(gy: number, gm: number, gd: number) {
|
||||
const converted = createGregorianDateObject(gy, gm, gd).convert(persian);
|
||||
return {
|
||||
year: Number(converted.year),
|
||||
month: Number(converted.month.number),
|
||||
day: Number(converted.day),
|
||||
};
|
||||
}
|
||||
|
||||
export function toGregorian(jy: number, jm: number, jd: number) {
|
||||
const converted = createPersianDateObject(jy, jm, jd).convert(gregorian);
|
||||
return {
|
||||
year: Number(converted.year),
|
||||
month: Number(converted.month.number),
|
||||
day: Number(converted.day),
|
||||
};
|
||||
}
|
||||
|
||||
export function isLeapJalaliYear(year: number) {
|
||||
return createPersianDateObject(year, 1, 1).isLeap;
|
||||
}
|
||||
|
||||
export function getJalaliMonthDays(year: number, month: number) {
|
||||
if (month <= 6) return 31;
|
||||
if (month <= 11) return 30;
|
||||
return isLeapJalaliYear(year) ? 30 : 29;
|
||||
}
|
||||
|
||||
export function getPersianMonthName(month: number) {
|
||||
return PERSIAN_MONTHS[month - 1];
|
||||
}
|
||||
|
||||
export function getPersianWeekdays() {
|
||||
return PERSIAN_WEEKDAYS;
|
||||
}
|
||||
|
||||
export function formatDateOnly(year: number, month: number, day: number) {
|
||||
return `${year}-${pad(month)}-${pad(day)}`;
|
||||
}
|
||||
|
||||
export function parseDateOnly(value: string) {
|
||||
const match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (!match) return null;
|
||||
return { year: Number(match[1]), month: Number(match[2]), day: Number(match[3]) };
|
||||
}
|
||||
|
||||
export function getGregorianDateInputValue(date: Date) {
|
||||
return formatDateOnly(date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate());
|
||||
}
|
||||
|
||||
export function formatGregorianDate(date: Date) {
|
||||
return new Intl.DateTimeFormat("en-GB", {
|
||||
timeZone: TEHRAN_TIMEZONE,
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
export function formatGregorianDateTime(date: Date) {
|
||||
return new Intl.DateTimeFormat("en-GB", {
|
||||
timeZone: TEHRAN_TIMEZONE,
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
export function formatPersianDate(date: Date) {
|
||||
return new Intl.DateTimeFormat("fa-IR-u-ca-persian", {
|
||||
timeZone: TEHRAN_TIMEZONE,
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
export function formatPersianDateTime(date: Date) {
|
||||
return new Intl.DateTimeFormat("fa-IR-u-ca-persian", {
|
||||
timeZone: TEHRAN_TIMEZONE,
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hourCycle: "h23",
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
export function formatPersianTime(date: Date) {
|
||||
return new Intl.DateTimeFormat("fa-IR", {
|
||||
timeZone: TEHRAN_TIMEZONE,
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hourCycle: "h23",
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
export function getTehranTimeInputValue(date: Date) {
|
||||
const parts = getTimeZoneDateParts(date, TEHRAN_TIMEZONE);
|
||||
return `${pad(parts.hour)}:${pad(parts.minute)}`;
|
||||
}
|
||||
|
||||
export function getWeekdayOffset(year: number, month: number, day = 1) {
|
||||
const gregorian = toGregorian(year, month, day);
|
||||
const jsDay = new Date(Date.UTC(gregorian.year, gregorian.month - 1, gregorian.day)).getUTCDay();
|
||||
return (jsDay + 1) % 7;
|
||||
}
|
||||
|
||||
export function getTodayJalali() {
|
||||
const nowParts = getTimeZoneDateParts(new Date(), TEHRAN_TIMEZONE);
|
||||
return toJalali(nowParts.year, nowParts.month, nowParts.day);
|
||||
}
|
||||
|
||||
export function jalaliDateToGregorianString(year: number, month: number, day: number) {
|
||||
const gregorian = toGregorian(year, month, day);
|
||||
return formatDateOnly(gregorian.year, gregorian.month, gregorian.day);
|
||||
}
|
||||
|
||||
export function gregorianDateAndTimeToUtcIso(date: string, time: string) {
|
||||
const gregorian = parseDateOnly(date);
|
||||
if (!gregorian) return "";
|
||||
const [hour, minute] = time.split(":").map(Number);
|
||||
return zonedDateTimeToUtcDate(
|
||||
gregorian.year,
|
||||
gregorian.month,
|
||||
gregorian.day,
|
||||
hour || 0,
|
||||
minute || 0
|
||||
).toISOString();
|
||||
}
|
||||
|
||||
export function jalaliDateTimeToUtcIso(year: number, month: number, day: number, time: string) {
|
||||
const gregorian = toGregorian(year, month, day);
|
||||
const [hour, minute] = time.split(":").map(Number);
|
||||
return zonedDateTimeToUtcDate(
|
||||
gregorian.year,
|
||||
gregorian.month,
|
||||
gregorian.day,
|
||||
hour || 0,
|
||||
minute || 0
|
||||
).toISOString();
|
||||
}
|
||||
|
||||
export function getDateFromJalaliDateTime(year: number, month: number, day: number, time = "12:00") {
|
||||
const gregorian = toGregorian(year, month, day);
|
||||
const [hour, minute] = time.split(":").map(Number);
|
||||
return zonedDateTimeToUtcDate(
|
||||
gregorian.year,
|
||||
gregorian.month,
|
||||
gregorian.day,
|
||||
hour || 0,
|
||||
minute || 0
|
||||
);
|
||||
}
|
||||
|
||||
export function getGregorianDateForDisplay(year: number, month: number, day: number) {
|
||||
return formatGregorianDate(getDateFromJalaliDateTime(year, month, day, "12:00"));
|
||||
}
|
||||
|
||||
export function dateValueToJalali(value: string, mode: "date" | "datetime") {
|
||||
if (!value) return null;
|
||||
|
||||
if (mode === "date") {
|
||||
const parsed = parseDateOnly(value);
|
||||
if (!parsed) return null;
|
||||
return {
|
||||
...toJalali(parsed.year, parsed.month, parsed.day),
|
||||
time: "",
|
||||
};
|
||||
}
|
||||
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return null;
|
||||
|
||||
const tehranParts = getTimeZoneDateParts(date, TEHRAN_TIMEZONE);
|
||||
return {
|
||||
...toJalali(tehranParts.year, tehranParts.month, tehranParts.day),
|
||||
time: `${pad(tehranParts.hour)}:${pad(tehranParts.minute)}`,
|
||||
};
|
||||
}
|
||||
43
lib/specialCards.ts
Normal file
43
lib/specialCards.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { Position, TeamPlayer } from "@prisma/client";
|
||||
import { FORMATIONS } from "@/lib/teamValidation";
|
||||
|
||||
export const SPECIAL_CARD_TEAM_LIMIT = 3;
|
||||
|
||||
export function getSpecialCardSalePrice(price: number) {
|
||||
return Math.round(price * 0.7);
|
||||
}
|
||||
|
||||
export function getPositionLabel(position: Position | string) {
|
||||
switch (position) {
|
||||
case "GK":
|
||||
return "دروازهبان";
|
||||
case "DEF":
|
||||
return "مدافع";
|
||||
case "MID":
|
||||
return "هافبک";
|
||||
case "FWD":
|
||||
return "مهاجم";
|
||||
default:
|
||||
return position;
|
||||
}
|
||||
}
|
||||
|
||||
export function getAutoPlacement(
|
||||
formation: string,
|
||||
teamPlayers: Array<TeamPlayer & { player: { position: Position } }>,
|
||||
position: Position
|
||||
) {
|
||||
const fmt = FORMATIONS[formation] ?? FORMATIONS["4-3-3"];
|
||||
const starterLimit = position === "GK" ? 1 : fmt[position.toLowerCase() as "def" | "mid" | "fwd"];
|
||||
const starters = teamPlayers.filter((item) => !item.isBench && item.player.position === position);
|
||||
if (starters.length < starterLimit) {
|
||||
return { isBench: false as const, placementLabel: "فیکس" };
|
||||
}
|
||||
|
||||
const bench = teamPlayers.filter((item) => item.isBench && item.player.position === position);
|
||||
if (bench.length < 1) {
|
||||
return { isBench: true as const, placementLabel: "ذخیره" };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user