@wasto/cms-components 1.5.10

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.
Files changed (94) hide show
  1. package/README.md +47 -0
  2. package/dist/index.d.mts +465 -0
  3. package/dist/index.d.ts +465 -0
  4. package/dist/index.js +10445 -0
  5. package/dist/index.mjs +10553 -0
  6. package/package.json +90 -0
  7. package/src/components/announcement-modal/AnnouncementModal.tsx +183 -0
  8. package/src/components/article/Article.tsx +22 -0
  9. package/src/components/article/ArticleCard.tsx +81 -0
  10. package/src/components/article/ArticleExcerpts.tsx +80 -0
  11. package/src/components/article/ArticleList.tsx +114 -0
  12. package/src/components/article/ArticleMarkdown.tsx +170 -0
  13. package/src/components/article/ArticleReaderModal.tsx +93 -0
  14. package/src/components/article/ArticleSingle.tsx +57 -0
  15. package/src/components/bottom-tab/BottomTab.tsx +392 -0
  16. package/src/components/button/Button.tsx +154 -0
  17. package/src/components/challenge/Badges.tsx +84 -0
  18. package/src/components/challenge/BadgesModal.tsx +166 -0
  19. package/src/components/challenge/Challenge.tsx +380 -0
  20. package/src/components/challenge/ChallengeModal.tsx +231 -0
  21. package/src/components/challenge/ChallengeVignette.tsx +249 -0
  22. package/src/components/challenge/ChallengesModal.tsx +243 -0
  23. package/src/components/challenge/QuizModal.tsx +381 -0
  24. package/src/components/challenge/ResultModal.tsx +144 -0
  25. package/src/components/chat/Chat.tsx +928 -0
  26. package/src/components/collection-calendar/CollectionCalendar.tsx +274 -0
  27. package/src/components/columns/Columns.tsx +34 -0
  28. package/src/components/consignes-modal/ConsignesModal.tsx +267 -0
  29. package/src/components/content-inset/ContentInset.tsx +33 -0
  30. package/src/components/header/Header.tsx +391 -0
  31. package/src/components/image/Image.tsx +16 -0
  32. package/src/components/image/ImageFrame.tsx +102 -0
  33. package/src/components/image/ImageMedia.tsx +31 -0
  34. package/src/components/image/ImageUrl.tsx +30 -0
  35. package/src/components/locations-map/LocationSheet.tsx +417 -0
  36. package/src/components/locations-map/LocationsMap.native.tsx +260 -0
  37. package/src/components/locations-map/LocationsMap.tsx +13 -0
  38. package/src/components/locations-map/LocationsMap.web.tsx +10 -0
  39. package/src/components/locations-map/LocationsMapShell.tsx +527 -0
  40. package/src/components/locations-map/LocationsMapView.tsx +108 -0
  41. package/src/components/locations-map/LocationsMapView.web.tsx +331 -0
  42. package/src/components/nav-button/NavButton.tsx +59 -0
  43. package/src/components/next-collection/NextCollection.tsx +621 -0
  44. package/src/components/notifications/NotificationsScreen.tsx +247 -0
  45. package/src/components/page/Page.tsx +64 -0
  46. package/src/components/parapraph/Paragraph.tsx +67 -0
  47. package/src/components/profile/ProfileScreen.tsx +236 -0
  48. package/src/components/recycling-info/RecyclingInfo.tsx +189 -0
  49. package/src/components/safe-area/OptionalSafeAreaProvider.tsx +17 -0
  50. package/src/components/search/Search.tsx +320 -0
  51. package/src/components/select-street-modal/SelectStreetModal.tsx +475 -0
  52. package/src/components/title/Title.tsx +100 -0
  53. package/src/components/vision/Vision.tsx +447 -0
  54. package/src/components/vision/VisionActionCard.tsx +117 -0
  55. package/src/components/vision/VisionCapture.tsx +30 -0
  56. package/src/components/vision/VisionTapOverlay.tsx +122 -0
  57. package/src/contexts/AnnouncementsContext.tsx +171 -0
  58. package/src/contexts/ChallengesContext.tsx +182 -0
  59. package/src/contexts/LocalContext.tsx +227 -0
  60. package/src/contexts/ThemeContext.tsx +37 -0
  61. package/src/contexts/WastoContext.tsx +127 -0
  62. package/src/helpers/blogPost.ts +72 -0
  63. package/src/helpers/generateQuizQuestions.ts +247 -0
  64. package/src/helpers/getContrastColor.ts +19 -0
  65. package/src/helpers/haptics.ts +20 -0
  66. package/src/helpers/hoursSchedule.ts +230 -0
  67. package/src/helpers/htmlContentToMarkdown.ts +114 -0
  68. package/src/helpers/layout.ts +54 -0
  69. package/src/helpers/loadLinearGradient.ts +22 -0
  70. package/src/helpers/maquetteTheme.ts +76 -0
  71. package/src/helpers/markdownUnderlinePlugin.ts +32 -0
  72. package/src/helpers/normalizeWasteTypeRecyclingInfo.ts +80 -0
  73. package/src/helpers/publicMediaUrl.ts +8 -0
  74. package/src/helpers/resolveApiAssetUrl.ts +21 -0
  75. package/src/helpers/systemRoutes.ts +7 -0
  76. package/src/helpers/themeStyles.ts +114 -0
  77. package/src/helpers/useSafeAreaPadding.ts +14 -0
  78. package/src/hooks/useBadges.ts +100 -0
  79. package/src/hooks/useBlogPosts.ts +105 -0
  80. package/src/hooks/useChallenges.ts +110 -0
  81. package/src/hooks/useCities.ts +56 -0
  82. package/src/hooks/useCityAnnouncements.ts +118 -0
  83. package/src/hooks/useCollectionCalendar.ts +421 -0
  84. package/src/hooks/useColumns.ts +20 -0
  85. package/src/hooks/useConsignes.ts +62 -0
  86. package/src/hooks/useLocations.ts +248 -0
  87. package/src/hooks/useNextCollection.ts +238 -0
  88. package/src/hooks/useRecyclingInfo.ts +90 -0
  89. package/src/hooks/useWastoClient.ts +16 -0
  90. package/src/index.ts +142 -0
  91. package/src/navigation/NavigationContext.tsx +56 -0
  92. package/src/sentry/initSentry.ts +120 -0
  93. package/src/types/Theme.ts +72 -0
  94. package/src/types/css-modules.d.ts +1 -0
