| 1 | import React from "react";
|
| 2 | import {
|
| 3 | View,
|
| 4 | Pressable,
|
| 5 | StyleSheet,
|
| 6 | Image,
|
| 7 | ImageSourcePropType,
|
| 8 | ViewStyle,
|
| 9 | } from "react-native";
|
| 10 | import { Text } from "@/components/ui/Text";
|
| 11 | import { Spacing, Typography, BorderRadius } from "@/constants/theme";
|
| 12 | import { useThemeColor } from "@/hooks/use-theme-color";
|
| 13 |
|
| 14 | type IconSource = string | ImageSourcePropType; // either string or a svg
|
| 15 |
|
| 16 | // Specialized variant with labels below
|
| 17 | interface MoodSelectorProps {
|
| 18 | items: {
|
| 19 | id: string;
|
| 20 | icon: IconSource;
|
| 21 | label: string;
|
| 22 | }[];
|
| 23 | value?: string;
|
| 24 | onSelect: (id: string) => void;
|
| 25 | disabled?: boolean;
|
| 26 | style?: ViewStyle;
|
| 27 | }
|
| 28 |
|
| 29 | export const MoodSelector: React.FC<MoodSelectorProps> = ({
|
| 30 | items,
|
| 31 | value,
|
| 32 | onSelect,
|
| 33 | disabled = false,
|
| 34 | style,
|
| 35 | }) => {
|
| 36 | const accentBlueColor = useThemeColor({}, "accentBlue");
|
| 37 | const surfaceColor = useThemeColor({}, "surface");
|
| 38 | const dividerColor = useThemeColor({}, "divider");
|
| 39 | const textSecondaryColor = useThemeColor({}, "textSecondary");
|
| 40 |
|
| 41 | const styles = StyleSheet.create({
|
| 42 | container: {
|
| 43 | flexDirection: "row",
|
| 44 | gap: Spacing.inlineGapSm,
|
| 45 | justifyContent: "space-around",
|
| 46 | alignItems: "flex-start",
|
| 47 | flexWrap: "wrap",
|
| 48 | },
|
| 49 | itemWrapper: {
|
| 50 | alignItems: "center",
|
| 51 | gap: 8,
|
| 52 | flex: 1,
|
| 53 | minWidth: "8%",
|
| 54 | },
|
| 55 | itemButton: {
|
| 56 | width: 50,
|
| 57 | height: 50,
|
| 58 | borderRadius: BorderRadius.lg,
|
| 59 | justifyContent: "center",
|
| 60 | alignItems: "center",
|
| 61 | backgroundColor: surfaceColor,
|
| 62 | borderWidth: 1,
|
| 63 | borderColor: dividerColor,
|
| 64 | },
|
| 65 | itemButtonActive: {
|
| 66 | backgroundColor: accentBlueColor,
|
| 67 | borderColor: "transparent",
|
| 68 | },
|
| 69 | icon: {
|
| 70 | fontSize: 48,
|
| 71 | lineHeight: 48,
|
| 72 | },
|
| 73 | emojiText: {
|
| 74 | fontSize: 36,
|
| 75 | lineHeight: 40,
|
| 76 | },
|
| 77 | label: {
|
| 78 | fontSize: Typography.labelSm.fontSize,
|
| 79 | fontWeight: "500",
|
| 80 | color: textSecondaryColor,
|
| 81 | textAlign: "center",
|
| 82 | },
|
| 83 | });
|
| 84 |
|
| 85 | const isEmoji = (icon: IconSource): icon is string =>
|
| 86 | typeof icon === "string";
|
| 87 |
|
| 88 | return (
|
| 89 | <View style={[styles.container, style]}>
|
| 90 | {items.map((item) => {
|
| 91 | const isSelected = value === item.id;
|
| 92 |
|
| 93 | return (
|
| 94 | <View key={item.id} style={styles.itemWrapper}>
|
| 95 | <Pressable
|
| 96 | onPress={() => !disabled && onSelect(item.id)}
|
| 97 | disabled={disabled}
|
| 98 | testID={`mood-${item.id}`}
|
| 99 | accessibilityLabel={item.label}
|
| 100 | style={({ pressed }) => [
|
| 101 | styles.itemButton,
|
| 102 | isSelected && styles.itemButtonActive,
|
| 103 | pressed && !disabled && { opacity: 0.7 },
|
| 104 | disabled && { opacity: 0.5 },
|
| 105 | ]}
|
| 106 | >
|
| 107 | {isEmoji(item.icon) ? (
|
| 108 | <Text style={styles.emojiText}>{item.icon}</Text>
|
| 109 | ) : (
|
| 110 | <Image
|
| 111 | source={item.icon}
|
| 112 | style={{
|
| 113 | width: 48,
|
| 114 | height: 48,
|
| 115 | }}
|
| 116 | resizeMode="contain"
|
| 117 | />
|
| 118 | )}
|
| 119 | </Pressable>
|
| 120 | <Text style={styles.label}>{item.label}</Text>
|
| 121 | </View>
|
| 122 | );
|
| 123 | })}
|
| 124 | </View>
|
| 125 | );
|
| 126 | };
|