@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,231 @@
1
+ import { Dimensions, Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
2
+ import { ChallengeSeriesWithProgress, ChallengeWithProgress } from "../../hooks/useChallenges";
3
+ import { Title } from "../title/Title";
4
+ import { NavButton } from "../nav-button/NavButton";
5
+ import { Play, Trophy, XIcon } from "lucide-react-native";
6
+ import { Theme } from "../../types/Theme";
7
+ import { useTheme } from "../../contexts/ThemeContext";
8
+ import { useSafeAreaPadding } from "../../helpers/useSafeAreaPadding";
9
+ import { useState, type ReactNode } from "react";
10
+ import { QuizModal } from "./QuizModal";
11
+ import { resolveTheme } from "../../helpers/themeStyles";
12
+ import { SPACE } from "../../helpers/layout";
13
+
14
+ export type ChallengeModalProps = {
15
+ challenge: ChallengeWithProgress;
16
+ onClose: () => void;
17
+ };
18
+
19
+ function isSerieComplete(serie: ChallengeSeriesWithProgress) {
20
+ return !!serie.progression;
21
+ }
22
+
23
+ function serieProgressRatio(serie: ChallengeSeriesWithProgress) {
24
+ return Math.min(1, Math.max(0, (serie.progression?.score ?? 0) / 100));
25
+ }
26
+
27
+ function serieProgressLabel(serie: ChallengeSeriesWithProgress) {
28
+ return `${Math.round(serie.progression?.score ?? 0)}%`;
29
+ }
30
+
31
+ export function ChallengeModal({
32
+ challenge,
33
+ onClose,
34
+ }: ChallengeModalProps): ReactNode {
35
+ const { theme } = useTheme();
36
+ const t = resolveTheme(theme);
37
+ const themedStyles = createThemedStyles(theme);
38
+ const topInset = useSafeAreaPadding(["top"]);
39
+ const bottomInset = useSafeAreaPadding(["bottom"]);
40
+ const [selectedSerieId, setSelectedSerieId] = useState<string | null>(null);
41
+ const series = challenge.series ?? [];
42
+ const selectedSerie =
43
+ selectedSerieId != null
44
+ ? (series.find((s) => s.id === selectedSerieId) ?? null)
45
+ : null;
46
+
47
+ const handleSerieSelected = (serie: ChallengeSeriesWithProgress) => () => {
48
+ setSelectedSerieId(serie.id);
49
+ };
50
+
51
+ return (
52
+ <>
53
+ {!selectedSerie ? (
54
+ <View style={[styles.modal, themedStyles.modal]}>
55
+ <View style={topInset}>
56
+ <View style={styles.header}>
57
+ <Title text={challenge.name} level="2" numberOfLines={1} />
58
+ <NavButton icon={XIcon} onPress={onClose} />
59
+ </View>
60
+ </View>
61
+ <View style={[styles.listWrap, bottomInset]}>
62
+ <ScrollView bounces={false} contentContainerStyle={styles.body}>
63
+ {series.length === 0 ? (
64
+ <Text style={styles.empty}>Aucune série pour ce challenge.</Text>
65
+ ) : (
66
+ series.map((serie) => {
67
+ const done = isSerieComplete(serie);
68
+ const ratio = serieProgressRatio(serie);
69
+ return (
70
+ <Pressable
71
+ key={serie.id}
72
+ style={themedStyles.serie}
73
+ onPress={handleSerieSelected(serie)}
74
+ >
75
+ <View style={styles.serieRow}>
76
+ <View style={styles.serieMain}>
77
+ <Title text={serie.name} level="3" />
78
+ {serie.description ? (
79
+ <Text numberOfLines={1}>
80
+ {serie.description}
81
+ </Text>
82
+ ) : null}
83
+ <View
84
+ style={[
85
+ styles.track,
86
+ {
87
+ backgroundColor: theme.colors.border,
88
+ borderRadius: t.radii.pill,
89
+ },
90
+ ]}
91
+ >
92
+ <View
93
+ style={[
94
+ styles.fill,
95
+ {
96
+ width: `${Math.round(ratio * 100)}%`,
97
+ backgroundColor: done
98
+ ? theme.colors.success
99
+ : theme.colors.primary,
100
+ borderRadius: t.radii.pill,
101
+ },
102
+ ]}
103
+ >
104
+ <Text
105
+ style={[
106
+ styles.fillLabel,
107
+ {
108
+ color: done
109
+ ? theme.colors.onSuccess
110
+ : theme.colors.onPrimary,
111
+ },
112
+ ]}
113
+ >
114
+ {serieProgressLabel(serie)}
115
+ </Text>
116
+ </View>
117
+ </View>
118
+ </View>
119
+ <View
120
+ style={[
121
+ styles.actionBtn,
122
+ {
123
+ backgroundColor: done
124
+ ? theme.colors.success
125
+ : theme.colors.primary,
126
+ borderRadius: t.radii.pill,
127
+ },
128
+ ]}
129
+ >
130
+ {done ? (
131
+ <Trophy
132
+ size={18}
133
+ color={theme.colors.onSuccess}
134
+ />
135
+ ) : (
136
+ <Play
137
+ size={18}
138
+ color={theme.colors.onPrimary}
139
+ fill={theme.colors.onPrimary}
140
+ />
141
+ )}
142
+ </View>
143
+ </View>
144
+ </Pressable>
145
+ );
146
+ })
147
+ )}
148
+ </ScrollView>
149
+ </View>
150
+ </View>
151
+ ) : (
152
+ <QuizModal
153
+ serie={selectedSerie}
154
+ challengeId={challenge.id}
155
+ onClose={() => setSelectedSerieId(null)}
156
+ />
157
+ )}
158
+ </>
159
+ );
160
+ }
161
+
162
+ const createThemedStyles = (theme: Theme) =>
163
+ StyleSheet.create({
164
+ modal: {
165
+ backgroundColor: theme.colors.background,
166
+ },
167
+ serie: {
168
+ backgroundColor: theme.colors.surface,
169
+ borderRadius: theme.radii?.lg ?? theme.radius,
170
+ ...(theme.shadow?.soft ?? {}),
171
+ paddingHorizontal: SPACE.cardPad,
172
+ },
173
+ });
174
+
175
+ const styles = StyleSheet.create({
176
+ modal: {
177
+ height: Dimensions.get("screen").height,
178
+ },
179
+ header: {
180
+ paddingHorizontal: 16,
181
+ paddingVertical: 16,
182
+ width: "100%",
183
+ flexDirection: "row",
184
+ justifyContent: "space-between",
185
+ alignItems: "center",
186
+ gap: 12,
187
+ },
188
+ listWrap: {
189
+ flex: 1,
190
+ },
191
+ body: {
192
+ padding: 16,
193
+ gap: 12,
194
+ flexGrow: 1,
195
+ },
196
+ empty: {
197
+ opacity: 0.7,
198
+ },
199
+ serieRow: {
200
+ flexDirection: "row",
201
+ alignItems: "center",
202
+ gap: 16,
203
+ paddingVertical: 14,
204
+ },
205
+ serieMain: {
206
+ flex: 1,
207
+ gap: 12,
208
+ },
209
+ track: {
210
+ height: 16,
211
+ overflow: "hidden",
212
+ justifyContent: "center",
213
+ },
214
+ fill: {
215
+ height: "100%",
216
+ minWidth: 40,
217
+ justifyContent: "center",
218
+ paddingHorizontal: 8,
219
+ },
220
+ fillLabel: {
221
+ fontSize: 11,
222
+ fontWeight: "700",
223
+ textAlign: "center",
224
+ },
225
+ actionBtn: {
226
+ width: SPACE.iconHit,
227
+ height: SPACE.iconHit,
228
+ alignItems: "center",
229
+ justifyContent: "center",
230
+ },
231
+ });
@@ -0,0 +1,249 @@
1
+ import { useState, type ReactNode } from "react";
2
+ import { ActivityIndicator, Modal, Pressable, StyleSheet, Text, View } from "react-native";
3
+ import { Play, Trophy } from "lucide-react-native";
4
+ import { useTheme } from "../../contexts/ThemeContext";
5
+ import { useChallenges } from "../../hooks/useChallenges";
6
+ import type { ChallengeWithProgress } from "../../hooks/useChallenges";
7
+ import { cardStyle, resolveTheme } from "../../helpers/themeStyles";
8
+ import { SPACE } from "../../helpers/layout";
9
+ import { ChallengesModal } from "./ChallengesModal";
10
+ import { ChallengeModal } from "./ChallengeModal";
11
+
12
+ export type ChallengeVignetteProps = {};
13
+
14
+ function isChallengeComplete(c: ChallengeWithProgress) {
15
+ return !!c.progression?.badge;
16
+ }
17
+
18
+ function progressLabel(c: ChallengeWithProgress) {
19
+ const series = c.series ?? [];
20
+ if (series.length === 0) return isChallengeComplete(c) ? "1/1" : "0/1";
21
+ const done = series.filter((s) => s.progression).length;
22
+ return `${done}/${series.length}`;
23
+ }
24
+
25
+ function progressRatio(c: ChallengeWithProgress) {
26
+ const series = c.series ?? [];
27
+ if (series.length === 0) return isChallengeComplete(c) ? 1 : 0;
28
+ return series.filter((s) => s.progression).length / series.length;
29
+ }
30
+
31
+ export function ChallengeVignette({}: ChallengeVignetteProps): ReactNode {
32
+ const { theme } = useTheme();
33
+ const t = resolveTheme(theme);
34
+ const { challenges, isLoading, error } = useChallenges();
35
+ const [displayAll, setDisplayAll] = useState(false);
36
+ const [active, setActive] = useState<ChallengeWithProgress | null>(null);
37
+
38
+ const preview = challenges.slice(0, 3);
39
+
40
+ if (isLoading && !challenges.length) {
41
+ return (
42
+ <View style={styles.stateBox}>
43
+ <ActivityIndicator size="small" color={theme.colors.primary} />
44
+ </View>
45
+ );
46
+ }
47
+
48
+ if (error) {
49
+ return (
50
+ <View style={styles.stateBox}>
51
+ <Text style={{ color: theme.colors.error }}>{error}</Text>
52
+ </View>
53
+ );
54
+ }
55
+
56
+ return (
57
+ <>
58
+ <View style={styles.section}>
59
+ <View style={styles.sectionHeader}>
60
+ <Text style={[styles.sectionLabel, { color: t.colors.muted }]}>
61
+ DERNIERS DÉFIS
62
+ </Text>
63
+ <Pressable onPress={() => setDisplayAll(true)}>
64
+ <Text style={{ color: theme.colors.primary, fontWeight: "700" }}>
65
+ Tous les défis ›
66
+ </Text>
67
+ </Pressable>
68
+ </View>
69
+
70
+ <View style={[styles.card, cardStyle(theme), { borderRadius: t.radii.lg }]}>
71
+ {preview.length === 0 ? (
72
+ <Text style={[styles.empty, { color: t.colors.muted }]}>
73
+ Aucun défi disponible
74
+ </Text>
75
+ ) : (
76
+ preview.map((c, index) => {
77
+ const done = isChallengeComplete(c);
78
+ const ratio = progressRatio(c);
79
+ return (
80
+ <Pressable
81
+ key={c.id}
82
+ onPress={() => setActive(c)}
83
+ style={[
84
+ styles.row,
85
+ index < preview.length - 1 && {
86
+ borderBottomWidth: StyleSheet.hairlineWidth,
87
+ borderBottomColor: theme.colors.border,
88
+ },
89
+ ]}
90
+ >
91
+ <View style={styles.main}>
92
+ <Text
93
+ style={[
94
+ styles.title,
95
+ { color: theme.colors.onSurface },
96
+ ]}
97
+ numberOfLines={2}
98
+ >
99
+ {c.name}
100
+ </Text>
101
+ <View
102
+ style={[
103
+ styles.track,
104
+ {
105
+ backgroundColor: theme.colors.border,
106
+ borderRadius: t.radii.pill,
107
+ },
108
+ ]}
109
+ >
110
+ <View
111
+ style={[
112
+ styles.fill,
113
+ {
114
+ width: `${Math.round(ratio * 100)}%`,
115
+ backgroundColor: done
116
+ ? theme.colors.success
117
+ : theme.colors.primary,
118
+ borderRadius: t.radii.pill,
119
+ },
120
+ ]}
121
+ >
122
+ <Text
123
+ style={[
124
+ styles.fillLabel,
125
+ {
126
+ color: done
127
+ ? theme.colors.onSuccess
128
+ : theme.colors.onPrimary,
129
+ },
130
+ ]}
131
+ >
132
+ {progressLabel(c)}
133
+ </Text>
134
+ </View>
135
+ </View>
136
+ </View>
137
+ <View
138
+ style={[
139
+ styles.action,
140
+ {
141
+ backgroundColor: done
142
+ ? theme.colors.success
143
+ : theme.colors.primary,
144
+ borderRadius: t.radii.pill,
145
+ },
146
+ ]}
147
+ >
148
+ {done ? (
149
+ <Trophy size={16} color={theme.colors.onSuccess} />
150
+ ) : (
151
+ <Play
152
+ size={16}
153
+ color={theme.colors.onPrimary}
154
+ fill={theme.colors.onPrimary}
155
+ />
156
+ )}
157
+ </View>
158
+ </Pressable>
159
+ );
160
+ })
161
+ )}
162
+ </View>
163
+ </View>
164
+
165
+ <ChallengesModal
166
+ visible={displayAll}
167
+ challenges={challenges}
168
+ onClose={() => setDisplayAll(false)}
169
+ />
170
+ <Modal
171
+ visible={!!active}
172
+ animationType="slide"
173
+ onRequestClose={() => setActive(null)}
174
+ >
175
+ {active ? (
176
+ <ChallengeModal
177
+ challenge={active}
178
+ onClose={() => setActive(null)}
179
+ />
180
+ ) : null}
181
+ </Modal>
182
+ </>
183
+ );
184
+ }
185
+
186
+ const styles = StyleSheet.create({
187
+ section: {
188
+ gap: 10,
189
+ },
190
+ sectionHeader: {
191
+ flexDirection: "row",
192
+ justifyContent: "space-between",
193
+ alignItems: "center",
194
+ },
195
+ sectionLabel: {
196
+ fontSize: 12,
197
+ fontWeight: "700",
198
+ letterSpacing: 0.6,
199
+ },
200
+ card: {
201
+ paddingHorizontal: SPACE.cardPad,
202
+ overflow: "hidden",
203
+ },
204
+ stateBox: {
205
+ minHeight: 80,
206
+ justifyContent: "center",
207
+ alignItems: "center",
208
+ },
209
+ empty: {
210
+ padding: 20,
211
+ textAlign: "center",
212
+ },
213
+ row: {
214
+ flexDirection: "row",
215
+ alignItems: "center",
216
+ gap: 12,
217
+ paddingVertical: 14,
218
+ },
219
+ main: {
220
+ flex: 1,
221
+ gap: 8,
222
+ },
223
+ title: {
224
+ fontSize: 15,
225
+ fontWeight: "700",
226
+ },
227
+ track: {
228
+ height: 16,
229
+ overflow: "hidden",
230
+ justifyContent: "center",
231
+ },
232
+ fill: {
233
+ height: "100%",
234
+ minWidth: 40,
235
+ justifyContent: "center",
236
+ paddingHorizontal: 8,
237
+ },
238
+ fillLabel: {
239
+ fontSize: 11,
240
+ fontWeight: "700",
241
+ textAlign: "center",
242
+ },
243
+ action: {
244
+ width: SPACE.iconHit,
245
+ height: SPACE.iconHit,
246
+ alignItems: "center",
247
+ justifyContent: "center",
248
+ },
249
+ });