fix list selection and other tickets
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user