Skip to content

Commit 222621a

Browse files
committed
feat: added ability to list swiss holidays for easier camp start selection #12
1 parent 1ea0e8c commit 222621a

10 files changed

+493
-1
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"@fortawesome/react-fontawesome": "^0.2.0",
3333
"axios": "^1.6.0",
3434
"date-fns": "^4.1.0",
35+
"dompurify": "^3.2.1",
3536
"exceljs": "^4.4.0",
3637
"i18next": "^23.8.2",
3738
"i18next-browser-languagedetector": "^8.0.0",

src/apis/openholidays-api.ts

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import axios from "axios";
2+
import { format } from "date-fns";
3+
4+
const client = axios.create({
5+
baseURL: 'https://openholidaysapi.org/',
6+
headers: {
7+
"Content-type": "application/json",
8+
},
9+
});
10+
11+
export const loadSubdivisions = async (languageCode?: OHApiLanguageCode): Promise<OHApiSubdivision[]> => {
12+
const params = getBaseParams(languageCode);
13+
return (await client.get<OHApiSubdivision[]>('/subdivisions', { params: params })).data;
14+
}
15+
16+
export const loadPublicHolidays = async (validFromDate: Date, validToDate: Date, languageCode?: OHApiLanguageCode): Promise<OHApiHoliday[]> => {
17+
const params = getBaseParams(languageCode);
18+
params.append('validFrom', format(validFromDate, 'yyyy-MM-dd'));
19+
params.append('validTo', format(validToDate, 'yyyy-MM-dd'));
20+
21+
return (await client.get<OHApiHoliday[]>('/publicholidays', { params: params })).data;
22+
}
23+
24+
export const loadSchoolHolidays = async (validFromDate: Date, validToDate: Date, languageCode?: OHApiLanguageCode): Promise<OHApiHoliday[]> => {
25+
const params = getBaseParams(languageCode);
26+
params.append('validFrom', format(validFromDate, 'yyyy-MM-dd'));
27+
params.append('validTo', format(validToDate, 'yyyy-MM-dd'));
28+
29+
return (await client.get<OHApiHoliday[]>('/schoolholidays', { params: params })).data;
30+
}
31+
32+
const getBaseParams = (languageCode?: OHApiLanguageCode): URLSearchParams => {
33+
const params = new URLSearchParams();
34+
params.append('countryIsoCode', 'CH');
35+
36+
if (languageCode) {
37+
params.append('languageIsoCode', languageCode);
38+
}
39+
40+
return params;
41+
}
42+
43+
export const parseLanguageOrDefault = (lang: string): OHApiLanguageCode => {
44+
const openApiLanguageCode = lang.toUpperCase();
45+
return isValidLanguageCode(openApiLanguageCode)
46+
? openApiLanguageCode as OHApiLanguageCode
47+
: 'EN';
48+
}
49+
50+
const isValidLanguageCode = (code: string): boolean => code === 'DE' || code === 'FR' || code === 'IT' || code === 'EN';
51+
52+
export type OHApiLanguageCode = 'DE' | 'FR' | 'IT' | 'EN';
53+
54+
export interface OHApiSubdivision {
55+
name: OHApiLanguageText[];
56+
shortName: string;
57+
category: OHApiLanguageText[];
58+
code: string;
59+
isoCode: string;
60+
children: string[];
61+
officialLanguages: string[];
62+
comment: string | null;
63+
}
64+
65+
export interface OHApiHoliday {
66+
id: string;
67+
name: OHApiLanguageText[];
68+
type: string;
69+
startDate: string;
70+
endDate: string;
71+
nationwide: boolean;
72+
regionalScope: string;
73+
temporalScope: string;
74+
subdivisions?: OHApiSubdivisionInfo[];
75+
comment?: OHApiLanguageText[];
76+
}
77+
78+
export interface OHApiLanguageText {
79+
language: OHApiLanguageCode;
80+
text: string;
81+
}
82+
83+
export interface OHApiSubdivisionInfo {
84+
code: string;
85+
shortName: string;
86+
}

src/i18n/de.json

