feat: improve events browsing filters and calendar
This commit is contained in:
167
src/lib/event-ui.ts
Normal file
167
src/lib/event-ui.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
export type EventAvailabilityInput = {
|
||||
status?: string | null;
|
||||
registration_start_date?: string | null;
|
||||
registration_end_date?: string | null;
|
||||
start_time?: string | null;
|
||||
end_time?: string | null;
|
||||
capacity?: number | null;
|
||||
registration_count?: number | null;
|
||||
};
|
||||
|
||||
export function getEventPath(event: { id: number; slug?: string | null }) {
|
||||
const segment = event.slug?.trim() || String(event.id);
|
||||
return `/events/${encodeURIComponent(segment)}`;
|
||||
}
|
||||
|
||||
export type EventRegistrationState =
|
||||
| "open"
|
||||
| "not_started"
|
||||
| "closed"
|
||||
| "full"
|
||||
| "unavailable";
|
||||
|
||||
function timestamp(value?: string | null) {
|
||||
if (!value) return null;
|
||||
const parsed = new Date(value).getTime();
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
export function getEventRegistrationState(
|
||||
event: EventAvailabilityInput,
|
||||
now = Date.now(),
|
||||
): { state: EventRegistrationState; remaining: number | null } {
|
||||
if (event.status !== "published") {
|
||||
return { state: "unavailable", remaining: null };
|
||||
}
|
||||
|
||||
const endsAt = timestamp(event.registration_end_date);
|
||||
if (endsAt !== null && now > endsAt) {
|
||||
return { state: "closed", remaining: getRemainingCapacity(event) };
|
||||
}
|
||||
|
||||
const startsAt = timestamp(event.registration_start_date);
|
||||
if (startsAt !== null && now < startsAt) {
|
||||
return { state: "not_started", remaining: getRemainingCapacity(event) };
|
||||
}
|
||||
|
||||
const remaining = getRemainingCapacity(event);
|
||||
if (remaining !== null && remaining <= 0) {
|
||||
return { state: "full", remaining: 0 };
|
||||
}
|
||||
|
||||
return { state: "open", remaining };
|
||||
}
|
||||
|
||||
function getRemainingCapacity(event: EventAvailabilityInput) {
|
||||
if (event.capacity == null || !Number.isFinite(Number(event.capacity))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const used = Number.isFinite(Number(event.registration_count))
|
||||
? Number(event.registration_count)
|
||||
: 0;
|
||||
return Math.max(0, Number(event.capacity) - used);
|
||||
}
|
||||
|
||||
export function getEventCardState(
|
||||
event: EventAvailabilityInput,
|
||||
now = Date.now(),
|
||||
): "past" | "full" | "upcoming" {
|
||||
const endsAt = timestamp(event.end_time);
|
||||
if (event.status === "completed" || (endsAt !== null && endsAt < now)) {
|
||||
return "past";
|
||||
}
|
||||
|
||||
if (getEventRegistrationState(event, now).state === "full") {
|
||||
return "full";
|
||||
}
|
||||
|
||||
return "upcoming";
|
||||
}
|
||||
|
||||
export function getEventBrowseLabel(event: EventAvailabilityInput, now = Date.now()) {
|
||||
if (getEventCardState(event, now) === "past") return "برگزار شده";
|
||||
if (getEventRegistrationState(event, now).state === "open") return "درحال برگزاری";
|
||||
const startsAt = timestamp(event.start_time);
|
||||
if (startsAt !== null && startsAt <= now) return "درحال برگزاری";
|
||||
return "بهزودی";
|
||||
}
|
||||
|
||||
export function getEventRegistrationMessage(event: EventAvailabilityInput, now = Date.now()) {
|
||||
if (getEventCardState(event, now) === "past") return "این رویداد برگزار شده است";
|
||||
const registration = getEventRegistrationState(event, now);
|
||||
if (registration.state === "not_started") return "ثبتنام هنوز آغاز نشده است";
|
||||
if (registration.state === "closed") return "زمان ثبتنام به پایان رسیده است";
|
||||
if (registration.state === "full") return "ظرفیت ثبتنام تکمیل شده است";
|
||||
if (registration.state === "unavailable") return "امکان ثبتنام وجود ندارد";
|
||||
if (registration.remaining === null) return "ثبتنام در حال انجام است";
|
||||
return `${registration.remaining.toLocaleString("fa-IR")} نفر ظرفیت باقیمانده`;
|
||||
}
|
||||
|
||||
export type RegistrationErrorKind =
|
||||
| "already_registered"
|
||||
| "not_started"
|
||||
| "closed"
|
||||
| "full"
|
||||
| "unknown";
|
||||
|
||||
function normalizeApiMessage(message: unknown) {
|
||||
return String(message ?? "")
|
||||
.replace(/[يى]/g, "ی")
|
||||
.replace(/\u200c/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.toLocaleLowerCase();
|
||||
}
|
||||
|
||||
export function classifyRegistrationError(message: unknown): RegistrationErrorKind {
|
||||
const normalized = normalizeApiMessage(message);
|
||||
if (normalized.includes("already registered") || normalized.includes("قبلا در این ایونت ثبت نام کرده اید")) {
|
||||
return "already_registered";
|
||||
}
|
||||
if (normalized.includes("زمان ثبت نام هنوز آغاز نشده") || normalized.includes("registration has not started")) {
|
||||
return "not_started";
|
||||
}
|
||||
if (normalized.includes("مهلت ثبت نام به پایان رسیده") || normalized.includes("registration period has ended")) {
|
||||
return "closed";
|
||||
}
|
||||
if (normalized.includes("ظرفیت شرکت کنندگان تکمیل است") || normalized.includes("event is full")) {
|
||||
return "full";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
export type PaymentOutcome = "paid" | "pending" | "failed" | "canceled" | "unknown";
|
||||
|
||||
export function getPaymentOutcome(status: unknown): PaymentOutcome {
|
||||
const normalized = normalizeApiMessage(status);
|
||||
if (["success", "paid", "ok", "پرداخت شده", "تایید شده"].includes(normalized)) {
|
||||
return "paid";
|
||||
}
|
||||
if (["pending", "initiated", "init", "در انتظار", "در حال پردازش"].includes(normalized)) {
|
||||
return "pending";
|
||||
}
|
||||
if (["canceled", "cancelled", "لغو شده"].includes(normalized)) {
|
||||
return "canceled";
|
||||
}
|
||||
if (["failed", "failure", "ناموفق"].includes(normalized)) {
|
||||
return "failed";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
export type RegistrationOutcome = "confirmed" | "pending" | "canceled" | "unknown";
|
||||
|
||||
export function getRegistrationOutcome(status: unknown): RegistrationOutcome {
|
||||
const normalized = normalizeApiMessage(status);
|
||||
if (["confirmed", "attended", "approved", "تایید شده"].includes(normalized)) {
|
||||
return "confirmed";
|
||||
}
|
||||
if (["pending", "در انتظار"].includes(normalized)) {
|
||||
return "pending";
|
||||
}
|
||||
if (["canceled", "cancelled", "لغو شده"].includes(normalized)) {
|
||||
return "canceled";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
Reference in New Issue
Block a user