@wasto/cms-components 1.5.10 → 1.5.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -47
- package/dist/index.d.mts +181 -51
- package/dist/index.d.ts +181 -51
- package/dist/index.js +3614 -3015
- package/dist/index.mjs +3021 -2418
- package/package.json +12 -2
- package/src/components/announcement-modal/AnnouncementModal.tsx +183 -183
- package/src/components/article/Article.tsx +21 -21
- package/src/components/article/ArticleCard.tsx +81 -81
- package/src/components/article/ArticleExcerpts.tsx +80 -80
- package/src/components/article/ArticleList.tsx +114 -114
- package/src/components/article/ArticleMarkdown.tsx +170 -170
- package/src/components/article/ArticleReaderModal.tsx +93 -93
- package/src/components/article/ArticleSingle.tsx +57 -57
- package/src/components/bottom-tab/BottomTab.tsx +392 -392
- package/src/components/button/Button.tsx +154 -154
- package/src/components/challenge/Badges.tsx +84 -84
- package/src/components/challenge/BadgesModal.tsx +166 -166
- package/src/components/challenge/Challenge.tsx +380 -380
- package/src/components/challenge/ChallengeModal.tsx +231 -231
- package/src/components/challenge/ChallengeVignette.tsx +249 -249
- package/src/components/challenge/ChallengesModal.tsx +243 -243
- package/src/components/challenge/QuizModal.tsx +381 -381
- package/src/components/challenge/ResultModal.tsx +144 -144
- package/src/components/chat/Chat.tsx +928 -928
- package/src/components/collection-calendar/CollectionCalendar.tsx +274 -274
- package/src/components/columns/Columns.tsx +33 -33
- package/src/components/consignes-modal/ConsignesModal.tsx +267 -267
- package/src/components/content-inset/ContentInset.tsx +33 -33
- package/src/components/header/Header.tsx +415 -391
- package/src/components/image/Image.tsx +15 -15
- package/src/components/image/ImageFrame.tsx +102 -102
- package/src/components/image/ImageMedia.tsx +31 -31
- package/src/components/image/ImageUrl.tsx +30 -30
- package/src/components/locations-map/LocationSheet.tsx +417 -417
- package/src/components/locations-map/LocationsMap.native.tsx +260 -260
- package/src/components/locations-map/LocationsMap.tsx +13 -13
- package/src/components/locations-map/LocationsMap.web.tsx +10 -10
- package/src/components/locations-map/LocationsMapShell.tsx +546 -527
- package/src/components/locations-map/LocationsMapView.tsx +108 -108
- package/src/components/locations-map/LocationsMapView.web.tsx +331 -331
- package/src/components/nav-button/NavButton.tsx +59 -59
- package/src/components/next-collection/NextCollection.tsx +636 -621
- package/src/components/notifications/NotificationsScreen.tsx +247 -247
- package/src/components/page/Page.tsx +102 -64
- package/src/components/parapraph/Paragraph.tsx +67 -67
- package/src/components/profile/ProfileScreen.tsx +236 -236
- package/src/components/push/PushTokenRegistrar.tsx +47 -0
- package/src/components/recycling-info/RecyclingInfo.tsx +189 -189
- package/src/components/safe-area/OptionalSafeAreaProvider.tsx +17 -17
- package/src/components/search/Search.tsx +320 -320
- package/src/components/select-street-modal/SelectStreetModal.tsx +475 -475
- package/src/components/title/Title.tsx +100 -100
- package/src/components/vision/Vision.tsx +447 -447
- package/src/components/vision/VisionActionCard.tsx +117 -117
- package/src/components/vision/VisionCapture.tsx +30 -30
- package/src/components/vision/VisionTapOverlay.tsx +122 -122
- package/src/contexts/AnnouncementsContext.tsx +171 -171
- package/src/contexts/AppDataContext.tsx +458 -0
- package/src/contexts/ChallengesContext.tsx +182 -182
- package/src/contexts/HeaderConfigContext.tsx +29 -0
- package/src/contexts/LocalContext.tsx +227 -227
- package/src/contexts/ThemeContext.tsx +37 -37
- package/src/contexts/WastoContext.tsx +132 -127
- package/src/helpers/blogPost.ts +72 -72
- package/src/helpers/generateQuizQuestions.ts +247 -247
- package/src/helpers/getContrastColor.ts +18 -18
- package/src/helpers/haptics.ts +20 -20
- package/src/helpers/hoursSchedule.ts +230 -230
- package/src/helpers/htmlContentToMarkdown.ts +114 -114
- package/src/helpers/layout.ts +54 -54
- package/src/helpers/loadLinearGradient.ts +22 -22
- package/src/helpers/mapLocations.ts +170 -0
- package/src/helpers/maquetteTheme.ts +76 -76
- package/src/helpers/markdownUnderlinePlugin.ts +32 -32
- package/src/helpers/nextCollection.ts +122 -0
- package/src/helpers/normalizeWasteTypeRecyclingInfo.ts +80 -80
- package/src/helpers/publicMediaUrl.ts +8 -8
- package/src/helpers/resolveApiAssetUrl.ts +21 -21
- package/src/helpers/systemRoutes.ts +7 -7
- package/src/helpers/themeStyles.ts +114 -114
- package/src/helpers/ttlCache.ts +41 -0
- package/src/helpers/useSafeAreaPadding.ts +14 -14
- package/src/hooks/useBadges.ts +100 -100
- package/src/hooks/useBlogPosts.ts +109 -105
- package/src/hooks/useChallenges.ts +110 -110
- package/src/hooks/useCities.ts +56 -56
- package/src/hooks/useCityAnnouncements.ts +118 -118
- package/src/hooks/useCollectionCalendar.ts +421 -421
- package/src/hooks/useColumns.ts +19 -19
- package/src/hooks/useConsignes.ts +62 -62
- package/src/hooks/useLocations.ts +120 -248
- package/src/hooks/useNextCollection.ts +163 -238
- package/src/hooks/useRecyclingInfo.ts +90 -90
- package/src/hooks/useWastoClient.ts +16 -16
- package/src/index.ts +161 -142
- package/src/navigation/NavigationContext.tsx +56 -56
- package/src/push/registerExpoPushToken.ts +133 -0
- package/src/sentry/initSentry.ts +120 -120
- package/src/types/Theme.ts +72 -72
- package/src/types/css-modules.d.ts +1 -1
|
@@ -1,230 +1,230 @@
|
|
|
1
|
-
import {
|
|
2
|
-
WEEKDAYS,
|
|
3
|
-
WEEKDAY_LABELS_FR,
|
|
4
|
-
formatOpeningHoursWeekly,
|
|
5
|
-
formatTimeDisplay,
|
|
6
|
-
getBrowserTimeZone,
|
|
7
|
-
parseTimeMinutes,
|
|
8
|
-
resolveHoursDisplay,
|
|
9
|
-
resolveHoursText,
|
|
10
|
-
resolveOpeningHoursUtc,
|
|
11
|
-
type TimeSlot,
|
|
12
|
-
type Weekday,
|
|
13
|
-
} from "@wasto/sdk";
|
|
14
|
-
|
|
15
|
-
export type HoursDayRow = {
|
|
16
|
-
label: string;
|
|
17
|
-
hours: string;
|
|
18
|
-
closed: boolean;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export type HoursStatus = {
|
|
22
|
-
open: boolean;
|
|
23
|
-
text: string;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export type HoursSchedule = {
|
|
27
|
-
rows: HoursDayRow[];
|
|
28
|
-
status: HoursStatus | null;
|
|
29
|
-
summary: string | null;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const DAY_ALIASES: Record<string, Weekday> = {
|
|
33
|
-
lun: "monday",
|
|
34
|
-
lundi: "monday",
|
|
35
|
-
mar: "tuesday",
|
|
36
|
-
mardi: "tuesday",
|
|
37
|
-
mer: "wednesday",
|
|
38
|
-
mercredi: "wednesday",
|
|
39
|
-
jeu: "thursday",
|
|
40
|
-
jeudi: "thursday",
|
|
41
|
-
ven: "friday",
|
|
42
|
-
vendredi: "friday",
|
|
43
|
-
sam: "saturday",
|
|
44
|
-
samedi: "saturday",
|
|
45
|
-
dim: "sunday",
|
|
46
|
-
dimanche: "sunday",
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
function parseTimeToken(raw: string): string | null {
|
|
50
|
-
const m = raw.trim().match(/^(\d{1,2})(?:[h:](\d{2}))?$/i);
|
|
51
|
-
if (!m) return null;
|
|
52
|
-
const h = Number(m[1]);
|
|
53
|
-
const min = m[2] ? Number(m[2]) : 0;
|
|
54
|
-
if (h > 24 || min > 59) return null;
|
|
55
|
-
const hours = Math.min(h, 23);
|
|
56
|
-
return `${String(hours).padStart(2, "0")}:${String(min).padStart(2, "0")}`;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function formatSlotDisplay(slot: TimeSlot): string {
|
|
60
|
-
const fmt = (hhmm: string) => {
|
|
61
|
-
const [h, m] = hhmm.split(":").map(Number);
|
|
62
|
-
return `${h}:${String(m).padStart(2, "0")}`;
|
|
63
|
-
};
|
|
64
|
-
return `${fmt(slot.open)} - ${fmt(slot.close)}`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function emptyByDay(): Map<Weekday, TimeSlot[]> {
|
|
68
|
-
const byDay = new Map<Weekday, TimeSlot[]>();
|
|
69
|
-
for (const day of WEEKDAYS) byDay.set(day, []);
|
|
70
|
-
return byDay;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function rowsFromByDay(byDay: Map<Weekday, TimeSlot[]>): HoursDayRow[] {
|
|
74
|
-
return WEEKDAYS.map((day) => {
|
|
75
|
-
const slots = byDay.get(day) ?? [];
|
|
76
|
-
return {
|
|
77
|
-
label: WEEKDAY_LABELS_FR[day],
|
|
78
|
-
hours: slots.length ? slots.map(formatSlotDisplay).join(", ") : "Fermé",
|
|
79
|
-
closed: slots.length === 0,
|
|
80
|
-
};
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function weekdayFromLocalDate(at: Date): Weekday {
|
|
85
|
-
const jsDay = at.getDay();
|
|
86
|
-
return WEEKDAYS[(jsDay + 6) % 7]!;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function statusFromByDay(
|
|
90
|
-
byDay: Map<Weekday, TimeSlot[]>,
|
|
91
|
-
at = new Date(),
|
|
92
|
-
): HoursStatus | null {
|
|
93
|
-
const hasAny = WEEKDAYS.some((day) => (byDay.get(day) ?? []).length > 0);
|
|
94
|
-
if (!hasAny) return null;
|
|
95
|
-
|
|
96
|
-
const weekday = weekdayFromLocalDate(at);
|
|
97
|
-
const nowMin = at.getHours() * 60 + at.getMinutes();
|
|
98
|
-
const todaySlots = byDay.get(weekday) ?? [];
|
|
99
|
-
|
|
100
|
-
for (const slot of todaySlots) {
|
|
101
|
-
const open = parseTimeMinutes(slot.open);
|
|
102
|
-
const close = parseTimeMinutes(slot.close);
|
|
103
|
-
if (open == null || close == null) continue;
|
|
104
|
-
if (nowMin >= open && nowMin < close) {
|
|
105
|
-
return { open: true, text: `Ouvert - Ferme à ${formatTimeDisplay(slot.close)}` };
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const startIdx = WEEKDAYS.indexOf(weekday);
|
|
110
|
-
for (let i = 0; i < 7; i++) {
|
|
111
|
-
const day = WEEKDAYS[(startIdx + i) % 7]!;
|
|
112
|
-
const daySlots = byDay.get(day) ?? [];
|
|
113
|
-
for (const slot of daySlots) {
|
|
114
|
-
const open = parseTimeMinutes(slot.open);
|
|
115
|
-
if (open == null) continue;
|
|
116
|
-
if (i === 0 && open <= nowMin) continue;
|
|
117
|
-
const openLabel = formatTimeDisplay(slot.open);
|
|
118
|
-
if (i === 0) return { open: false, text: `Fermé - Ouvre à ${openLabel}` };
|
|
119
|
-
return {
|
|
120
|
-
open: false,
|
|
121
|
-
text: `Fermé - Ouvre ${WEEKDAY_LABELS_FR[day]} à ${openLabel}`,
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return { open: false, text: "Fermé" };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function applyDayRange(
|
|
130
|
-
byDay: Map<Weekday, TimeSlot[]>,
|
|
131
|
-
from: Weekday,
|
|
132
|
-
to: Weekday,
|
|
133
|
-
slot: TimeSlot,
|
|
134
|
-
) {
|
|
135
|
-
const startIdx = WEEKDAYS.indexOf(from);
|
|
136
|
-
const endIdx = WEEKDAYS.indexOf(to);
|
|
137
|
-
if (startIdx < 0 || endIdx < 0) return;
|
|
138
|
-
for (let i = startIdx; ; i = (i + 1) % 7) {
|
|
139
|
-
byDay.set(WEEKDAYS[i]!, [slot]);
|
|
140
|
-
if (i === endIdx) break;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function parseTextHours(text: string): Map<Weekday, TimeSlot[]> | null {
|
|
145
|
-
const t = text.trim();
|
|
146
|
-
if (!t) return null;
|
|
147
|
-
const byDay = emptyByDay();
|
|
148
|
-
|
|
149
|
-
if (/24\s*h?\s*\/\s*24/i.test(t) || /^24\s*h/i.test(t)) {
|
|
150
|
-
for (const day of WEEKDAYS) byDay.set(day, [{ open: "00:00", close: "23:59" }]);
|
|
151
|
-
return byDay;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const timeRe = /(\d{1,2})(?:[:h](\d{2}))?h?\s*[-–à]\s*(\d{1,2})(?:[:h](\d{2}))?h?/i;
|
|
155
|
-
const timeMatch = t.match(timeRe);
|
|
156
|
-
if (!timeMatch || timeMatch.index == null) return null;
|
|
157
|
-
|
|
158
|
-
const open = parseTimeToken(`${timeMatch[1]}h${timeMatch[2] ?? "00"}`);
|
|
159
|
-
const close = parseTimeToken(`${timeMatch[3]}h${timeMatch[4] ?? "00"}`);
|
|
160
|
-
if (!open || !close) return null;
|
|
161
|
-
const slot: TimeSlot = { open, close };
|
|
162
|
-
|
|
163
|
-
const dayPart = t.slice(0, timeMatch.index);
|
|
164
|
-
const dayRe =
|
|
165
|
-
/(lundi|mardi|mercredi|jeudi|vendredi|samedi|dimanche|lun|mar|mer|jeu|ven|sam|dim)\.?\b/gi;
|
|
166
|
-
const daysFound: Weekday[] = [];
|
|
167
|
-
let match: RegExpExecArray | null;
|
|
168
|
-
while ((match = dayRe.exec(dayPart)) !== null) {
|
|
169
|
-
const raw = match[1].toLowerCase();
|
|
170
|
-
const day = DAY_ALIASES[raw] ?? DAY_ALIASES[raw.slice(0, 3)];
|
|
171
|
-
if (day) daysFound.push(day);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (daysFound.length === 0) {
|
|
175
|
-
for (const day of WEEKDAYS) byDay.set(day, [slot]);
|
|
176
|
-
return byDay;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (daysFound.length >= 2 && /[-–à]/.test(dayPart)) {
|
|
180
|
-
applyDayRange(byDay, daysFound[0]!, daysFound[1]!, slot);
|
|
181
|
-
return byDay;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
for (const day of daysFound) byDay.set(day, [slot]);
|
|
185
|
-
return byDay;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function byDayFromWeekly(
|
|
189
|
-
weekly: ReturnType<typeof formatOpeningHoursWeekly>,
|
|
190
|
-
): Map<Weekday, TimeSlot[]> {
|
|
191
|
-
const byDay = emptyByDay();
|
|
192
|
-
for (const row of weekly) {
|
|
193
|
-
byDay.set(row.day, row.closed ? [] : row.slots);
|
|
194
|
-
}
|
|
195
|
-
return byDay;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export function buildHoursSchedule(point: {
|
|
199
|
-
hours?: string | null;
|
|
200
|
-
details?: Record<string, unknown> | null;
|
|
201
|
-
}): HoursSchedule {
|
|
202
|
-
const utc = resolveOpeningHoursUtc(point);
|
|
203
|
-
if (utc) {
|
|
204
|
-
const weekly = formatOpeningHoursWeekly(utc, getBrowserTimeZone());
|
|
205
|
-
const byDay = byDayFromWeekly(weekly);
|
|
206
|
-
return {
|
|
207
|
-
rows: weekly.map((row) => ({
|
|
208
|
-
label: row.label,
|
|
209
|
-
hours: row.closed ? "Fermé" : row.slots.map(formatSlotDisplay).join(", "),
|
|
210
|
-
closed: row.closed,
|
|
211
|
-
})),
|
|
212
|
-
status: statusFromByDay(byDay),
|
|
213
|
-
summary: resolveHoursDisplay(point),
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const text =
|
|
218
|
-
resolveHoursText(point) ??
|
|
219
|
-
(typeof point.hours === "string" && point.hours.trim() ? point.hours.trim() : null);
|
|
220
|
-
if (!text) return { rows: [], status: null, summary: null };
|
|
221
|
-
|
|
222
|
-
const parsed = parseTextHours(text);
|
|
223
|
-
if (!parsed) return { rows: [], status: null, summary: text };
|
|
224
|
-
|
|
225
|
-
return {
|
|
226
|
-
rows: rowsFromByDay(parsed),
|
|
227
|
-
status: statusFromByDay(parsed),
|
|
228
|
-
summary: text,
|
|
229
|
-
};
|
|
230
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
WEEKDAYS,
|
|
3
|
+
WEEKDAY_LABELS_FR,
|
|
4
|
+
formatOpeningHoursWeekly,
|
|
5
|
+
formatTimeDisplay,
|
|
6
|
+
getBrowserTimeZone,
|
|
7
|
+
parseTimeMinutes,
|
|
8
|
+
resolveHoursDisplay,
|
|
9
|
+
resolveHoursText,
|
|
10
|
+
resolveOpeningHoursUtc,
|
|
11
|
+
type TimeSlot,
|
|
12
|
+
type Weekday,
|
|
13
|
+
} from "@wasto/sdk";
|
|
14
|
+
|
|
15
|
+
export type HoursDayRow = {
|
|
16
|
+
label: string;
|
|
17
|
+
hours: string;
|
|
18
|
+
closed: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type HoursStatus = {
|
|
22
|
+
open: boolean;
|
|
23
|
+
text: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type HoursSchedule = {
|
|
27
|
+
rows: HoursDayRow[];
|
|
28
|
+
status: HoursStatus | null;
|
|
29
|
+
summary: string | null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const DAY_ALIASES: Record<string, Weekday> = {
|
|
33
|
+
lun: "monday",
|
|
34
|
+
lundi: "monday",
|
|
35
|
+
mar: "tuesday",
|
|
36
|
+
mardi: "tuesday",
|
|
37
|
+
mer: "wednesday",
|
|
38
|
+
mercredi: "wednesday",
|
|
39
|
+
jeu: "thursday",
|
|
40
|
+
jeudi: "thursday",
|
|
41
|
+
ven: "friday",
|
|
42
|
+
vendredi: "friday",
|
|
43
|
+
sam: "saturday",
|
|
44
|
+
samedi: "saturday",
|
|
45
|
+
dim: "sunday",
|
|
46
|
+
dimanche: "sunday",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function parseTimeToken(raw: string): string | null {
|
|
50
|
+
const m = raw.trim().match(/^(\d{1,2})(?:[h:](\d{2}))?$/i);
|
|
51
|
+
if (!m) return null;
|
|
52
|
+
const h = Number(m[1]);
|
|
53
|
+
const min = m[2] ? Number(m[2]) : 0;
|
|
54
|
+
if (h > 24 || min > 59) return null;
|
|
55
|
+
const hours = Math.min(h, 23);
|
|
56
|
+
return `${String(hours).padStart(2, "0")}:${String(min).padStart(2, "0")}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function formatSlotDisplay(slot: TimeSlot): string {
|
|
60
|
+
const fmt = (hhmm: string) => {
|
|
61
|
+
const [h, m] = hhmm.split(":").map(Number);
|
|
62
|
+
return `${h}:${String(m).padStart(2, "0")}`;
|
|
63
|
+
};
|
|
64
|
+
return `${fmt(slot.open)} - ${fmt(slot.close)}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function emptyByDay(): Map<Weekday, TimeSlot[]> {
|
|
68
|
+
const byDay = new Map<Weekday, TimeSlot[]>();
|
|
69
|
+
for (const day of WEEKDAYS) byDay.set(day, []);
|
|
70
|
+
return byDay;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function rowsFromByDay(byDay: Map<Weekday, TimeSlot[]>): HoursDayRow[] {
|
|
74
|
+
return WEEKDAYS.map((day) => {
|
|
75
|
+
const slots = byDay.get(day) ?? [];
|
|
76
|
+
return {
|
|
77
|
+
label: WEEKDAY_LABELS_FR[day],
|
|
78
|
+
hours: slots.length ? slots.map(formatSlotDisplay).join(", ") : "Fermé",
|
|
79
|
+
closed: slots.length === 0,
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function weekdayFromLocalDate(at: Date): Weekday {
|
|
85
|
+
const jsDay = at.getDay();
|
|
86
|
+
return WEEKDAYS[(jsDay + 6) % 7]!;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function statusFromByDay(
|
|
90
|
+
byDay: Map<Weekday, TimeSlot[]>,
|
|
91
|
+
at = new Date(),
|
|
92
|
+
): HoursStatus | null {
|
|
93
|
+
const hasAny = WEEKDAYS.some((day) => (byDay.get(day) ?? []).length > 0);
|
|
94
|
+
if (!hasAny) return null;
|
|
95
|
+
|
|
96
|
+
const weekday = weekdayFromLocalDate(at);
|
|
97
|
+
const nowMin = at.getHours() * 60 + at.getMinutes();
|
|
98
|
+
const todaySlots = byDay.get(weekday) ?? [];
|
|
99
|
+
|
|
100
|
+
for (const slot of todaySlots) {
|
|
101
|
+
const open = parseTimeMinutes(slot.open);
|
|
102
|
+
const close = parseTimeMinutes(slot.close);
|
|
103
|
+
if (open == null || close == null) continue;
|
|
104
|
+
if (nowMin >= open && nowMin < close) {
|
|
105
|
+
return { open: true, text: `Ouvert - Ferme à ${formatTimeDisplay(slot.close)}` };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const startIdx = WEEKDAYS.indexOf(weekday);
|
|
110
|
+
for (let i = 0; i < 7; i++) {
|
|
111
|
+
const day = WEEKDAYS[(startIdx + i) % 7]!;
|
|
112
|
+
const daySlots = byDay.get(day) ?? [];
|
|
113
|
+
for (const slot of daySlots) {
|
|
114
|
+
const open = parseTimeMinutes(slot.open);
|
|
115
|
+
if (open == null) continue;
|
|
116
|
+
if (i === 0 && open <= nowMin) continue;
|
|
117
|
+
const openLabel = formatTimeDisplay(slot.open);
|
|
118
|
+
if (i === 0) return { open: false, text: `Fermé - Ouvre à ${openLabel}` };
|
|
119
|
+
return {
|
|
120
|
+
open: false,
|
|
121
|
+
text: `Fermé - Ouvre ${WEEKDAY_LABELS_FR[day]} à ${openLabel}`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { open: false, text: "Fermé" };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function applyDayRange(
|
|
130
|
+
byDay: Map<Weekday, TimeSlot[]>,
|
|
131
|
+
from: Weekday,
|
|
132
|
+
to: Weekday,
|
|
133
|
+
slot: TimeSlot,
|
|
134
|
+
) {
|
|
135
|
+
const startIdx = WEEKDAYS.indexOf(from);
|
|
136
|
+
const endIdx = WEEKDAYS.indexOf(to);
|
|
137
|
+
if (startIdx < 0 || endIdx < 0) return;
|
|
138
|
+
for (let i = startIdx; ; i = (i + 1) % 7) {
|
|
139
|
+
byDay.set(WEEKDAYS[i]!, [slot]);
|
|
140
|
+
if (i === endIdx) break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function parseTextHours(text: string): Map<Weekday, TimeSlot[]> | null {
|
|
145
|
+
const t = text.trim();
|
|
146
|
+
if (!t) return null;
|
|
147
|
+
const byDay = emptyByDay();
|
|
148
|
+
|
|
149
|
+
if (/24\s*h?\s*\/\s*24/i.test(t) || /^24\s*h/i.test(t)) {
|
|
150
|
+
for (const day of WEEKDAYS) byDay.set(day, [{ open: "00:00", close: "23:59" }]);
|
|
151
|
+
return byDay;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const timeRe = /(\d{1,2})(?:[:h](\d{2}))?h?\s*[-–à]\s*(\d{1,2})(?:[:h](\d{2}))?h?/i;
|
|
155
|
+
const timeMatch = t.match(timeRe);
|
|
156
|
+
if (!timeMatch || timeMatch.index == null) return null;
|
|
157
|
+
|
|
158
|
+
const open = parseTimeToken(`${timeMatch[1]}h${timeMatch[2] ?? "00"}`);
|
|
159
|
+
const close = parseTimeToken(`${timeMatch[3]}h${timeMatch[4] ?? "00"}`);
|
|
160
|
+
if (!open || !close) return null;
|
|
161
|
+
const slot: TimeSlot = { open, close };
|
|
162
|
+
|
|
163
|
+
const dayPart = t.slice(0, timeMatch.index);
|
|
164
|
+
const dayRe =
|
|
165
|
+
/(lundi|mardi|mercredi|jeudi|vendredi|samedi|dimanche|lun|mar|mer|jeu|ven|sam|dim)\.?\b/gi;
|
|
166
|
+
const daysFound: Weekday[] = [];
|
|
167
|
+
let match: RegExpExecArray | null;
|
|
168
|
+
while ((match = dayRe.exec(dayPart)) !== null) {
|
|
169
|
+
const raw = match[1].toLowerCase();
|
|
170
|
+
const day = DAY_ALIASES[raw] ?? DAY_ALIASES[raw.slice(0, 3)];
|
|
171
|
+
if (day) daysFound.push(day);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (daysFound.length === 0) {
|
|
175
|
+
for (const day of WEEKDAYS) byDay.set(day, [slot]);
|
|
176
|
+
return byDay;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (daysFound.length >= 2 && /[-–à]/.test(dayPart)) {
|
|
180
|
+
applyDayRange(byDay, daysFound[0]!, daysFound[1]!, slot);
|
|
181
|
+
return byDay;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
for (const day of daysFound) byDay.set(day, [slot]);
|
|
185
|
+
return byDay;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function byDayFromWeekly(
|
|
189
|
+
weekly: ReturnType<typeof formatOpeningHoursWeekly>,
|
|
190
|
+
): Map<Weekday, TimeSlot[]> {
|
|
191
|
+
const byDay = emptyByDay();
|
|
192
|
+
for (const row of weekly) {
|
|
193
|
+
byDay.set(row.day, row.closed ? [] : row.slots);
|
|
194
|
+
}
|
|
195
|
+
return byDay;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function buildHoursSchedule(point: {
|
|
199
|
+
hours?: string | null;
|
|
200
|
+
details?: Record<string, unknown> | null;
|
|
201
|
+
}): HoursSchedule {
|
|
202
|
+
const utc = resolveOpeningHoursUtc(point);
|
|
203
|
+
if (utc) {
|
|
204
|
+
const weekly = formatOpeningHoursWeekly(utc, getBrowserTimeZone());
|
|
205
|
+
const byDay = byDayFromWeekly(weekly);
|
|
206
|
+
return {
|
|
207
|
+
rows: weekly.map((row) => ({
|
|
208
|
+
label: row.label,
|
|
209
|
+
hours: row.closed ? "Fermé" : row.slots.map(formatSlotDisplay).join(", "),
|
|
210
|
+
closed: row.closed,
|
|
211
|
+
})),
|
|
212
|
+
status: statusFromByDay(byDay),
|
|
213
|
+
summary: resolveHoursDisplay(point),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const text =
|
|
218
|
+
resolveHoursText(point) ??
|
|
219
|
+
(typeof point.hours === "string" && point.hours.trim() ? point.hours.trim() : null);
|
|
220
|
+
if (!text) return { rows: [], status: null, summary: null };
|
|
221
|
+
|
|
222
|
+
const parsed = parseTextHours(text);
|
|
223
|
+
if (!parsed) return { rows: [], status: null, summary: text };
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
rows: rowsFromByDay(parsed),
|
|
227
|
+
status: statusFromByDay(parsed),
|
|
228
|
+
summary: text,
|
|
229
|
+
};
|
|
230
|
+
}
|