@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,243 @@
1
+ import { Dimensions, Modal, Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
2
+ import { ChallengeWithProgress } from "../../hooks/useChallenges";
3
+ import { Theme } from "../../types/Theme";
4
+ import { useTheme } from "../../contexts/ThemeContext";
5
+ import { Title } from "../title/Title";
6
+ import { NavButton } from "../nav-button/NavButton";
7
+ import { Play, Trophy, XIcon } from "lucide-react-native";
8
+ import { useEffect, useState, type ReactNode } from "react";
9
+ import { ChallengeModal } from "./ChallengeModal";
10
+ import { useSafeAreaPadding } from "../../helpers/useSafeAreaPadding";
11
+ import { resolveTheme } from "../../helpers/themeStyles";
12
+ import { SPACE } from "../../helpers/layout";
13
+
14
+ export type ChallengesModalProps = {
15
+ visible: boolean;
16
+ challenges: ChallengeWithProgress[];
17
+ onClose: () => void;
18
+ };
19
+
20
+ function isChallengeComplete(c: ChallengeWithProgress) {
21
+ return !!c.progression?.badge;
22
+ }
23
+
24
+ function progressLabel(c: ChallengeWithProgress) {
25
+ const series = c.series ?? [];
26
+ if (series.length === 0) return isChallengeComplete(c) ? "1/1" : "0/1";
27
+ const done = series.filter((s) => s.progression).length;
28
+ return `${done}/${series.length}`;
29
+ }
30
+
31
+ function progressRatio(c: ChallengeWithProgress) {
32
+ const series = c.series ?? [];
33
+ if (series.length === 0) return isChallengeComplete(c) ? 1 : 0;
34
+ return series.filter((s) => s.progression).length / series.length;
35
+ }
36
+
37
+ export function ChallengesModal({
38
+ visible,
39
+ challenges,
40
+ onClose,
41
+ }: ChallengesModalProps): ReactNode {
42
+ const { theme } = useTheme();
43
+ const t = resolveTheme(theme);
44
+ const themedStyles = createThemedStyles(theme);
45
+ const topInset = useSafeAreaPadding(["top"]);
46
+ const bottomInset = useSafeAreaPadding(["bottom"]);
47
+ const [selectedChallengeId, setSelectedChallengeId] = useState<string | null>(null);
48
+
49
+ const selectedChallenge =
50
+ selectedChallengeId != null
51
+ ? (challenges.find((c) => c.id === selectedChallengeId) ?? null)
52
+ : null;
53
+
54
+ useEffect(() => {
55
+ if (!visible) setSelectedChallengeId(null);
56
+ }, [visible]);
57
+
58
+ const handleSelectChallenge = (challenge: ChallengeWithProgress) => () => {
59
+ setSelectedChallengeId(challenge.id);
60
+ };
61
+
62
+ return <>
63
+ <Modal
64
+ visible={visible}
65
+ animationType="slide"
66
+ onRequestClose={onClose}
67
+ >
68
+ {!selectedChallenge ? (
69
+
70
+ <View style={[styles.modal, themedStyles.modal]}>
71
+ <View style={topInset}>
72
+ <View style={styles.header}>
73
+ <Title
74
+ text="Challenges"
75
+ level="2"
76
+ />
77
+ <NavButton
78
+ icon={XIcon}
79
+ onPress={onClose}
80
+ />
81
+ </View>
82
+ </View>
83
+ <View style={[styles.listWrap, bottomInset]}>
84
+ <ScrollView bounces={false} contentContainerStyle={styles.body}>
85
+ {challenges.map((challenge) => {
86
+ const done = isChallengeComplete(challenge);
87
+ const ratio = progressRatio(challenge);
88
+ return (
89
+ <Pressable
90
+ key={challenge.id}
91
+ style={themedStyles.challenge}
92
+ onPress={handleSelectChallenge(challenge)}
93
+ >
94
+ <View style={styles.challengeRow}>
95
+ <View style={styles.challengeMain}>
96
+ <Title text={challenge.name} level="3" />
97
+ {challenge.description ? (
98
+ <Text numberOfLines={1}>
99
+ {challenge.description}
100
+ </Text>
101
+ ) : null}
102
+ <View
103
+ style={[
104
+ styles.track,
105
+ {
106
+ backgroundColor: theme.colors.border,
107
+ borderRadius: t.radii.pill,
108
+ },
109
+ ]}
110
+ >
111
+ <View
112
+ style={[
113
+ styles.fill,
114
+ {
115
+ width: `${Math.round(ratio * 100)}%`,
116
+ backgroundColor: done
117
+ ? theme.colors.success
118
+ : theme.colors.primary,
119
+ borderRadius: t.radii.pill,
120
+ },
121
+ ]}
122
+ >
123
+ <Text
124
+ style={[
125
+ styles.fillLabel,
126
+ {
127
+ color: done
128
+ ? theme.colors.onSuccess
129
+ : theme.colors.onPrimary,
130
+ },
131
+ ]}
132
+ >
133
+ {progressLabel(challenge)}
134
+ </Text>
135
+ </View>
136
+ </View>
137
+ </View>
138
+ <View
139
+ style={[
140
+ styles.actionBtn,
141
+ {
142
+ backgroundColor: done
143
+ ? theme.colors.success
144
+ : theme.colors.primary,
145
+ borderRadius: t.radii.pill,
146
+ },
147
+ ]}
148
+ >
149
+ {done ? (
150
+ <Trophy
151
+ size={18}
152
+ color={theme.colors.onSuccess}
153
+ />
154
+ ) : (
155
+ <Play
156
+ size={18}
157
+ color={theme.colors.onPrimary}
158
+ fill={theme.colors.onPrimary}
159
+ />
160
+ )}
161
+ </View>
162
+ </View>
163
+ </Pressable>
164
+ );
165
+ })}
166
+ </ScrollView>
167
+ </View>
168
+ </View>
169
+ ) : (
170
+ <ChallengeModal
171
+ challenge={selectedChallenge!}
172
+ onClose={() => setSelectedChallengeId(null)}
173
+ />
174
+ )}
175
+ </Modal>
176
+ </>
177
+ }
178
+
179
+ const createThemedStyles = (theme: Theme) => StyleSheet.create({
180
+ modal: {
181
+ backgroundColor: theme.colors.background,
182
+ },
183
+ challenge: {
184
+ backgroundColor: theme.colors.surface,
185
+ borderRadius: theme.radii?.lg ?? theme.radius,
186
+ ...(theme.shadow?.soft ?? {}),
187
+ paddingHorizontal: SPACE.cardPad,
188
+ },
189
+ });
190
+
191
+ const styles = StyleSheet.create({
192
+ modal: {
193
+ height: Dimensions.get("screen").height,
194
+ },
195
+ header: {
196
+ paddingHorizontal: 16,
197
+ paddingVertical: 16,
198
+ width: "100%",
199
+ flexDirection: "row",
200
+ justifyContent: "space-between",
201
+ alignItems: "center",
202
+ },
203
+ listWrap: {
204
+ flex: 1,
205
+ },
206
+ body: {
207
+ padding: 16,
208
+ gap: 12,
209
+ flexGrow: 1,
210
+ },
211
+ challengeRow: {
212
+ flexDirection: "row",
213
+ alignItems: "center",
214
+ gap: 16,
215
+ paddingVertical: 14,
216
+ },
217
+ challengeMain: {
218
+ flex: 1,
219
+ gap: 12,
220
+ },
221
+ track: {
222
+ height: 16,
223
+ overflow: "hidden",
224
+ justifyContent: "center",
225
+ },
226
+ fill: {
227
+ height: "100%",
228
+ minWidth: 40,
229
+ justifyContent: "center",
230
+ paddingHorizontal: 8,
231
+ },
232
+ fillLabel: {
233
+ fontSize: 11,
234
+ fontWeight: "700",
235
+ textAlign: "center",
236
+ },
237
+ actionBtn: {
238
+ width: SPACE.iconHit,
239
+ height: SPACE.iconHit,
240
+ alignItems: "center",
241
+ justifyContent: "center",
242
+ },
243
+ });
@@ -0,0 +1,381 @@
1
+ import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
2
+ import {
3
+ ActivityIndicator,
4
+ Dimensions,
5
+ Pressable,
6
+ ScrollView,
7
+ StyleSheet,
8
+ Text,
9
+ View,
10
+ } from "react-native";
11
+ import { Image } from "expo-image";
12
+ import { ChallengeSeriesWithProgress, useChallenges } from "../../hooks/useChallenges";
13
+ import { useNextCollection } from "../../hooks/useNextCollection";
14
+ import { useTheme } from "../../contexts/ThemeContext";
15
+ import { useLocal } from "../../contexts/LocalContext";
16
+ import { useWastoClient } from "../../hooks/useWastoClient";
17
+ import { Theme } from "../../types/Theme";
18
+ import { Title } from "../title/Title";
19
+ import { NavButton } from "../nav-button/NavButton";
20
+ import { Button } from "../button/Button";
21
+ import { ChevronLeftIcon } from "lucide-react-native";
22
+ import { useSafeAreaPadding } from "../../helpers/useSafeAreaPadding";
23
+ import {
24
+ generateQuizQuestions,
25
+ type QuizAnswerOption,
26
+ } from "../../helpers/generateQuizQuestions";
27
+ import { ResultModal } from "./ResultModal";
28
+
29
+ export interface QuizModalProps {
30
+ serie: ChallengeSeriesWithProgress;
31
+ challengeId: string;
32
+ onClose: () => void;
33
+ }
34
+
35
+ export function QuizModal({
36
+ serie,
37
+ challengeId,
38
+ onClose,
39
+ }: QuizModalProps): ReactNode {
40
+ const { theme } = useTheme();
41
+ const themedStyles = createThemedStyles(theme);
42
+ const topInset = useSafeAreaPadding(["top"]);
43
+ const { deviceId } = useLocal();
44
+ const { refetch } = useChallenges();
45
+ const { city, wasteTypes, isLoading } = useNextCollection();
46
+
47
+ const client = useWastoClient();
48
+
49
+ const questions = useMemo(
50
+ () => generateQuizQuestions(wasteTypes, serie),
51
+ [wasteTypes, serie]
52
+ );
53
+
54
+ const [index, setIndex] = useState(0);
55
+ const [selected, setSelected] = useState<QuizAnswerOption | null>(null);
56
+ const [correct, setCorrect] = useState(0);
57
+ const [incorrect, setIncorrect] = useState(0);
58
+ const [showResults, setShowResults] = useState(false);
59
+ const completedRef = useRef(false);
60
+
61
+ const question = questions[index];
62
+
63
+ useEffect(() => {
64
+ setIndex(0);
65
+ setSelected(null);
66
+ setCorrect(0);
67
+ setIncorrect(0);
68
+ setShowResults(false);
69
+ completedRef.current = false;
70
+ }, [serie.id]);
71
+
72
+ const persistScore = async (nextCorrect: number, total: number) => {
73
+ if (completedRef.current || !deviceId || !challengeId || !serie.id || total === 0) return;
74
+ completedRef.current = true;
75
+ const score = Math.round((nextCorrect / total) * 100);
76
+ try {
77
+ await client.public.challenges.complete(challengeId, {
78
+ deviceId,
79
+ score,
80
+ seriesId: serie.id,
81
+ });
82
+ await refetch();
83
+ } catch (err) {
84
+ console.error("QuizModal: complete challenge", err);
85
+ }
86
+ };
87
+
88
+ const handleAnswer = (answer: QuizAnswerOption) => {
89
+ if (selected || !question) return;
90
+ setSelected(answer);
91
+ if (answer.id === question.correctOptionId) {
92
+ setCorrect((n) => n + 1);
93
+ } else {
94
+ setIncorrect((n) => n + 1);
95
+ }
96
+ };
97
+
98
+ const handleNext = () => {
99
+ if (index < questions.length - 1) {
100
+ setIndex((n) => n + 1);
101
+ setSelected(null);
102
+ return;
103
+ }
104
+ // Last question: `correct`/`incorrect` may still be stale if setState has not flushed.
105
+ let finalCorrect = correct;
106
+ let finalIncorrect = incorrect;
107
+ if (selected && question && correct + incorrect < questions.length) {
108
+ if (selected.id === question.correctOptionId) {
109
+ finalCorrect = correct + 1;
110
+ } else {
111
+ finalIncorrect = incorrect + 1;
112
+ }
113
+ }
114
+ setCorrect(finalCorrect);
115
+ setIncorrect(finalIncorrect);
116
+ void persistScore(finalCorrect, questions.length);
117
+ setShowResults(true);
118
+ };
119
+
120
+ const handleFinish = () => {
121
+ setShowResults(false);
122
+ onClose();
123
+ };
124
+
125
+ return (
126
+ <View style={[styles.modal, themedStyles.modal]}>
127
+ <View style={topInset}>
128
+ <View style={styles.header}>
129
+ <NavButton icon={ChevronLeftIcon} onPress={onClose} />
130
+ <Title text={serie.name} level="2" numberOfLines={1} />
131
+ <Text style={[styles.progress, themedStyles.muted]}>
132
+ {questions.length ? `${index + 1}/${questions.length}` : "0/0"}
133
+ </Text>
134
+ </View>
135
+ </View>
136
+
137
+ {isLoading && questions.length === 0 ? (
138
+ <View style={styles.center}>
139
+ <ActivityIndicator color={theme.colors.primary} />
140
+ <Text style={themedStyles.muted}>Chargement du quiz…</Text>
141
+ </View>
142
+ ) : !city ? (
143
+ <View style={styles.center}>
144
+ <Text style={themedStyles.muted}>Aucune ville sélectionnée</Text>
145
+ <Button onPress={onClose} variant="ghost">Fermer</Button>
146
+ </View>
147
+ ) : questions.length === 0 ? (
148
+ <View style={styles.center}>
149
+ <Text style={styles.emoji}>🐸</Text>
150
+ <Text style={[styles.emptyText, themedStyles.text]}>
151
+ Aucun quiz disponible pour cette série
152
+ </Text>
153
+ <Button onPress={onClose} variant="ghost">Fermer</Button>
154
+ </View>
155
+ ) : question ? (
156
+ <ScrollView bounces={false} contentContainerStyle={styles.body}>
157
+ {question.kind !== "ab_image" && question.imageUrl ? (
158
+ <Image
159
+ source={question.imageUrl}
160
+ style={[styles.questionImage, themedStyles.questionImage]}
161
+ contentFit="contain"
162
+ />
163
+ ) : question.kind !== "ab_image" ? (
164
+ <View style={[styles.mascot, themedStyles.mascot]}>
165
+ <Text style={styles.emoji}>{question.material ? "♻️" : "🐸"}</Text>
166
+ </View>
167
+ ) : null}
168
+
169
+ {question.material ? (
170
+ <Text style={[styles.material, themedStyles.muted]}>{question.material}</Text>
171
+ ) : null}
172
+
173
+ <Title text={question.question} level="3" />
174
+
175
+ <View
176
+ style={
177
+ question.kind === "ab_image" ? styles.abAnswers : styles.answers
178
+ }
179
+ >
180
+ {question.answers.map((answer) => {
181
+ const isSelected = selected?.id === answer.id;
182
+ const isCorrect = answer.id === question.correctOptionId;
183
+ const showState = !!selected;
184
+ const stateStyle = !showState
185
+ ? themedStyles.answer
186
+ : isCorrect
187
+ ? themedStyles.answerCorrect
188
+ : isSelected
189
+ ? themedStyles.answerWrong
190
+ : themedStyles.answer;
191
+
192
+ if (question.kind === "ab_image" && answer.imageUrl) {
193
+ return (
194
+ <Pressable
195
+ key={answer.id}
196
+ style={[styles.abAnswer, stateStyle]}
197
+ onPress={() => handleAnswer(answer)}
198
+ disabled={!!selected}
199
+ >
200
+ <Image
201
+ source={answer.imageUrl}
202
+ style={styles.abAnswerImage}
203
+ contentFit="contain"
204
+ />
205
+ <Text style={[styles.answerText, themedStyles.answerText]}>
206
+ {answer.text}
207
+ </Text>
208
+ </Pressable>
209
+ );
210
+ }
211
+
212
+ return (
213
+ <Pressable
214
+ key={`${answer.id}-${answer.text}`}
215
+ style={[styles.answer, stateStyle]}
216
+ onPress={() => handleAnswer(answer)}
217
+ disabled={!!selected}
218
+ >
219
+ <Text style={[styles.answerText, themedStyles.answerText]}>
220
+ {answer.text}
221
+ </Text>
222
+ </Pressable>
223
+ );
224
+ })}
225
+ </View>
226
+
227
+ {selected ? (
228
+ <View style={styles.feedbackBlock}>
229
+ {question.explanation ? (
230
+ <Text style={[styles.explanation, themedStyles.muted]}>
231
+ {question.explanation}
232
+ </Text>
233
+ ) : null}
234
+ <Button onPress={handleNext}>
235
+ {index < questions.length - 1 ? "Suivant" : "Voir les résultats"}
236
+ </Button>
237
+ </View>
238
+ ) : null}
239
+ </ScrollView>
240
+ ) : null}
241
+
242
+ <ResultModal
243
+ visible={showResults}
244
+ correct={correct}
245
+ incorrect={incorrect}
246
+ total={questions.length}
247
+ onClose={handleFinish}
248
+ />
249
+ </View>
250
+ );
251
+ }
252
+
253
+ const createThemedStyles = (theme: Theme) =>
254
+ StyleSheet.create({
255
+ modal: {
256
+ backgroundColor: theme.colors.background,
257
+ },
258
+ muted: {
259
+ color: theme.colors.onBackground,
260
+ opacity: 0.7,
261
+ },
262
+ text: {
263
+ color: theme.colors.onBackground,
264
+ },
265
+ questionImage: {
266
+ backgroundColor: theme.colors.surface,
267
+ borderRadius: theme.radius,
268
+ },
269
+ mascot: {
270
+ backgroundColor: theme.colors.surface,
271
+ borderRadius: theme.radius,
272
+ },
273
+ answer: {
274
+ backgroundColor: theme.colors.surface,
275
+ borderColor: theme.colors.border,
276
+ },
277
+ answerCorrect: {
278
+ backgroundColor: `${theme.colors.success}22`,
279
+ borderColor: theme.colors.success,
280
+ },
281
+ answerWrong: {
282
+ backgroundColor: `${theme.colors.error}22`,
283
+ borderColor: theme.colors.error,
284
+ },
285
+ answerText: {
286
+ color: theme.colors.onSurface,
287
+ },
288
+ });
289
+
290
+ const styles = StyleSheet.create({
291
+ modal: {
292
+ height: Dimensions.get("screen").height,
293
+ },
294
+ header: {
295
+ padding: 20,
296
+ width: "100%",
297
+ flexDirection: "row",
298
+ alignItems: "center",
299
+ justifyContent: "space-between",
300
+ gap: 12,
301
+ },
302
+ progress: {
303
+ fontSize: 15,
304
+ fontWeight: "600",
305
+ minWidth: 48,
306
+ textAlign: "right",
307
+ },
308
+ center: {
309
+ flex: 1,
310
+ justifyContent: "center",
311
+ alignItems: "center",
312
+ padding: 24,
313
+ gap: 16,
314
+ },
315
+ body: {
316
+ padding: 20,
317
+ gap: 16,
318
+ paddingBottom: 40,
319
+ },
320
+ questionImage: {
321
+ width: "100%",
322
+ height: 180,
323
+ },
324
+ mascot: {
325
+ width: "100%",
326
+ height: 140,
327
+ alignItems: "center",
328
+ justifyContent: "center",
329
+ },
330
+ emoji: {
331
+ fontSize: 48,
332
+ },
333
+ material: {
334
+ fontSize: 13,
335
+ fontWeight: "600",
336
+ textTransform: "uppercase",
337
+ letterSpacing: 0.4,
338
+ },
339
+ emptyText: {
340
+ textAlign: "center",
341
+ fontSize: 16,
342
+ },
343
+ answers: {
344
+ gap: 10,
345
+ },
346
+ abAnswers: {
347
+ flexDirection: "row",
348
+ gap: 12,
349
+ },
350
+ feedbackBlock: {
351
+ gap: 12,
352
+ },
353
+ explanation: {
354
+ fontSize: 14,
355
+ lineHeight: 20,
356
+ fontWeight: "500",
357
+ },
358
+ answer: {
359
+ padding: 16,
360
+ borderWidth: 1,
361
+ borderRadius: 16,
362
+ },
363
+ abAnswer: {
364
+ flex: 1,
365
+ padding: 10,
366
+ borderWidth: 1,
367
+ borderRadius: 16,
368
+ alignItems: "center",
369
+ gap: 8,
370
+ },
371
+ abAnswerImage: {
372
+ width: "100%",
373
+ aspectRatio: 1,
374
+ borderRadius: 12,
375
+ },
376
+ answerText: {
377
+ fontSize: 16,
378
+ fontWeight: "600",
379
+ textAlign: "center",
380
+ },
381
+ });