frontend: flesh mood components screen
Parents:
2b711ef1 file(s) changed
- frontend/app/entry/mood-components.tsx +245 -0
frontend/app/entry/mood-components.tsx
@@ -0,0 +1,245 @@
1 + import React from 'react';
2 + import {
3 + View,
4 + Text,
5 + ScrollView,
6 + Pressable,
7 + StyleSheet,
8 + } from 'react-native';
9 + import { router } from 'expo-router';
10 + import { Ionicons } from '@expo/vector-icons';
11 +
12 + import { ScaleSlider } from '@/components/ui/ScaleSlider';
13 + import { Row, Grid } from '@/components/ui/LayoutHelpers';
14 + import { Section, SectionHeader } from '@/components/ui/Sections';
15 + import { Button } from '@/components/ui/Button';
16 + import { Card } from '@/components/ui/Cards';
17 + import { Theme } from '@/constants/theme';
18 + import {
19 + MOOD_COMPONENTS,
20 + getMoodComponent,
21 + intensityLabel,
22 + } from '@/constants/mood-components';
23 +
24 + import {
25 + useMoodEntryStore,
26 + } from '@/stores/moodEntry';
27 +
28 + export default function MoodComponentsScreen() {
29 + const {
30 + selectedMood,
31 + components,
32 + addComponent,
33 + removeComponent,
34 + setComponentIntensity,
35 + } = useMoodEntryStore();
36 +
37 + const activeIds = components.map((c) => c.id);
38 + const available = MOOD_COMPONENTS.filter((d) => !activeIds.includes(d.id));
39 +
40 + const handleDone = () => {
41 + router.back();
42 + };
43 +
44 + return (
45 + <View style={styles.container}>
46 +
47 + {/* Header */}
48 + <View style={styles.header}>
49 + <Pressable onPress={() => router.back()} hitSlop={8}>
50 + <Ionicons name="close" size={24} color={Theme.colors.text} />
51 + </Pressable>
52 + <Text style={styles.headerTitle}>Editar Componentes</Text>
53 + <Pressable onPress={handleDone} hitSlop={8}>
54 + <Text style={styles.headerAction}>Pronto</Text>
55 + </Pressable>
56 + </View>
57 +
58 + <ScrollView
59 + contentContainerStyle={styles.content}
60 + showsVerticalScrollIndicator={false}
61 + >
62 +
63 + {/* Subtitle */}
64 + {selectedMood && (
65 + <View style={styles.subtitleBlock}>
66 + <Text style={styles.subtitleSmall}>Baseado no seu humor selecionado</Text>
67 + <Text style={styles.subtitleLarge}>{selectedMood.label}</Text>
68 + </View>
69 + )}
70 +
71 + {/* Active components */}
72 + {components.length > 0 && (
73 + <Section>
74 + <SectionHeader
75 + title="Intensidade dos Componentes"
76 + info={`Ativos: ${components.length}`} />
77 +
78 + {components.map((comp) => {
79 + const def = getMoodComponent(comp.id);
80 + if (!def) return null;
81 +
82 + return (
83 + <Card key={comp.id} style={styles.activeCard}>
84 + {/* Card header */}
85 + <Row style={styles.activeCardHeader}>
86 + <View style={[styles.dot, { backgroundColor: def.color }]} />
87 + <Text style={styles.activeCardLabel}>{def.label}</Text>
88 + <Pressable
89 + onPress={() => removeComponent(comp.id)}
90 + hitSlop={8}
91 + >
92 + <Ionicons
93 + name="remove-circle-outline"
94 + size={22}
95 + color="#F87171"
96 + />
97 + </Pressable>
98 + </Row>
99 +
100 + {/* Slider */}
101 + <ScaleSlider
102 + variant="compact"
103 + label="Intensidade"
104 + value={comp.intensity}
105 + onValueChange={(v) => setComponentIntensity(comp.id, v)}
106 + min={1}
107 + max={10}
108 + step={1}
109 + minLabel="Suave"
110 + maxLabel="Intensa"
111 + style={styles.slider}
112 + />
113 + </Card>
114 + );
115 + })}
116 +
117 + </Section>
118 + )}
119 +
120 + {/* Available to add */}
121 + {available.length > 0 && (
122 + <Section>
123 + <SectionHeader title="Adicionar outros" />
124 + <Grid gap={2}>
125 + {available.map((def) => (
126 + <Card key={def.id} style={styles.availableItem}>
127 + <Pressable
128 + onPress={() => addComponent(def.id)}
129 + style={styles.availableItemInternalWrapper}
130 + >
131 + <Row gap={8} style={styles.availableInner}>
132 + <View style={[styles.dot, { backgroundColor: def.color }]} />
133 + <Text style={styles.availableLabel}>{def.label}</Text>
134 + </Row>
135 +
136 + <Ionicons
137 + name="add-circle-outline"
138 + size={20}
139 + color={Theme.colors.light.smallAddButtonIcon}
140 + />
141 + </Pressable>
142 + </Card>
143 + ))}
144 + </Grid>
145 + </Section>
146 + )}
147 + </ScrollView>
148 + </View>
149 + );
150 + }
151 +
152 + const styles = StyleSheet.create({
153 + container: {
154 + flex: 1,
155 + },
156 + header: {
157 + flexDirection: 'row',
158 + justifyContent: 'space-between',
159 + alignItems: 'center',
160 + paddingHorizontal: Theme.spacing.containerPadding,
161 + paddingVertical: 16,
162 + borderBottomWidth: 1,
163 + borderBottomColor: Theme.colors.light.divider,
164 + },
165 + headerTitle: {
166 + fontSize: Theme.typography.bodyLg.fontSize,
167 + fontWeight: '700',
168 + color: Theme.colors.light.text,
169 + },
170 + headerAction: {
171 + fontSize: Theme.typography.bodyMd.fontSize,
172 + fontWeight: '600',
173 + color: Theme.colors.light.tint,
174 + },
175 + content: {
176 + padding: Theme.spacing.containerPadding,
177 + gap: Theme.spacing.sectionGap,
178 + paddingBottom: 120,
179 + },
180 + subtitleBlock: {
181 + alignItems: 'center',
182 + gap: 4,
183 + },
184 + subtitleSmall: {
185 + fontSize: Theme.typography.bodyMd.fontSize,
186 + color: Theme.colors.light.textSecondary,
187 + },
188 + subtitleLarge: {
189 + fontSize: 28,
190 + fontWeight: '700',
191 + color: Theme.colors.light.text,
192 + },
193 + section: {
194 + gap: Theme.spacing.cardGap,
195 + },
196 + sectionHeader: {
197 + justifyContent: 'space-between',
198 + alignItems: 'center',
199 + },
200 + activeCard: {
201 + padding: Theme.spacing.cardGap,
202 + },
203 + activeCardHeader: {
204 + alignItems: 'center',
205 + justifyContent: 'space-between',
206 + marginBottom: 16
207 + },
208 + activeCardLabel: {
209 + flex: 1,
210 + ... { ...Theme.typography.bodyLg, lineHeight: 24 },
211 + color: Theme.colors.light.text,
212 + marginLeft: 4,
213 + },
214 + dot: {
215 + width: 12,
216 + height: 12,
217 + borderRadius: 6,
218 + flexShrink: 0,
219 + },
220 + slider: {
221 + marginTop: 4,
222 + },
223 + availableItem: {
224 + flexDirection: 'row',
225 + justifyContent: 'space-between',
226 + alignItems: 'center',
227 + paddingHorizontal: 12,
228 + paddingVertical: 10,
229 + },
230 + availableItemInternalWrapper: {
231 + flexDirection: 'row',
232 + width: '100%',
233 + justifyContent: 'space-between',
234 + alignItems: 'center',
235 + },
236 + availableInner: {
237 + alignItems: 'center',
238 + flexShrink: 1,
239 + },
240 + availableLabel: {
241 + fontSize: Theme.typography.bodyMd.fontSize,
242 + fontWeight: '500',
243 + color: Theme.colors.light.text,
244 + }
245 + });