@@ -0,0 +1,421 @@
1
+ import { useCallback, useMemo, useState } from "react";
2
+ import { useLocal } from "../contexts/LocalContext";
3
+ import { useNextCollection, type WasteCollectionPlace, type WasteType } from "./useNextCollection";
4
+
5
+ export const MONTHS = [
6
+ "Janvier",
7
+ "Février",
8
+ "Mars",
9
+ "Avril",
10
+ "Mai",
11
+ "Juin",
12
+ "Juillet",
13
+ "Août",
14
+ "Septembre",
15
+ "Octobre",
16
+ "Novembre",
17
+ "Décembre",
18
+ ] as const;
19
+
20
+ const DAY_MAPPING: Record<string, number> = {
21
+ Dimanche: 6,
22
+ Lundi: 0,
23
+ Mardi: 1,
24
+ Mercredi: 2,
25
+ Jeudi: 3,
26
+ Vendredi: 4,
27
+ Samedi: 5,
28
+ };
29
+
30
+ export type CollectionScheduleEntry = {
31
+ dayOfWeek: number; // lundi=0 ... dimanche=6
32
+ wasteTypeId: string;
33
+ color: string;
34
+ biweekly?: boolean;
35
+ startDate?: string;
36
+ };
37
+
38
+ export type CalendarException = {
39
+ dateFrom: string;
40
+ dateTo: string;
41
+ wasteTypeId?: string;
42
+ };
43
+
44
+ export type WasteTypeInfoForDay = {
45
+ wasteTypeId: string;
46
+ color: string;
47
+ /** Marqueur utilisé dans l’UI historique: “retour” d’exception */
48
+ isException?: boolean;
49
+ hasActiveException?: boolean;
50
+ };
51
+
52
+ /** Coordonnées d’une case (sans données de collecte). */
53
+ export type CalendarDayCellCore = {
54
+ day: number;
55
+ month: number;
56
+ year: number;
57
+ /** `true` si le jour appartient au mois affiché dans le titre. */
58
+ inCurrentMonth: boolean;
59
+ };
60
+
61
+ /** Case prête à l’affichage : planning global + exceptions **de ce jour uniquement** + collectes du jour. */
62
+ export type CalendarDayCell = CalendarDayCellCore & {
63
+ schedule: CollectionScheduleEntry[];
64
+ exceptions: CalendarException[];
65
+ wasteTypesForDay: WasteTypeInfoForDay[];
66
+ };
67
+
68
+ export type UseCollectionCalendarOptions = {
69
+ /** Si omis, on utilise `LocalContext.selectedCityId`. */
70
+ cityId?: string | null;
71
+ /** Si omis, on utilise `LocalContext.selectedCityId` + 1er lieu disponible. */
72
+ selectedPlaceId?: string | null;
73
+ /** Mois initial (0-11). Par défaut: mois courant. */
74
+ initialMonth?: number;
75
+ /** Année initiale. Par défaut: année courante. */
76
+ initialYear?: number;
77
+ };
78
+
79
+ export type UseCollectionCalendarResult = {
80
+ cityId: string | null;
81
+ isLoading: boolean;
82
+ error: string | null;
83
+
84
+ currentMonth: number;
85
+ currentYear: number;
86
+ monthLabel: string;
87
+ setCurrentMonth: (m: number) => void;
88
+ setCurrentYear: (y: number) => void;
89
+ goToPrevMonth: () => void;
90
+ goToNextMonth: () => void;
91
+
92
+ weekDays: string[];
93
+ /** Semaines complètes (7 colonnes), avec jours du mois précédent / suivant pour remplir la grille. */
94
+ weeks: CalendarDayCell[][];
95
+
96
+ selectedPlaceId: string | null;
97
+
98
+ /** Collectes pour une date précise (utile pour les cases en débordement). */
99
+ getWasteTypesForDate: (year: number, month: number, day: number) => WasteTypeInfoForDay[];
100
+ /** Raccourci : jour du **mois affiché** uniquement (même que `getWasteTypesForDate(currentYear, currentMonth, day)`). */
101
+ getWasteTypesForDay: (day: number) => WasteTypeInfoForDay[];
102
+ };
103
+
104
+ function clampMonth(m: number): number {
105
+ if (!Number.isFinite(m)) return new Date().getMonth();
106
+ return Math.max(0, Math.min(11, Math.floor(m)));
107
+ }
108
+
109
+ /**
110
+ * Parse `dateFrom` / `dateTo` renvoyés par l’API (souvent ISO complet `...T00:00:00.000Z`).
111
+ * Évite `${date}T00:00:00` qui produit une chaîne invalide si la valeur contient déjà `T…`.
112
+ * Extrait `YYYY-MM-DD` et construit une date **locale** à minuit (alignée sur les cases du calendrier).
113
+ */
114
+ function exceptionBoundaryToLocalDate(value: string): Date {
115
+ const trimmed = value.trim();
116
+ const datePart = trimmed.includes("T") ? trimmed.split("T")[0]! : trimmed;
117
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(datePart);
118
+ if (m) {
119
+ return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]), 0, 0, 0, 0);
120
+ }
121
+ const d = new Date(trimmed);
122
+ if (!Number.isNaN(d.getTime())) {
123
+ d.setHours(0, 0, 0, 0);
124
+ return d;
125
+ }
126
+ return new Date(NaN);
127
+ }
128
+
129
+ /**
130
+ * Grille calendrier : semaines de 7 jours, semaine commençant lundi.
131
+ * Les cases avant le 1er et après le dernier jour du mois utilisent le mois précédent / suivant.
132
+ */
133
+ export function buildCalendarWeeks(year: number, month: number): CalendarDayCellCore[][] {
134
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
135
+ const firstDayOfMonth = new Date(year, month, 1).getDay();
136
+ const mondayOffset = firstDayOfMonth === 0 ? 6 : firstDayOfMonth - 1;
137
+
138
+ const prevMonth = month === 0 ? 11 : month - 1;
139
+ const prevYear = month === 0 ? year - 1 : year;
140
+ const daysInPrevMonth = new Date(year, month, 0).getDate();
141
+
142
+ const cells: CalendarDayCellCore[] = [];
143
+
144
+ for (let i = 0; i < mondayOffset; i += 1) {
145
+ const d = daysInPrevMonth - mondayOffset + 1 + i;
146
+ cells.push({ day: d, month: prevMonth, year: prevYear, inCurrentMonth: false });
147
+ }
148
+
149
+ for (let d = 1; d <= daysInMonth; d += 1) {
150
+ cells.push({ day: d, month, year, inCurrentMonth: true });
151
+ }
152
+
153
+ const nextMonth = month === 11 ? 0 : month + 1;
154
+ const nextYear = month === 11 ? year + 1 : year;
155
+ let nextDay = 1;
156
+ while (cells.length % 7 !== 0) {
157
+ cells.push({ day: nextDay, month: nextMonth, year: nextYear, inCurrentMonth: false });
158
+ nextDay += 1;
159
+ }
160
+
161
+ const weeks: CalendarDayCellCore[][] = [];
162
+ for (let i = 0; i < cells.length; i += 7) {
163
+ weeks.push(cells.slice(i, i + 7));
164
+ }
165
+ return weeks;
166
+ }
167
+
168
+ /**
169
+ * Exceptions qui concernent la date calendaire donnée (aligné sur {@link computeWasteTypesForDay} : [dateFrom, dateTo) ;
170
+ * si dateFrom === dateTo, traité comme une exception d’une seule journée).
171
+ */
172
+ export function exceptionsForCalendarDay(
173
+ day: number,
174
+ year: number,
175
+ month: number,
176
+ allExceptions: CalendarException[]
177
+ ): CalendarException[] {
178
+ const cell = new Date(year, month, day);
179
+ cell.setHours(0, 0, 0, 0);
180
+ const c = cell.getTime();
181
+
182
+ return allExceptions.filter((exc) => {
183
+ const from = exceptionBoundaryToLocalDate(exc.dateFrom);
184
+ const to = exceptionBoundaryToLocalDate(exc.dateTo);
185
+ const f = from.getTime();
186
+ const t = to.getTime();
187
+ if (f === t) {
188
+ return c === f;
189
+ }
190
+ return c >= f && c < t;
191
+ });
192
+ }
193
+
194
+ function enrichCalendarWeeks(
195
+ base: CalendarDayCellCore[][],
196
+ schedule: CollectionScheduleEntry[],
197
+ allExceptions: CalendarException[]
198
+ ): CalendarDayCell[][] {
199
+ return base.map((row) =>
200
+ row.map((cell) => {
201
+ const exceptionsForDay = exceptionsForCalendarDay(cell.day, cell.year, cell.month, allExceptions);
202
+ return {
203
+ ...cell,
204
+ schedule,
205
+ exceptions: exceptionsForDay,
206
+ wasteTypesForDay: computeWasteTypesForDay(
207
+ cell.day,
208
+ cell.year,
209
+ cell.month,
210
+ schedule,
211
+ allExceptions
212
+ ),
213
+ };
214
+ })
215
+ );
216
+ }
217
+
218
+ /** @deprecated Préférer `buildCalendarWeeks` pour une grille avec débordements. */
219
+ export function getDaysInMonth(year: number, month: number): Array<number | null> {
220
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
221
+ const firstDayOfMonth = new Date(year, month, 1).getDay();
222
+ const mondayBasedDay = firstDayOfMonth === 0 ? 6 : firstDayOfMonth - 1;
223
+ const days: Array<number | null> = [];
224
+ for (let i = 0; i < mondayBasedDay; i += 1) days.push(null);
225
+ for (let day = 1; day <= daysInMonth; day += 1) days.push(day);
226
+ return days;
227
+ }
228
+
229
+ function getWeeksSinceStart(startDateStr: string, currentDate: Date): number {
230
+ const startDate = new Date(startDateStr);
231
+ const diffDays = Math.floor((currentDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));
232
+ return Math.floor(diffDays / 7);
233
+ }
234
+
235
+ function buildSchedule(
236
+ wasteTypes: WasteType[],
237
+ places: WasteCollectionPlace[],
238
+ selectedPlaceId?: string | null
239
+ ): { schedule: CollectionScheduleEntry[]; exceptions: CalendarException[]; resolvedPlaceId: string | null } {
240
+ const safePlaces = (places || []) as WasteCollectionPlace[];
241
+ if (!safePlaces.length) {
242
+ return { schedule: [], exceptions: [], resolvedPlaceId: null };
243
+ }
244
+
245
+ const selected =
246
+ (selectedPlaceId
247
+ ? safePlaces.find((p) => p.id === selectedPlaceId)
248
+ : undefined) ?? safePlaces[0];
249
+
250
+ const wasteTypeMap: Record<string, { color?: string }> = {};
251
+ (wasteTypes || []).forEach((wt) => {
252
+ if (wt?.id) wasteTypeMap[wt.id] = { color: wt.color };
253
+ });
254
+
255
+ const rawDays = (selected.collectionDays || []) as Array<{
256
+ name: string;
257
+ wasteTypeId: string;
258
+ biweekly?: boolean;
259
+ startDate?: string;
260
+ }>;
261
+
262
+ const schedule: CollectionScheduleEntry[] = [];
263
+ for (const day of rawDays) {
264
+ const dayIndex = DAY_MAPPING[day.name];
265
+ if (dayIndex === undefined) continue;
266
+ const color = wasteTypeMap[day.wasteTypeId]?.color;
267
+ schedule.push({
268
+ dayOfWeek: dayIndex,
269
+ wasteTypeId: day.wasteTypeId,
270
+ color: typeof color === "string" && color.trim().length > 0 ? color : "#7C7C7C",
271
+ biweekly: day.biweekly,
272
+ startDate: day.startDate,
273
+ });
274
+ }
275
+
276
+ const exceptions = ((selected as unknown as { exceptions?: unknown[] })?.exceptions || []) as CalendarException[];
277
+
278
+ return { schedule, exceptions, resolvedPlaceId: selected?.id ?? null };
279
+ }
280
+
281
+ function computeWasteTypesForDay(
282
+ day: number,
283
+ year: number,
284
+ month: number,
285
+ schedule: CollectionScheduleEntry[],
286
+ exceptions: CalendarException[]
287
+ ): WasteTypeInfoForDay[] {
288
+ const date = new Date(year, month, day);
289
+ const dayOfWeek = date.getDay();
290
+ const mondayBasedDay = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
291
+
292
+ const daySchedules = schedule.filter((s) => s.dayOfWeek === mondayBasedDay);
293
+
294
+ const wasteTypes: WasteTypeInfoForDay[] = [];
295
+ for (const s of daySchedules) {
296
+ if (s.biweekly && s.startDate) {
297
+ const weeksSinceStart = getWeeksSinceStart(s.startDate, date);
298
+ if (weeksSinceStart >= 0 && weeksSinceStart % 2 === 0) {
299
+ wasteTypes.push({ wasteTypeId: s.wasteTypeId, color: s.color });
300
+ }
301
+ } else if (!s.biweekly) {
302
+ wasteTypes.push({ wasteTypeId: s.wasteTypeId, color: s.color });
303
+ }
304
+ }
305
+
306
+ // Filtre des exceptions: si l’exception cible un wasteTypeId précis, on ne garde pas ce type pendant la période.
307
+ const filtered: WasteTypeInfoForDay[] = [];
308
+ for (const wt of wasteTypes) {
309
+ const hasActiveException = exceptions.some((exc) => {
310
+ if (exc?.wasteTypeId && exc.wasteTypeId !== wt.wasteTypeId) return false;
311
+ const from = exceptionBoundaryToLocalDate(exc.dateFrom);
312
+ const to = exceptionBoundaryToLocalDate(exc.dateTo);
313
+ return date.getTime() >= from.getTime() && date.getTime() < to.getTime();
314
+ });
315
+ if (!hasActiveException) {
316
+ filtered.push({ ...wt, hasActiveException: false });
317
+ }
318
+ }
319
+
320
+ // “Retour” d’exception: quand on tombe exactement sur dateTo, on peut marquer le type comme exception (UI ✓).
321
+ for (const exc of exceptions) {
322
+ if (!exc?.wasteTypeId) continue;
323
+ const dateToBoundary = exceptionBoundaryToLocalDate(exc.dateTo);
324
+ if (date.getTime() !== dateToBoundary.getTime()) continue;
325
+ const matching = schedule.find((s) => s.wasteTypeId === exc.wasteTypeId);
326
+ if (!matching) continue;
327
+ if (!filtered.some((x) => x.wasteTypeId === matching.wasteTypeId)) {
328
+ filtered.push({
329
+ wasteTypeId: matching.wasteTypeId,
330
+ color: matching.color,
331
+ isException: true,
332
+ hasActiveException: false,
333
+ });
334
+ }
335
+ }
336
+
337
+ return filtered;
338
+ }
339
+
340
+ export function useCollectionCalendar(options: UseCollectionCalendarOptions = {}): UseCollectionCalendarResult {
341
+ const { selectedCityId, selectedPlaceId: localPlaceId } = useLocal();
342
+
343
+ const resolvedCityId = options.cityId ?? selectedCityId ?? null;
344
+ const resolvedPlaceIdOption = options.selectedPlaceId ?? localPlaceId ?? null;
345
+ const today = new Date();
346
+ const initialMonth = clampMonth(options.initialMonth ?? today.getMonth());
347
+ const initialYear = Number.isFinite(options.initialYear) ? (options.initialYear as number) : today.getFullYear();
348
+
349
+ const [currentMonth, setCurrentMonth] = useState<number>(initialMonth);
350
+ const [currentYear, setCurrentYear] = useState<number>(initialYear);
351
+
352
+ const { wasteTypes, wasteCollectionPlaces, isLoading, error } = useNextCollection({
353
+ cityId: resolvedCityId,
354
+ selectedPlaceId: resolvedPlaceIdOption,
355
+ });
356
+
357
+ const { schedule, exceptions, resolvedPlaceId } = useMemo(
358
+ () => buildSchedule(wasteTypes, wasteCollectionPlaces, resolvedPlaceIdOption),
359
+ [wasteTypes, wasteCollectionPlaces, resolvedPlaceIdOption]
360
+ );
361
+
362
+ const goToPrevMonth = useCallback(() => {
363
+ setCurrentMonth((m) => {
364
+ if (m === 0) {
365
+ setCurrentYear((y) => y - 1);
366
+ return 11;
367
+ }
368
+ return m - 1;
369
+ });
370
+ }, []);
371
+
372
+ const goToNextMonth = useCallback(() => {
373
+ setCurrentMonth((m) => {
374
+ if (m === 11) {
375
+ setCurrentYear((y) => y + 1);
376
+ return 0;
377
+ }
378
+ return m + 1;
379
+ });
380
+ }, []);
381
+
382
+ const weeks = useMemo(() => {
383
+ const base = buildCalendarWeeks(currentYear, currentMonth);
384
+ return enrichCalendarWeeks(base, schedule, exceptions);
385
+ }, [currentYear, currentMonth, schedule, exceptions]);
386
+ const weekDays = useMemo(() => ["Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"], []);
387
+
388
+ const getWasteTypesForDate = useCallback(
389
+ (y: number, m: number, d: number) => computeWasteTypesForDay(d, y, m, schedule, exceptions),
390
+ [schedule, exceptions]
391
+ );
392
+
393
+ const getWasteTypesForDay = useCallback(
394
+ (day: number) => computeWasteTypesForDay(day, currentYear, currentMonth, schedule, exceptions),
395
+ [currentYear, currentMonth, schedule, exceptions]
396
+ );
397
+
398
+ const monthLabel = useMemo(() => `${MONTHS[currentMonth]} ${currentYear}`, [currentMonth, currentYear]);
399
+
400
+ return {
401
+ cityId: resolvedCityId,
402
+ isLoading,
403
+ error,
404
+
405
+ currentMonth,
406
+ currentYear,
407
+ monthLabel,
408
+ setCurrentMonth,
409
+ setCurrentYear,
410
+ goToPrevMonth,
411
+ goToNextMonth,
412
+
413
+ weekDays,
414
+ weeks,
415
+
416
+ selectedPlaceId: resolvedPlaceId,
417
+
418
+ getWasteTypesForDate,
419
+ getWasteTypesForDay,
420
+ };
421
+ }
@@ -0,0 +1,20 @@
1
+ import { useMemo } from "react";
2
+
3
+ export function useColumns<T = any>(
4
+ data: T[],
5
+ itemsPerRow: number = 4,
6
+ ): (T | null)[][] {
7
+ return useMemo(() => {
8
+ const rows: (T | null)[][] = [];
9
+
10
+ for (let i = 0; i < data.length; i += itemsPerRow) {
11
+ const row: (T | null)[] = data.slice(i, i + itemsPerRow);
12
+ while (row.length < itemsPerRow) {
13
+ row.push(null);
14
+ }
15
+ rows.push(row);
16
+ }
17
+
18
+ return rows;
19
+ }, [data]);
20
+ }
@@ -0,0 +1,62 @@
1
+ import { useMemo } from "react";
2
+ import { normalizeWasteTypeToRecyclingInfo } from "../helpers/normalizeWasteTypeRecyclingInfo";
3
+ import { useNextCollection } from "./useNextCollection";
4
+
5
+ export type WasteTypeForConsignes = {
6
+ name: string;
7
+ imageUrl?: string;
8
+ rules?: string[];
9
+ color?: string;
10
+ details?: Array<{
11
+ id: string | number;
12
+ material?: string;
13
+ description?: string;
14
+ imageUrl?: string;
15
+ }>;
16
+ };
17
+
18
+ export type CityForConsignes = {
19
+ general_collection_rules?: Record<string, string>;
20
+ [key: string]: unknown;
21
+ };
22
+
23
+ export type UseConsignesResult = {
24
+ currentCity: CityForConsignes | null;
25
+ wasteType: WasteTypeForConsignes | null;
26
+ isLoading: boolean;
27
+ error: string | null;
28
+ };
29
+
30
+ /**
31
+ * Prépare les données pour la modale de consignes à partir de:
32
+ * - la ville courante (useNextCollection.city)
33
+ * - la liste des wasteTypes (useNextCollection.wasteTypes)
34
+ */
35
+ export function useConsignes(wasteTypeId: string | null | undefined): UseConsignesResult {
36
+ const { city, wasteTypes, isLoading, error } = useNextCollection();
37
+
38
+ const wasteType = useMemo<WasteTypeForConsignes | null>(() => {
39
+ if (!wasteTypeId) return null;
40
+
41
+ const wt = (wasteTypes ?? []).find((w) => w.id === wasteTypeId);
42
+ if (!wt) return null;
43
+
44
+ const info = normalizeWasteTypeToRecyclingInfo(wt);
45
+ if (!info) return null;
46
+
47
+ return {
48
+ name: info.name,
49
+ imageUrl: info.imageUrl,
50
+ rules: info.rules,
51
+ details: info.details,
52
+ color: info.color
53
+ };
54
+ }, [wasteTypeId, wasteTypes]);
55
+
56
+ return {
57
+ currentCity: (city as any) ?? null,
58
+ wasteType,
59
+ isLoading,
60
+ error,
61
+ };
62
+ }