2026-09-30 17:48:32 +08:00
|
|
|
|
/* ============================================================
|
|
|
|
|
|
Festival skin gating — date-driven, URL-overridable.
|
|
|
|
|
|
Only Christmas is implemented today; add more festivals by
|
|
|
|
|
|
extending FESTIVAL_WINDOWS plus the matching components under
|
|
|
|
|
|
components/festival/.
|
|
|
|
|
|
============================================================ */
|
|
|
|
|
|
import { useMemo } from "react";
|
|
|
|
|
|
|
2026-10-09 13:56:57 +08:00
|
|
|
|
export type FestivalId = "christmas" | "halloween" | "newyear";
|
2026-09-30 17:48:32 +08:00
|
|
|
|
|
|
|
|
|
|
/** Inclusive [month, day] windows. Christmas: Dec 20 – Dec 26; Halloween: Oct 26 – Nov 1. */
|
|
|
|
|
|
const FESTIVAL_WINDOWS: Record<
|
|
|
|
|
|
FestivalId,
|
|
|
|
|
|
{ from: [number, number]; to: [number, number] }
|
|
|
|
|
|
> = {
|
|
|
|
|
|
christmas: { from: [12, 20], to: [12, 26] },
|
|
|
|
|
|
halloween: { from: [10, 26], to: [11, 1] },
|
2026-10-09 13:56:57 +08:00
|
|
|
|
// 元旦接续圣诞节,跨年窗口包含首尾日期。
|
|
|
|
|
|
newyear: { from: [12, 27], to: [1, 3] },
|
2026-09-30 17:48:32 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function inWindow(
|
|
|
|
|
|
month: number,
|
|
|
|
|
|
day: number,
|
|
|
|
|
|
w: { from: [number, number]; to: [number, number] },
|
|
|
|
|
|
) {
|
|
|
|
|
|
const cur = month * 100 + day;
|
|
|
|
|
|
const from = w.from[0] * 100 + w.from[1];
|
|
|
|
|
|
const to = w.to[0] * 100 + w.to[1];
|
|
|
|
|
|
// Support windows that wrap the new year (from > to).
|
|
|
|
|
|
return from <= to ? cur >= from && cur <= to : cur >= from || cur <= to;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Resolve the active festival skin.
|
|
|
|
|
|
* `?festival=christmas` forces it on and `?festival=off` forces it off (QA hook,
|
|
|
|
|
|
* so the skin can be verified without waiting for the real date window).
|
|
|
|
|
|
* Otherwise falls back to the system-date window.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function getFestival(
|
|
|
|
|
|
now: Date = new Date(),
|
|
|
|
|
|
search: string = typeof location !== "undefined" ? location.search : "",
|
|
|
|
|
|
): FestivalId | null {
|
|
|
|
|
|
const override = new URLSearchParams(search).get("festival");
|
|
|
|
|
|
if (override === "off" || override === "none") return null;
|
|
|
|
|
|
if (override && Object.prototype.hasOwnProperty.call(FESTIVAL_WINDOWS, override)) {
|
|
|
|
|
|
return override as FestivalId;
|
|
|
|
|
|
}
|
|
|
|
|
|
for (const id of Object.keys(FESTIVAL_WINDOWS) as FestivalId[]) {
|
|
|
|
|
|
if (inWindow(now.getMonth() + 1, now.getDate(), FESTIVAL_WINDOWS[id])) return id;
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Stable per-mount festival resolution (the date won't matter mid-session). */
|
|
|
|
|
|
export function useFestival(): FestivalId | null {
|
|
|
|
|
|
return useMemo(() => getFestival(), []);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-09 13:56:57 +08:00
|
|
|
|
/** 跨年夜展示即将到来的年份,其余月份预览展示当年。 */
|
|
|
|
|
|
export function getNewYearDisplayYear(now: Date = new Date()): number {
|
|
|
|
|
|
return now.getFullYear() + (now.getMonth() === 11 ? 1 : 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-30 17:48:32 +08:00
|
|
|
|
/** 首页 VU 区的节日问候语,键与 FestivalId 对应。 */
|
|
|
|
|
|
export const FESTIVAL_GREETINGS: Record<FestivalId, string> = {
|
|
|
|
|
|
christmas: "Merry Christmas",
|
|
|
|
|
|
halloween: "Happy Halloween",
|
2026-10-09 13:56:57 +08:00
|
|
|
|
newyear: "Happy New Year",
|
2026-09-30 17:48:32 +08:00
|
|
|
|
};
|