diff --git a/apps/adm/src/views/SettingsView.vue b/apps/adm/src/views/SettingsView.vue index dde539d..c25e603 100644 --- a/apps/adm/src/views/SettingsView.vue +++ b/apps/adm/src/views/SettingsView.vue @@ -25,7 +25,11 @@
- + +

برای همهٔ شهرهای ایران فقط Asia/Tehran معتبر است.

diff --git a/apps/api/src/content/content.service.ts b/apps/api/src/content/content.service.ts index 431c796..7fea5d6 100644 --- a/apps/api/src/content/content.service.ts +++ b/apps/api/src/content/content.service.ts @@ -1,4 +1,5 @@ import { Injectable, NotFoundException } from "@nestjs/common"; +import { resolveTimezone } from "@montech/shared"; import { PrismaService } from "../prisma/prisma.service"; @Injectable() @@ -19,11 +20,12 @@ export class ContentService { socials?: object; logoUrl?: string | null; }) { - const { logoUrl, ...rest } = data; + const { logoUrl, timezone, ...rest } = data; return this.prisma.siteSettings.update({ where: { id: "default" }, data: { ...rest, + ...(timezone !== undefined ? { timezone: resolveTimezone(timezone) } : {}), ...(logoUrl !== undefined ? { logoUrl: logoUrl || null } : {}), }, }); @@ -48,11 +50,22 @@ export class ContentService { } async getPublicSite() { - const [settings, sections, pages] = await Promise.all([ + const [settingsRaw, sections, pages] = await Promise.all([ this.getSettings(), this.listSections(), this.prisma.page.findMany({ include: { seo: true } }), ]); + const settings = { + ...settingsRaw, + timezone: resolveTimezone(settingsRaw.timezone), + }; + // heal invalid timezone stored in DB (e.g. Asia/Esfahan) + if (settingsRaw.timezone !== settings.timezone) { + await this.prisma.siteSettings.update({ + where: { id: "default" }, + data: { timezone: settings.timezone }, + }); + } const sectionMap: Record = {}; for (const s of sections) sectionMap[s.key] = s.data; return { settings, sections: sectionMap, pages }; diff --git a/apps/landing/components/SiteNavbar.vue b/apps/landing/components/SiteNavbar.vue index bf716b0..3a756e8 100644 --- a/apps/landing/components/SiteNavbar.vue +++ b/apps/landing/components/SiteNavbar.vue @@ -18,20 +18,19 @@ diff --git a/apps/landing/utils/timezone.ts b/apps/landing/utils/timezone.ts new file mode 100644 index 0000000..fa8f52b --- /dev/null +++ b/apps/landing/utils/timezone.ts @@ -0,0 +1,32 @@ +const FALLBACK_TZ = "Asia/Tehran"; + +const TZ_ALIASES: Record = { + "Asia/Esfahan": FALLBACK_TZ, + "Asia/Isfahan": FALLBACK_TZ, + "Asia/Mashhad": FALLBACK_TZ, + "Asia/Shiraz": FALLBACK_TZ, + "Asia/Tabriz": FALLBACK_TZ, + Iran: FALLBACK_TZ, +}; + +export function resolveTimezone(input?: string | null): string { + const raw = (input || FALLBACK_TZ).trim() || FALLBACK_TZ; + const candidate = TZ_ALIASES[raw] || raw; + try { + Intl.DateTimeFormat("en-US", { timeZone: candidate }); + return candidate; + } catch { + return FALLBACK_TZ; + } +} + +export function formatClock(date: Date, timezone?: string | null, locale = "fa-IR") { + const tz = resolveTimezone(timezone); + return new Intl.DateTimeFormat(locale, { + timeZone: tz, + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }).format(date); +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 6793255..a202c1d 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1 +1,2 @@ export * from "./brand"; +export * from "./timezone"; diff --git a/packages/shared/src/timezone.ts b/packages/shared/src/timezone.ts new file mode 100644 index 0000000..d353b39 --- /dev/null +++ b/packages/shared/src/timezone.ts @@ -0,0 +1,34 @@ +const FALLBACK_TZ = "Asia/Tehran"; + +/** Common mistaken city zones → valid IANA */ +const TZ_ALIASES: Record = { + "Asia/Esfahan": FALLBACK_TZ, + "Asia/Isfahan": FALLBACK_TZ, + "Asia/Mashhad": FALLBACK_TZ, + "Asia/Shiraz": FALLBACK_TZ, + "Asia/Tabriz": FALLBACK_TZ, + "Iran": FALLBACK_TZ, +}; + +export function resolveTimezone(input?: string | null): string { + const raw = (input || FALLBACK_TZ).trim() || FALLBACK_TZ; + const candidate = TZ_ALIASES[raw] || raw; + try { + // Throws RangeError if invalid + Intl.DateTimeFormat("en-US", { timeZone: candidate }); + return candidate; + } catch { + return FALLBACK_TZ; + } +} + +export function formatClock(date: Date, timezone?: string | null, locale = "fa-IR") { + const tz = resolveTimezone(timezone); + return new Intl.DateTimeFormat(locale, { + timeZone: tz, + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }).format(date); +}