@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,621 @@
1
+ import {
2
+ Dimensions,
3
+ Platform,
4
+ Pressable,
5
+ StyleSheet,
6
+ Text,
7
+ View,
8
+ type NativeScrollEvent,
9
+ type NativeSyntheticEvent,
10
+ } from "react-native";
11
+ import { Image } from "expo-image";
12
+ import Animated, {
13
+ Extrapolation,
14
+ FadeInRight,
15
+ FadeOutLeft,
16
+ interpolate,
17
+ interpolateColor,
18
+ runOnJS,
19
+ type SharedValue,
20
+ useAnimatedReaction,
21
+ useAnimatedScrollHandler,
22
+ useAnimatedStyle,
23
+ useSharedValue,
24
+ } from "react-native-reanimated";
25
+ import { useNextCollection } from "../../hooks/useNextCollection";
26
+ import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react-native";
27
+ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
28
+ import { ConsignesModal } from "../consignes-modal/ConsignesModal";
29
+ import { NavButton } from "../nav-button/NavButton";
30
+ import { Button } from "../button/Button";
31
+ import { ContentInset } from "../content-inset/ContentInset";
32
+ import { useTheme } from "../../contexts/ThemeContext";
33
+ import { cardStyle, resolveTheme } from "../../helpers/themeStyles";
34
+ import { hapticSelection } from "../../helpers/haptics";
35
+ import { resolveApiAssetUrl } from "../../helpers/resolveApiAssetUrl";
36
+ import { useWasto } from "../../contexts/WastoContext";
37
+ import { SPACE } from "../../helpers/layout";
38
+ import type { Theme } from "../../types/Theme";
39
+ import { useAppNavigation } from "../../navigation/NavigationContext";
40
+
41
+ export type NextCollectionProps = {
42
+ /** Route / screen name for the calendar CTA. Defaults to trying common tab paths. */
43
+ calendarHref?: string;
44
+ /** Show calendar CTA under the slider (home mockup). Default true. */
45
+ showCalendarCta?: boolean;
46
+ /**
47
+ * Initial day offset relative to today (0 = today, 1 = tomorrow, -1 = yesterday).
48
+ * User can still navigate with prev/next.
49
+ */
50
+ dayOffset?: number;
51
+ };
52
+
53
+ /** Maquette: side cards ≈ 174/196 of active height */
54
+ const SIDE_SCALE = 174 / 196;
55
+ const CARD_GAP = 12;
56
+ /** Side inset so current card is centered with a peek of the next (~maquette). */
57
+ const SIDE_INSET = 20;
58
+ const CARD_ASPECT = 196 / 286;
59
+ /** White contour around the light-grey panel (screenshot). */
60
+ const FRAME_PAD = 10;
61
+ /**
62
+ * Figma Ellipse 4 on card 286×196 — only the top of a huge circle is visible
63
+ * (~52px), as a white bump on the bottom border.
64
+ */
65
+ const BUMP = {
66
+ w: 813 / 286,
67
+ /** Visible bump height / card height (196 − 144) / 196 */
68
+ visible: 52 / 196,
69
+ } as const;
70
+
71
+ type CollectionItem = {
72
+ id: string;
73
+ name: string;
74
+ color?: string | null;
75
+ imageUrl?: string | null;
76
+ icon?: string;
77
+ };
78
+
79
+ function CollectionSlide({
80
+ item,
81
+ index,
82
+ scrollX,
83
+ cardWidth,
84
+ cardHeight,
85
+ snapInterval,
86
+ theme,
87
+ imageUri,
88
+ onPress,
89
+ isLast,
90
+ }: {
91
+ item: CollectionItem;
92
+ index: number;
93
+ scrollX: SharedValue<number>;
94
+ cardWidth: number;
95
+ cardHeight: number;
96
+ snapInterval: number;
97
+ theme: Theme;
98
+ imageUri: string | null;
99
+ onPress: () => void;
100
+ isLast: boolean;
101
+ }) {
102
+ const t = resolveTheme(theme);
103
+ const accent = item.color ?? theme.colors.primary;
104
+ const imageSize = Math.round(cardHeight * 0.55);
105
+ const panelW = cardWidth - FRAME_PAD * 2;
106
+ // True circle (square + radius/2). A non-square + borderRadius 9999
107
+ // renders as a rounded rectangle / pill on RN web.
108
+ const bumpSize = cardWidth * BUMP.w;
109
+ const bumpVisible = cardHeight * BUMP.visible;
110
+
111
+ const animatedStyle = useAnimatedStyle(() => {
112
+ const center = index * snapInterval;
113
+ const scale = interpolate(
114
+ scrollX.value,
115
+ [center - snapInterval, center, center + snapInterval],
116
+ [SIDE_SCALE, 1, SIDE_SCALE],
117
+ Extrapolation.CLAMP,
118
+ );
119
+ return {
120
+ transform: [{ scale }],
121
+ };
122
+ }, [index, snapInterval]);
123
+
124
+ return (
125
+ <Animated.View
126
+ style={[
127
+ styles.slide,
128
+ {
129
+ width: cardWidth,
130
+ height: cardHeight,
131
+ marginRight: isLast ? 0 : CARD_GAP,
132
+ ...(Platform.OS === "web"
133
+ ? ({ scrollSnapAlign: "center", scrollSnapStop: "always" } as object)
134
+ : null),
135
+ },
136
+ animatedStyle,
137
+ ]}
138
+ >
139
+ <Pressable
140
+ onPress={onPress}
141
+ style={[
142
+ styles.cardFrame,
143
+ {
144
+ borderRadius: t.radii.lg,
145
+ backgroundColor: "#FFFFFF",
146
+ borderWidth: 1,
147
+ borderColor: theme.colors.border,
148
+ padding: FRAME_PAD,
149
+ ...t.shadow.card,
150
+ },
151
+ ]}
152
+ >
153
+ {/* Light-grey interior — white bump is the frame swelling up */}
154
+ <View
155
+ style={[
156
+ styles.cardPanel,
157
+ {
158
+ backgroundColor: "#F0F0F0",
159
+ borderRadius: Math.max(10, t.radii.lg - 6),
160
+ },
161
+ ]}
162
+ >
163
+ <View
164
+ pointerEvents="none"
165
+ style={{
166
+ position: "absolute",
167
+ width: bumpSize,
168
+ height: bumpSize,
169
+ borderRadius: bumpSize / 2,
170
+ left: (panelW - bumpSize) / 2,
171
+ bottom: -(bumpSize - bumpVisible),
172
+ backgroundColor: "#FFFFFF",
173
+ }}
174
+ />
175
+
176
+ <View style={styles.cardContent}>
177
+ <Text
178
+ style={[styles.wasteName, { color: theme.colors.onSurface }]}
179
+ numberOfLines={2}
180
+ >
181
+ {item.name}
182
+ </Text>
183
+ {imageUri ? (
184
+ <Image
185
+ source={imageUri}
186
+ style={{ width: imageSize, height: imageSize }}
187
+ contentFit="contain"
188
+ />
189
+ ) : (
190
+ <View
191
+ style={{
192
+ width: imageSize,
193
+ height: imageSize,
194
+ backgroundColor: `${accent}22`,
195
+ borderRadius: t.radii.md,
196
+ }}
197
+ />
198
+ )}
199
+ </View>
200
+ </View>
201
+ </Pressable>
202
+ </Animated.View>
203
+ );
204
+ }
205
+
206
+ /** Animated dots driven by scrollX. Maquette: pill 16×6 / dot 6×6. */
207
+ function CollectionPagination({
208
+ count,
209
+ scrollX,
210
+ snapInterval,
211
+ activeColor,
212
+ inactiveColor,
213
+ pageLabelColor,
214
+ }: {
215
+ count: number;
216
+ scrollX: SharedValue<number>;
217
+ snapInterval: number;
218
+ activeColor: string;
219
+ inactiveColor: string;
220
+ pageLabelColor: string;
221
+ }) {
222
+ const [pageIndex, setPageIndex] = useState(0);
223
+
224
+ useAnimatedReaction(
225
+ () => Math.round(scrollX.value / Math.max(snapInterval, 1)),
226
+ (current, previous) => {
227
+ if (current !== previous) {
228
+ const clamped = Math.max(0, Math.min(current, count - 1));
229
+ runOnJS(setPageIndex)(clamped);
230
+ }
231
+ },
232
+ [snapInterval, count],
233
+ );
234
+
235
+ return (
236
+ <View style={styles.pagination}>
237
+ {Array.from({ length: count }, (_, idx) => (
238
+ <PaginationDot
239
+ key={idx}
240
+ index={idx}
241
+ scrollX={scrollX}
242
+ snapInterval={snapInterval}
243
+ activeColor={activeColor}
244
+ inactiveColor={inactiveColor}
245
+ />
246
+ ))}
247
+ <Text style={[styles.pageCount, { color: pageLabelColor }]}>
248
+ {pageIndex + 1}/{count}
249
+ </Text>
250
+ </View>
251
+ );
252
+ }
253
+
254
+ function PaginationDot({
255
+ index,
256
+ scrollX,
257
+ snapInterval,
258
+ activeColor,
259
+ inactiveColor,
260
+ }: {
261
+ index: number;
262
+ scrollX: SharedValue<number>;
263
+ snapInterval: number;
264
+ activeColor: string;
265
+ inactiveColor: string;
266
+ }) {
267
+ const animatedStyle = useAnimatedStyle(() => {
268
+ const inputRange = [
269
+ (index - 1) * snapInterval,
270
+ index * snapInterval,
271
+ (index + 1) * snapInterval,
272
+ ];
273
+ return {
274
+ width: interpolate(
275
+ scrollX.value,
276
+ inputRange,
277
+ [6, 16, 6],
278
+ Extrapolation.CLAMP,
279
+ ),
280
+ opacity: interpolate(
281
+ scrollX.value,
282
+ inputRange,
283
+ [0.4, 1, 0.4],
284
+ Extrapolation.CLAMP,
285
+ ),
286
+ backgroundColor: interpolateColor(scrollX.value, inputRange, [
287
+ inactiveColor,
288
+ activeColor,
289
+ inactiveColor,
290
+ ]),
291
+ };
292
+ }, [index, snapInterval, activeColor, inactiveColor]);
293
+
294
+ return <Animated.View style={[styles.dotBase, animatedStyle]} />;
295
+ }
296
+
297
+ export function NextCollection({
298
+ calendarHref,
299
+ showCalendarCta = true,
300
+ dayOffset = 0,
301
+ }: NextCollectionProps): ReactNode {
302
+ const { theme } = useTheme();
303
+ const { apiUrl } = useWasto();
304
+ const { push } = useAppNavigation();
305
+ const t = resolveTheme(theme);
306
+ const [offset, setOffset] = useState(() => Math.trunc(dayOffset));
307
+ const { collectionsForDay, dateLabel } = useNextCollection({
308
+ dateOffset: offset,
309
+ });
310
+
311
+ useEffect(() => {
312
+ setOffset(Math.trunc(dayOffset));
313
+ }, [dayOffset]);
314
+
315
+ const [selectedWasteTypeId, setSelectedWasteTypeId] = useState<string | null>(null);
316
+ const listRef = useRef<Animated.FlatList<CollectionItem>>(null);
317
+ const scrollX = useSharedValue(0);
318
+ const lastSnappedIndex = useRef(0);
319
+
320
+ const screenWidth = Dimensions.get("window").width;
321
+ // Near full-bleed with SIDE_INSET peek — matches maquette presence (286 on 402 ≈ side 16–58)
322
+ const cardWidth = Math.round(screenWidth - SIDE_INSET * 2);
323
+ const cardHeight = Math.round(cardWidth * CARD_ASPECT);
324
+ const sidePad = SIDE_INSET;
325
+ const snapInterval = cardWidth + CARD_GAP;
326
+
327
+ const snapOffsets = useMemo(
328
+ () => collectionsForDay.map((_, i) => i * snapInterval),
329
+ [collectionsForDay, snapInterval],
330
+ );
331
+
332
+ const imageSource = (item: CollectionItem) => {
333
+ const fromUrl = resolveApiAssetUrl(apiUrl, item.imageUrl);
334
+ if (fromUrl) return fromUrl;
335
+ if (
336
+ typeof item.icon === "string" &&
337
+ item.icon.trim() &&
338
+ /^https?:\/\//i.test(item.icon)
339
+ ) {
340
+ return item.icon.trim();
341
+ }
342
+ return null;
343
+ };
344
+
345
+ const scrollHandler = useAnimatedScrollHandler(
346
+ {
347
+ onScroll: (e) => {
348
+ scrollX.value = e.contentOffset.x;
349
+ },
350
+ },
351
+ [],
352
+ );
353
+
354
+ const snapToNearest = useCallback(
355
+ (x: number) => {
356
+ if (collectionsForDay.length === 0) return;
357
+ const index = Math.round(x / snapInterval);
358
+ const clamped = Math.max(0, Math.min(index, collectionsForDay.length - 1));
359
+ const target = clamped * snapInterval;
360
+ if (clamped !== lastSnappedIndex.current) {
361
+ lastSnappedIndex.current = clamped;
362
+ void hapticSelection();
363
+ }
364
+ if (Math.abs(x - target) > 1) {
365
+ listRef.current?.scrollToOffset({ offset: target, animated: true });
366
+ }
367
+ scrollX.value = target;
368
+ },
369
+ [collectionsForDay.length, snapInterval, scrollX],
370
+ );
371
+
372
+ const onScrollSettle = useCallback(
373
+ (
374
+ e:
375
+ | NativeSyntheticEvent<NativeScrollEvent>
376
+ | { nativeEvent: { contentOffset: { x: number } } },
377
+ ) => {
378
+ snapToNearest(e.nativeEvent.contentOffset.x);
379
+ },
380
+ [snapToNearest],
381
+ );
382
+
383
+ const changeDate = async (delta: number) => {
384
+ await hapticSelection();
385
+ setOffset((o) => o + delta);
386
+ lastSnappedIndex.current = 0;
387
+ scrollX.value = 0;
388
+ requestAnimationFrame(() => {
389
+ listRef.current?.scrollToOffset({ offset: 0, animated: true });
390
+ });
391
+ };
392
+
393
+ const openCalendar = () => {
394
+ const normalizeHref = (href: string) =>
395
+ href.startsWith("/") ? href : `/${href}`;
396
+ const candidates = [
397
+ calendarHref,
398
+ "/(tabs)/calendrier",
399
+ "/(tabs)/calendar",
400
+ "/calendrier",
401
+ "/calendar",
402
+ ]
403
+ .filter(Boolean)
404
+ .map((href) => normalizeHref(href as string));
405
+
406
+ for (const href of candidates) {
407
+ try {
408
+ push(href);
409
+ return;
410
+ } catch {
411
+ // try next
412
+ }
413
+ }
414
+ };
415
+
416
+ return (
417
+ <>
418
+ <View style={styles.section}>
419
+ <ContentInset>
420
+ <View style={styles.header}>
421
+ <Animated.Text
422
+ key={dateLabel}
423
+ entering={FadeInRight.duration(220)}
424
+ exiting={FadeOutLeft.duration(160)}
425
+ style={[styles.sectionLabel, { color: t.colors.muted }]}
426
+ >
427
+ {dateLabel.toUpperCase()}
428
+ </Animated.Text>
429
+ <View style={styles.navRow}>
430
+ <NavButton
431
+ icon={ChevronLeftIcon}
432
+ onPress={() => changeDate(-1)}
433
+ style={styles.navCircle}
434
+ />
435
+ <NavButton
436
+ icon={ChevronRightIcon}
437
+ onPress={() => changeDate(1)}
438
+ style={styles.navCircle}
439
+ />
440
+ </View>
441
+ </View>
442
+ </ContentInset>
443
+
444
+ {collectionsForDay.length === 0 ? (
445
+ <ContentInset>
446
+ <View
447
+ style={[
448
+ styles.emptyCard,
449
+ cardStyle(theme),
450
+ {
451
+ borderRadius: t.radii.lg,
452
+ minHeight: cardHeight,
453
+ },
454
+ ]}
455
+ >
456
+ <Text style={[styles.emptyText, { color: t.colors.muted }]}>
457
+ Rien pour aujourd'hui
458
+ </Text>
459
+ </View>
460
+ </ContentInset>
461
+ ) : (
462
+ <View key={offset}>
463
+ <Animated.FlatList
464
+ ref={listRef}
465
+ data={collectionsForDay as CollectionItem[]}
466
+ keyExtractor={(item) => item.id}
467
+ horizontal
468
+ showsHorizontalScrollIndicator={false}
469
+ decelerationRate="fast"
470
+ snapToInterval={Platform.OS === "web" ? undefined : snapInterval}
471
+ snapToOffsets={Platform.OS === "web" ? undefined : snapOffsets}
472
+ snapToAlignment="center"
473
+ disableIntervalMomentum
474
+ bounces={false}
475
+ scrollEventThrottle={16}
476
+ onScroll={scrollHandler}
477
+ onScrollEndDrag={onScrollSettle}
478
+ onMomentumScrollEnd={onScrollSettle}
479
+ contentContainerStyle={{
480
+ paddingHorizontal: sidePad,
481
+ alignItems: "center",
482
+ }}
483
+ style={[
484
+ { height: cardHeight },
485
+ Platform.OS === "web"
486
+ ? ({
487
+ scrollSnapType: "x mandatory",
488
+ WebkitOverflowScrolling: "touch",
489
+ } as object)
490
+ : null,
491
+ ]}
492
+ renderItem={({ item, index }) => (
493
+ <CollectionSlide
494
+ item={item}
495
+ index={index}
496
+ scrollX={scrollX}
497
+ cardWidth={cardWidth}
498
+ cardHeight={cardHeight}
499
+ snapInterval={snapInterval}
500
+ theme={theme}
501
+ imageUri={imageSource(item)}
502
+ isLast={index === collectionsForDay.length - 1}
503
+ onPress={() => setSelectedWasteTypeId(item.id)}
504
+ />
505
+ )}
506
+ />
507
+
508
+ <CollectionPagination
509
+ count={collectionsForDay.length}
510
+ scrollX={scrollX}
511
+ snapInterval={snapInterval}
512
+ activeColor={theme.colors.primary}
513
+ inactiveColor={theme.colors.border}
514
+ pageLabelColor={t.colors.muted}
515
+ />
516
+ </View>
517
+ )}
518
+
519
+ {showCalendarCta ? (
520
+ <ContentInset>
521
+ <Button
522
+ onPress={openCalendar}
523
+ style={[styles.cta, { borderRadius: t.radii.md }]}
524
+ >
525
+ Afficher le calendrier
526
+ </Button>
527
+ </ContentInset>
528
+ ) : null}
529
+ </View>
530
+
531
+ <ConsignesModal
532
+ visible={!!selectedWasteTypeId}
533
+ wasteTypeId={selectedWasteTypeId!}
534
+ onClose={() => setSelectedWasteTypeId(null)}
535
+ />
536
+ </>
537
+ );
538
+ }
539
+
540
+ const styles = StyleSheet.create({
541
+ section: {
542
+ gap: 14,
543
+ },
544
+ header: {
545
+ flexDirection: "row",
546
+ alignItems: "center",
547
+ justifyContent: "space-between",
548
+ },
549
+ sectionLabel: {
550
+ fontSize: 13,
551
+ fontWeight: "700",
552
+ letterSpacing: 0.6,
553
+ },
554
+ navRow: {
555
+ flexDirection: "row",
556
+ gap: 8,
557
+ },
558
+ navCircle: {
559
+ width: SPACE.iconHit,
560
+ height: SPACE.iconHit,
561
+ },
562
+ slide: {
563
+ justifyContent: "center",
564
+ },
565
+ cardFrame: {
566
+ flex: 1,
567
+ overflow: "hidden",
568
+ },
569
+ cardPanel: {
570
+ flex: 1,
571
+ overflow: "hidden",
572
+ position: "relative",
573
+ },
574
+ cardContent: {
575
+ flex: 1,
576
+ zIndex: 1,
577
+ flexDirection: "row",
578
+ alignItems: "center",
579
+ justifyContent: "space-between",
580
+ gap: 12,
581
+ paddingLeft: 14,
582
+ paddingRight: 12,
583
+ paddingVertical: 14,
584
+ },
585
+ wasteName: {
586
+ flex: 1,
587
+ fontSize: 22,
588
+ fontWeight: "800",
589
+ lineHeight: 28,
590
+ },
591
+ pagination: {
592
+ flexDirection: "row",
593
+ alignItems: "center",
594
+ justifyContent: "center",
595
+ width: "100%",
596
+ gap: 6,
597
+ marginTop: 10,
598
+ paddingHorizontal: SPACE.pageX,
599
+ },
600
+ dotBase: {
601
+ height: 6,
602
+ borderRadius: 3,
603
+ },
604
+ pageCount: {
605
+ marginLeft: 4,
606
+ fontSize: 12,
607
+ fontWeight: "600",
608
+ },
609
+ emptyCard: {
610
+ padding: 24,
611
+ alignItems: "center",
612
+ justifyContent: "center",
613
+ },
614
+ emptyText: {
615
+ fontSize: 14,
616
+ fontWeight: "600",
617
+ },
618
+ cta: {
619
+ height: SPACE.buttonH,
620
+ },
621
+ });