fix list selection and other tickets

This commit is contained in:
Thomas Demirdjian
2025-09-09 11:57:21 +02:00
parent 69977aba2b
commit 11f65023b7
23 changed files with 483 additions and 1397 deletions
@@ -0,0 +1,262 @@
import React, { useMemo, useState } from "react";
import { FlatList, SectionList, Text, View, Platform } from "react-native";
import { BlurView } from "expo-blur";
import CreateLyricsHeader from "../../screens/Writing/components/CreateLyricsHeader";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
// Reusable selection list supporting single, multiple and sectioned data.
// Variants supported:
// - "simple": options are strings
// - "titleDescription": options are { title, description }
// - "emotion": options are { title, description, color } (uses gradient colors)
// - "sectioned": options are sections [{ title, data: [] }] (single per section)
const ListSelection = ({
options = [],
variant = "simple",
selected: selectedProp,
setSelected: setSelectedProp,
multiple = false,
maxSelection,
getItemValue,
selectedValueExtractor,
formatSelectedValue,
// styles
contentContainerStyle,
itemContainerStyle,
itemOuterStyle,
itemTextStyle,
highlightColor = "#F94697",
}) => {
// Internal fallback state when not provided by parent
const [internalSelected, setInternalSelected] = useState(
variant === "sectioned" ? {} : multiple ? [] : null,
);
const selected = selectedProp !== undefined ? selectedProp : internalSelected;
const setSelected =
setSelectedProp !== undefined ? setSelectedProp : setInternalSelected;
const valueOf = useMemo(() => {
if (typeof getItemValue === "function") return getItemValue;
if (variant === "simple") return (item) => item;
return (item) => item?.title ?? item; // default to title when present
}, [getItemValue, variant]);
const formatValue = (item) =>
typeof formatSelectedValue === "function"
? formatSelectedValue(item)
: valueOf(item);
const extractSelectedComparable = (s) => {
if (typeof selectedValueExtractor === "function")
return selectedValueExtractor(s);
if (s && typeof s === "object" && "title" in s) return s.title;
return s;
};
const isSelected = (val, category) => {
if (variant === "sectioned") {
return selected?.[category] === val;
}
if (multiple) {
const list = Array.isArray(selected) ? selected : [];
return list.some((v) => v === val);
}
return extractSelectedComparable(selected) === val;
};
const toggleSelect = (item, category) => {
const val = valueOf(item);
if (variant === "sectioned") {
const current = selected && typeof selected === "object" ? selected : {};
const next = { ...current };
if (current?.[category] === val) next[category] = null;
else next[category] = formatValue(item);
setSelected(next);
return;
}
if (multiple) {
const list = Array.isArray(selected) ? selected : [];
const exists = list.some((v) => v === val);
if (exists) {
setSelected(list.filter((v) => v !== val));
} else {
if (!maxSelection || list.length < maxSelection) {
setSelected([...list, formatValue(item)]);
}
}
} else {
if (selected === val) setSelected(null);
else setSelected(formatValue(item));
}
};
const renderSimpleContent = (label) => (
<View style={{ minHeight: 40, justifyContent: "center" }}>
<Text
style={{
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "left",
...itemTextStyle,
}}
>
{label}
</Text>
</View>
);
const renderTitleDescriptionContent = (item) => (
<View style={{ paddingVertical: 6 }}>
<Text
style={{
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
...itemTextStyle,
}}
>
<Text style={{ fontFamily: FONT_FAMILY.InterBold }}>{item?.title}</Text>{" "}
: {item?.description}
</Text>
</View>
);
if (variant === "sectioned") {
// Sectioned list: options as [{ title, data: [] }]
return (
<SectionList
sections={options}
showsVerticalScrollIndicator={false}
contentContainerStyle={contentContainerStyle}
renderItem={({ item, section }) => {
const cat = section?.title;
const val = valueOf(item);
const sel = isSelected(val, cat);
return (
<View style={[{ position: "relative" }, itemOuterStyle]}>
<CreateLyricsHeader
onPress={() => toggleSelect(item, cat)}
tint={sel ? "default" : "dark"}
colors={[Palette.tran, Palette.tran]}
containerStyle={{
borderWidth: 0,
borderColor: "transparent",
borderRadius: 14,
...itemContainerStyle,
}}
>
{renderSimpleContent(item)}
</CreateLyricsHeader>
{sel && (
<View
pointerEvents="none"
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
borderWidth: 2,
borderRadius: 14,
borderColor: highlightColor,
zIndex: 2,
}}
/>
)}
</View>
);
}}
renderSectionHeader={({ section: { title } }) => (
<View style={{ alignSelf: "flex-start", marginLeft: 10, marginBottom: 6 }}>
<BlurView
intensity={40}
tint="dark"
experimentalBlurMethod={Platform.OS !== "ios" ? "dimezisBlurView" : "none"}
style={{
borderRadius: 18,
overflow: "hidden",
paddingHorizontal: 8,
paddingVertical: 4,
}}
>
<Text
style={{
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterBold,
}}
>
{title}
</Text>
</BlurView>
</View>
)}
/>
);
}
return (
<FlatList
data={options}
showsVerticalScrollIndicator={false}
contentContainerStyle={contentContainerStyle}
renderItem={({ item }) => {
const val = valueOf(item);
const sel = isSelected(val);
const useEmotionColors = variant === "emotion";
// For emotion variant, hide the left color when selected
const borderColors = useEmotionColors
? sel
? [Palette.tran, Palette.tran]
: item?.color
: [Palette.tran, Palette.tran];
const tint = useEmotionColors ? "dark" : sel ? "default" : "dark";
return (
<View style={[{ position: "relative" }, itemOuterStyle]}>
<CreateLyricsHeader
colors={borderColors}
tint={tint}
onPress={() => toggleSelect(item)}
gradientProps={
useEmotionColors && !sel ? { locations: [0.24, 1] } : undefined
}
containerStyle={{
borderWidth: 0,
borderColor: "transparent",
borderRadius: 14,
...itemContainerStyle,
}}
>
{variant === "simple"
? renderSimpleContent(item)
: renderTitleDescriptionContent(item)}
</CreateLyricsHeader>
{sel && (
<View
pointerEvents="none"
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
borderWidth: 2,
borderRadius: 14,
borderColor: highlightColor,
zIndex: 2,
}}
/>
)}
</View>
);
}}
keyExtractor={(item, idx) => `${valueOf(item)}-${idx}`}
/>
);
};
export default ListSelection;