frontend: align the badge vocabulary and type the variant prop
Pedro Lucas Porcellis porcellis@eletrotupi.com · 12 days ago
2bd9b6733f955a443d284beecfa664749fcb3900 Browse filesParents
98f51d9Changed files
7
Additions
+43
Deletions
-26
frontend/app/entry/[id].tsx
1 1
import { useState, useEffect, use } from "react";
2 2
import {
3 3
StyleSheet,
4 4
View,
5 5
ScrollView,
131 131
</Text>
132 132
<Text style={styles.moodTime}>{formatMoment(moment)}</Text>
133 133
</Col>
134 134
</Row>
135 135
{/* If this is the latest mood entry, show the active badge */}
136 -
<Badge
137 -
label="Registro Ativo"
138 -
backgroundColor={Theme.colors.activeBackground}
139 -
color={Theme.colors.tint}
140 -
/>
136 +
<Badge label="Registro Ativo" variant="green" />
141 137
</Between>
142 138
</Card>
143 139
144 140
{/* Core levels */}
145 141
<Section>
237 233
</Card>
238 234
</Pressable>
239 235
);
240 236
})}
241 237
242 -
{entry.careActions.map((ca) => {
238 +
{(entry.careActions ?? []).map((ca) => {
243 239
const meta = getCareActionMeta(ca);
244 240
245 241
return (
246 242
<Pressable
247 243
key={ca.id}
248 -
onPress={() => (router.push as any)(`/care-actions/${ca.id}`)}
244 +
onPress={() => router.push(`/care-actions/${ca.id}`)}
249 245
style={({ pressed }) => [pressed && { opacity: 0.7 }]}
250 246
>
251 247
<Card style={styles.linkCard}>
252 248
<Row style={{ alignItems: 'center', gap: 12 }}>
253 249
<View style={[styles.linkIconCircle, { backgroundColor: meta.iconBackground }]}>
254 -
<MaterialIcons name={meta.icon as any} size={22} color={meta.color} />
250 +
<MaterialIcons name={meta.icon} size={22} color={meta.color} />
255 251
</View>
256 252
<Col gap={3} style={{ flex: 1 }}>
257 253
<Row style={{ alignItems: 'center', gap: 6 }}>
258 254
<Text style={styles.linkLabel}>{meta.label}</Text>
259 -
<Badge label="Consulta" variant="info" style={undefined} />
255 +
<Badge label="Consulta" variant="blue" style={undefined} />
260 256
</Row>
261 257
<Text style={styles.linkTime}>
262 258
{formatMoment(new Date(ca.moment))}
263 259
</Text>
264 260
</Col>
444 440
height: "100%",
445 441
borderRadius: 99,
446 442
},
447 443
448 444
});
frontend/app/triggers/[id].tsx
1 1
import { useState, useEffect, use } from "react";
2 2
import {
3 3
StyleSheet,
4 4
View,
5 5
ScrollView,
220 220
</Card>
221 221
</Pressable>
222 222
);
223 223
})}
224 224
225 -
{trigger.careActions.map((ca) => {
225 +
{(trigger.careActions ?? []).map((ca) => {
226 226
const meta = getCareActionMeta(ca);
227 227
return (
228 228
<Pressable
229 229
key={ca.id}
230 -
onPress={() => (router.push as any)(`/care-actions/${ca.id}`)}
230 +
onPress={() => router.push(`/care-actions/${ca.id}`)}
231 231
style={({ pressed }) => [pressed && { opacity: 0.7 }]}
232 232
>
233 233
<Card style={styles.linkCard}>
234 234
<Row style={{ alignItems: 'center', gap: 12 }}>
235 235
<View style={[styles.linkIconCircle, { backgroundColor: meta.iconBackground }]}>
236 -
<MaterialIcons name={meta.icon as any} size={22} color={meta.color} />
236 +
<MaterialIcons name={meta.icon} size={22} color={meta.color} />
237 237
</View>
238 238
<Col gap={3} style={{ flex: 1 }}>
239 239
<Row style={{ alignItems: 'center', gap: 6 }}>
240 240
<Text style={styles.linkLabel}>{meta.label}</Text>
241 -
<Badge label="Consulta" variant="info" style={undefined} />
241 +
<Badge label="Consulta" variant="blue" style={undefined} />
242 242
</Row>
243 243
<Text style={styles.linkTime}>
244 244
{formatMoment(new Date(ca.moment))}
245 245
</Text>
246 246
</Col>
435 435
impactFill: {
436 436
height: "100%",
437 437
borderRadius: 99,
438 438
},
439 439
});
frontend/components/ui/Badge.tsx
1 1
import {
2 2
StyleSheet,
3 -
View
3 +
View,
4 +
type StyleProp,
5 +
type ViewStyle,
4 6
} from 'react-native';
5 7
import { Text } from '@/components/ui/Text';
6 8
7 9
// Badge color palettes
8 10
export const badgeVariants = {
12 14
red: { bg: "#fee2e2", text: "#991b1b", dot: "#ef4444" },
13 15
purple: { bg: "#ede9fe", text: "#5b21b6", dot: "#8b5cf6" },
14 16
gray: { bg: "#f1f5f9", text: "#475569", dot: "#94a3b8" },
15 17
orange: { bg: "#ffedd5", text: "#9a3412", dot: "#f97316" },
16 18
pink: { bg: "#fce7f3", text: "#9d174d", dot: "#ec4899" },
19 +
indigo: { bg: "#e0e7ff", text: "#3730a3", dot: "#6366f1" },
20 +
};
21 +
22 +
export type BadgeVariant = keyof typeof badgeVariants;
23 +
24 +
type BadgeProps = {
25 +
label: string;
26 +
variant?: BadgeVariant;
27 +
style?: StyleProp<ViewStyle>;
17 28
};
18 29
19 30
// TODO: Add a subtle variant and allow to drop the dot as well
20 31
// TODO: Experiment with a less round-ish version?
21 -
export const Badge = ({ label, variant = "green", style }) => {
32 +
export const Badge = ({ label, variant = "green", style }: BadgeProps) => {
22 33
const palette = badgeVariants[variant] ?? badgeVariants.green;
23 34
24 35
const styles = StyleSheet.create({
25 36
badge: {
26 37
flexDirection: "row",
48 59
<View style={[styles.badgeDot, { backgroundColor: palette.dot }]} />
49 60
<Text style={[styles.badgeText, { color: palette.text }]}>{label}</Text>
50 61
</View>
51 62
);
52 63
}
frontend/constants/trigger-categories.ts
1 1
import { MaterialIcons } from "@expo/vector-icons";
2 2
import { TriggerCategory } from "@/constants/triggers";
3 +
import type { BadgeVariant } from "@/components/ui/Badge";
3 4
4 5
export interface TriggerCategoryDefinition {
5 6
id: TriggerCategory;
6 7
label: string;
7 8
icon: keyof typeof MaterialIcons.glyphMap;
8 9
color: string;
9 10
iconBackground: string;
10 -
variant: string;
11 +
variant: BadgeVariant;
11 12
}
12 13
13 14
export const TRIGGER_CATEGORY_DEFINITIONS: TriggerCategoryDefinition[] = [
14 15
{ id: "WORK", label: "Trabalho", icon: "work", color: "#3b82f6", iconBackground: "#dbeafe", variant: "blue" },
15 16
{ id: "SOCIAL", label: "Social", icon: "diversity-1", color: "#8b5cf6", iconBackground: "#ede9fe", variant: "purple" },
22 23
];
23 24
24 25
export const getTriggerCategory = (id: string): TriggerCategoryDefinition =>
25 26
TRIGGER_CATEGORY_DEFINITIONS.find((c) => c.id === id) ??
26 27
TRIGGER_CATEGORY_DEFINITIONS.find((c) => c.id === "OTHER")!;
frontend/lib/history/mappers.ts
1 1
import type { HistoryCard } from "@/lib/history/types";
2 2
import type { MoodEntry, SleepRecord, Trigger, CareAction } from "@/lib/api";
3 3
import { getMood } from "@/constants/moods";
4 4
5 5
25 25
}
26 26
27 27
// Sleep Records
28 28
29 29
function sleepQualityBadge(hours: number): HistoryCard["badge"] {
30 -
if (hours >= 7) return { label: "Excelente", variant: "success" };
31 -
if (hours >= 5) return { label: "Regular", variant: "warning" };
30 +
if (hours >= 7) return { label: "Excelente", variant: "green" };
31 +
if (hours >= 5) return { label: "Regular", variant: "yellow" };
32 32
33 33
return {
34 34
label: "Ruim",
35 -
variant: "danger",
35 +
variant: "red",
36 36
};
37 37
}
38 38
39 39
export function mapSleepToHistoryCard(record: SleepRecord): HistoryCard {
40 40
console.log("mapping...", record)
127 127
summary: parts.join(" · ") || "Sem anotações",
128 128
badge:
129 129
linkedCount > 0
130 130
? {
131 131
label: `${linkedCount} vínculo${linkedCount > 1 ? "s" : ""}`,
132 -
variant: "info",
132 +
variant: "blue",
133 133
}
134 134
: undefined,
135 135
raw: record,
136 136
};
137 137
}
160 160
summary: "",
161 161
raw: record,
162 162
};
163 163
}
164 164
}
frontend/lib/history/types.ts
1 1
import type { MoodEntry, SleepRecord, Trigger, CareAction } from "@/lib/api";
2 +
import type { BadgeVariant } from "@/components/ui/Badge";
2 3
3 4
export type HistoryCategory = "mood" | "sleep" | "trigger" | "care_action";
4 5
5 6
export type CareActionSubtype = "medicine" | "appointment" | "activity";
6 7
7 8
export type HistoryBadge = {
8 9
label: string;
9 -
variant: "neutral" | "warning" | "success" | "danger" | "info";
10 +
variant: BadgeVariant;
10 11
};
11 12
12 -
export type HistoryCard = {
13 +
type HistoryCardBase = {
13 14
id: string; // "<category>-<id>" to avoid collisions
14 -
category: HistoryCategory;
15 -
subtype?: CareActionSubtype; // only set when category === "care_action"
16 15
timestamp: string; // ISO string — used for sorting and display
17 16
title: string;
18 17
summary: string;
19 18
badge?: HistoryBadge;
20 -
raw: MoodEntry | SleepRecord | Trigger | CareAction;
21 19
};
20 +
21 +
export type HistoryCard =
22 +
| (HistoryCardBase & { category: "mood"; raw: MoodEntry })
23 +
| (HistoryCardBase & { category: "sleep"; raw: SleepRecord })
24 +
| (HistoryCardBase & { category: "trigger"; raw: Trigger })
25 +
| (HistoryCardBase & {
26 +
category: "care_action";
27 +
subtype?: CareActionSubtype; // only set when category === "care_action"
28 +
raw: CareAction;
29 +
});
frontend/lib/utils/time.ts
1 1
import { format, isToday, isYesterday } from 'date-fns';
2 2
import { ptBR } from 'date-fns/locale';
3 +
import type { BadgeVariant } from '@/components/ui/Badge';
3 4
4 5
type TimeBadgeVariant = 'morning' | 'afternoon' | 'night' | 'latenight';
5 6
6 -
export const TIME_BADGES: Record<TimeBadgeVariant, { label: string; variant: string; }> = {
7 +
export const TIME_BADGES: Record<TimeBadgeVariant, { label: string; variant: BadgeVariant; }> = {
7 8
morning: { label: 'Manhã', variant: "green" },
8 9
afternoon: { label: 'Tarde', variant: "orange" },
9 10
night: { label: 'Noite', variant: "blue" },
10 11
latenight: { label: 'Madrugada', variant: "purple" },
11 12
};
39 40
// to prevent timezone offsets from shifting the calendar day
40 41
export const parseDateOnly = (dateStr: string | Date): Date => {
41 42
const iso = typeof dateStr === 'string' ? dateStr : dateStr.toISOString();
42 43
return new Date(`${iso.slice(0, 10)}T12:00:00`);
43 44
};