Files
TREK/client/src/components/Vacay/holidayRegions.ts
T
Elmer 7bd9e08016 feat(vacay): add school holiday overlays (#1633)
* feat: add vacay school holiday overlays

* fix(vacay): remove SOH control character corruption in locale files

13 of 22 vacay locale files contained a 0x01 (SOH) control character
that corrupted the closing }; token. This broke the shared package
build (tsdown/rolldown PARSE_ERROR: Invalid Character) and cascaded
to all client tests failing to resolve @trek/shared.

Affected locales: ar, br, ca, cs, es, fr, gr, hu, id, it, ja, ko,
nl, pl, ru, sv, tr, uk, vi, zh, zh-TW

* fix(vacay): school-holiday migration order, i18n dupes, settings layout

- Move the school-holiday columns migration to the end of the migrations
  array. It sat before the #1435/#1281 migrations, which every existing DB
  had already run, so the runner skipped it and school_holidays_enabled /
  vacay_holiday_calendars.type were never created — PUT /addons/vacay/plan
  then 500'd with "no such column".
- Remove duplicate i18n keys in 13 locale vacay files (TS1117) that failed
  the shared + client typechecks.
- Move the school-holidays block into the right column, directly under
  public holidays, instead of full-width below everything.
- Add the school-holidays section to the mobile Vacay settings sheet.

* feat(vacay): modern school-holiday day styling + combined hover tooltip

- Replace the stacked 2px underlines with a single rounded accent band and a
  soft background wash on plain school-break days (desktop + mobile — mobile
  did not render school holidays at all before).
- Fold school holidays into the custom day tooltip under a divider instead of a
  separate native title, and show the tooltip on every set day (whole vacation
  days too), not only half days / shared calendars.

---------

Co-authored-by: Otto <uif86164@automotive-wan.com>
Co-authored-by: Maurice <mauriceboe@icloud.com>
2026-07-23 22:48:42 +02:00

84 lines
3.3 KiB
TypeScript

import apiClient from '../../api/client'
import iso31662 from 'iso-3166-2'
import { SCHOOL_HOLIDAY_COUNTRY_CONFIG } from '../../vacay/schoolHolidayCountries'
// Loads the subdivision (state/region) options for a holiday-calendar country.
//
// The subdivision list is sourced from ISO 3166-2 (proper names, complete per country)
// rather than inferred from which subdivisions happen to have a holiday this year — the
// latter silently dropped states with no state-specific holiday (e.g. US-WA, issue #1456).
// We only surface a list for countries that are actually region-partitioned, so countries
// with only nationwide holidays keep showing no region picker (and allow a country-level
// calendar, matching the server's applyHolidayCalendars behaviour).
export async function fetchRegionOptions(country: string): Promise<{ value: string; label: string }[]> {
try {
const year = new Date().getFullYear()
const r = await apiClient.get(`/addons/vacay/holidays/${year}/${country}`)
const hasRegions = r.data.some(h => h.counties && h.counties.length > 0)
if (!hasRegions) return []
const opts = new Map<string, string>() // ISO code -> display name
const sub = iso31662.country(country)?.sub || {}
for (const [code, info] of Object.entries(sub)) opts.set(code, info.name)
// Fall back to any nager county code ISO doesn't know about, so nothing regresses.
r.data.forEach(h => h.counties?.forEach(c => {
if (!opts.has(c)) opts.set(c, iso31662.subdivision(c)?.name || c.split('-')[1] || c)
}))
return [...opts]
.map(([value, label]) => ({ value, label }))
.sort((a, b) => a.label.localeCompare(b.label))
} catch {
return []
}
}
type LocalizedName = { language: string; text: string }
type OpenHolidayOption = {
code: string
shortName?: string
name?: LocalizedName[]
children?: OpenHolidayOption[] | null
}
function localizedName(names: LocalizedName[] | undefined, fallback: string): string {
return names?.find(n => n.language?.toUpperCase() === 'DE')?.text
|| names?.find(n => n.language?.toUpperCase() === 'EN')?.text
|| names?.[0]?.text
|| fallback
}
function flattenOptions(items: OpenHolidayOption[] | undefined, prefix = ''): { value: string; label: string }[] {
const result: { value: string; label: string }[] = []
for (const item of items ?? []) {
const label = localizedName(item.name, item.shortName || item.code)
result.push({ value: item.code, label: prefix ? `${prefix} / ${label}` : label })
result.push(...flattenOptions(item.children ?? undefined, prefix ? `${prefix} / ${label}` : label))
}
return result
}
export async function fetchSchoolHolidayRegionOptions(country: string): Promise<{ value: string; label: string }[]> {
const config = SCHOOL_HOLIDAY_COUNTRY_CONFIG[country]
if (!config || config.strategy === 'country') return []
try {
const r = await apiClient.get(`/addons/vacay/school-holidays/regions/${country}`)
if (config.strategy === 'groups') {
return flattenOptions(r.data.groups)
.map(opt => ({
value: `${country}|group:${opt.value}`,
label: opt.label,
}))
.sort((a, b) => a.label.localeCompare(b.label))
}
return flattenOptions(r.data.subdivisions)
.map(opt => ({ value: opt.value, label: opt.label }))
.sort((a, b) => a.label.localeCompare(b.label))
} catch {
return []
}
}