+14
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"calendarPage": {
1818
"title": "Kalender",
1919
"startDate": "Erster Lagertag",
20+
"viewHolidays": "Ferientage suchen",
2021
"responsible": "Verantwortlich",
2122
"puffer": "Puffer (Tage)",
2223
"pufferDescription": "Reduziert das Datum der Termine um die angegebene Anzahl an Tagen.",
@@ -57,6 +58,19 @@
5758
"link": "Zum Kapitel"
5859
}
5960
},
61+
"holidaysModal": {
62+
"title": "Ferien und Feiertage",
63+
"description": "Wähle einen Kanton aus und definiere das Jahr für welches du die Ferien und Feiertage sehen möchtest. Wenn du auf ein Datum klickst wird dieses als der erste Lagertag ausgewählt.",
64+
"canton": "Kanton",
65+
"selectCanton": "Kanton auswählen...",
66+
"year": "Jahr",
67+
"from": "Von",
68+
"to": "Bis",
69+
"type": "Art",
70+
"noResults": "Keine Informationen zu Ferien und Feiertage gefunden.",
71+
"close": "Schliessen",
72+
"dataHint": "Die Informationen werden durch das <a href=\"https://www.openholidaysapi.org/\" rel=\"noreferrer\">OpenHolidays API</a> bereitgestellt."
73+
},
6074
"searchPage": {
6175
"title": "Suche",
6276
"searchPlaceholder": "Suchbegriff eingeben...",

src/i18n/fr.json

+14
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"calendarPage": {
1818
"title": "Calendrier",
1919
"startDate": "Premier jour du camp",
20+
"viewHolidays": "Chercher des jours de vacances",
2021
"responsible": "Responsable",
2122
"puffer": "Marge (jours)",
2223
"pufferDescription": "Réduit la date des rendez-vous du nombre de jours indiqué.",
@@ -57,6 +58,19 @@
5758
"link": "Au chapitre"
5859
}
5960
},
61+
"holidaysModal": {
62+
"title": "Ferien und Feiertage",
63+
"description": "Choisis un canton et définis l'année pour laquelle tu souhaites voir les vacances et les jours fériés. Si tu cliques sur une date, celle-ci sera sélectionnée comme premier jour de camp.",
64+
"canton": "Canton",
65+
"selectCanton": "Sélectionner un canton...",
66+
"year": "Année",
67+
"from": "Du",
68+
"to": "À",
69+
"type": "Type",
70+
"noResults": "Aucune information trouvée sur les vacances et les jours fériés",
71+
"close": "Fermer",
72+
"dataHint": "Les informations sont fournies par <a href=\"https://www.openholidaysapi.org/\" rel=\"noreferrer\">API OpenHolidays</a>."
73+
},
6074
"searchPage": {
6175
"title": "Recherche",
6276
"searchPlaceholder": "Saisir un terme de recherche...",

src/i18n/it.json

+14
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"calendarPage": {
1818
"title": "Calendario",
1919
"startDate": "Primo giorno del campo",
20+
"viewHolidays": "Ricerca giorni di vacanza",
2021
"responsible": "Responsabile",
2122
"puffer": "Tampone (giorni)",
2223
"pufferDescription": "Riduce la data degli appuntamenti del numero di giorni specificato.",
@@ -57,6 +58,19 @@
5758
"link": "Al capitolo"
5859
}
5960
},
61+
"holidaysModal": {
62+
"title": "Ferien und Feiertage",
63+
"description": "Selezionare un cantone e definire l'anno per il quale si desidera visualizzare le vacanze e i giorni festivi. Se si fa clic su una data, questa verrà selezionata come primo giorno del campo.",
64+
"canton": "Canton",
65+
"selectCanton": "Selezionare il cantone...",
66+
"year": "Anno",
67+
"from": "Da",
68+
"to": "A",
69+
"type": "Tipo",
70+
"noResults": "Non sono state trovate informazioni sulle vacanze e sui giorni festivi.",
71+
"close": "Chiudere",
72+
"dataHint": "Le informazioni sono fornite da <a href=\"https://www.openholidaysapi.org/\" rel=\"noreferrer\">API di OpenHolidays</a>."
73+
},
6074
"searchPage": {
6175
"title": "Cerca",
6276
"searchPlaceholder": "Inserisci il termine di ricerca...",

src/pages/calendar/components/CalendarForm.tsx

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { ChangeEvent, useCallback, useEffect, useState } from 'react'
1+
import React, { ChangeEvent, useContext, useEffect, useState } from 'react'
22
import { useTranslation } from 'react-i18next';
33
import CalendarTable from './CalendarTable';
44
import { CalendarTask } from './Task';
@@ -12,6 +12,9 @@ import { Tooltip } from 'react-tooltip'
1212
import { sessionCache } from "../../../shared/session-cache";
1313
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
1414
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
15+
import { ModalContext } from "../../../components/modal/ModalContext";
16+
import HolidaySelectModal, { HolidayModalResultData } from "./HolidaySelectModal";
17+
1518

1619
const dateFormat = 'yyyy-MM-dd'
1720
const initialStartDate = format(Date.now(), dateFormat)
@@ -26,6 +29,7 @@ const calendarDesignationCacheKey = 'calendar-designation'
2629
function CalendarForm() {
2730

2831
const { t } = useTranslation()
32+
const { openModal } = useContext(ModalContext);
2933

3034
const defaultCalendarDesignation = t('calendarPage.defaultDesignation');
3135

@@ -87,6 +91,15 @@ function CalendarForm() {
8791
sessionCache.set(calendarDesignationCacheKey, newPrefix);
8892
}
8993

94+
const openHolidaysModal = async () => {
95+
const result = await openModal<HolidayModalResultData>(HolidaySelectModal, {}, { isWide: true });
96+
if (result.isCancelled || !result.data?.selectedDate) {
97+
return
98+
}
99+
100+
updateStartDate(result.data.selectedDate)
101+
};
102+
90103
useEffect(() => {
91104
const parsedStartDate = parse(startDate, dateFormat, Date.now())
92105
const isValidDate = isValid(parsedStartDate)
@@ -173,6 +186,9 @@ function CalendarForm() {
173186
</label>
174187
<input id="startDate" type="date" name="startDate" value={startDate}
175188
onChange={onStartDateChanged}/>
189+
<a className="cursor-pointer" onClick={openHolidaysModal}>
190+
{t('calendarPage.viewHolidays')}
191+
</a>
176192
</div>
177193

178194
<div className="form-entry">

0 commit comments

Comments
 (0)