Files
musicland/src/components/ListSelection/ListSelection.js
T
Thomas Demirdjian f3229be1f0 fix tickets
2025-10-03 16:19:48 +02:00

357 lines
11 KiB
JavaScript

import { BlurView } from "expo-blur";
import React, { useEffect, useMemo, useState } from "react";
import { FlatList, Platform, SectionList, Text, View } from "react-native";
import useLayoutType from "../../hooks/useLayoutType";
import CreateLyricsHeader from "../../screens/Writing/components/CreateLyricsHeader";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { LinearGradient } from "../LinearGradient/LinearGradient";
const BLUE_SELECTION_GRADIENT = ["#4673F9", "#7023F7"];
const buildItemKey = (value, category) =>
category ? `${category}::${value}` : `${value}`;
const ListSelection = ({
options = [],
variant = "simple",
selected: selectedProp,
setSelected: setSelectedProp,
multiple = false,
maxSelection,
getItemValue,
selectedValueExtractor,
formatSelectedValue,
// styles
contentContainerStyle,
itemContainerStyle,
itemOuterStyle,
itemTextStyle,
highlightColor = "#F94697",
disableHover = false,
}) => {
// état interne si non contrôlé
const [internalSelected, setInternalSelected] = useState(
variant === "sectioned" ? {} : multiple ? [] : null
);
const selected = selectedProp !== undefined ? selectedProp : internalSelected;
const setSelected =
setSelectedProp !== undefined ? setSelectedProp : setInternalSelected;
const { isWeb } = useLayoutType();
const [hoveredKey, setHoveredKey] = useState(null);
useEffect(() => {
if (disableHover && hoveredKey !== null) setHoveredKey(null);
}, [disableHover, hoveredKey]);
// helpers
const valueOf = useMemo(() => {
if (typeof getItemValue === "function") return getItemValue;
if (variant === "simple") return (item) => item;
return (item) => item?.title ?? item;
}, [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 (extractSelectedComparable(selected) === val) setSelected(null);
else setSelected(formatValue(item));
}
};
// contenus élémentaires : toujours encapsuler le texte dans <Text>
const renderSimpleContent = (label) => (
<View style={{ minHeight: 40, justifyContent: "center" }}>
<Text
style={{
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "left",
...itemTextStyle,
}}
>
{typeof label === "string" ? label : String(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>
<Text>{` : ${item?.description ?? ""}`}</Text>
</Text>
</View>
);
// applique le gradient bleu UNIQUEMENT sur web et quand sélectionné
const withWebBlueGradientIfSelected = (content, sel) => {
if (!isWeb || !sel) return content;
// wrap dans un View pour éviter texte brut direct sous le gradient (compat RN Web)
return (
<LinearGradient
colors={BLUE_SELECTION_GRADIENT}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
locations={[0, 1]}
style={{
borderRadius: 14,
paddingHorizontal: 12,
paddingVertical: 8,
overflow: "hidden",
}}
>
<View>{content}</View>
</LinearGradient>
);
};
// ============ SECTIONED ============
if (variant === "sectioned") {
return (
<SectionList
sections={options}
showsVerticalScrollIndicator={false}
contentContainerStyle={contentContainerStyle}
renderItem={({ item, section }) => {
const cat = section?.title;
const val = valueOf(item);
const sel = isSelected(val, cat);
const itemKey = buildItemKey(String(val ?? ""), cat);
const isHovered = isWeb && !disableHover && hoveredKey === itemKey;
const showHoverOutline = isHovered && !sel;
const baseContent = renderSimpleContent(item);
const headerContainerStyle = {
borderWidth: 0,
borderColor: "transparent",
borderRadius: 14,
...itemContainerStyle,
};
if (sel && isWeb)
headerContainerStyle.backgroundColor = "transparent";
return (
<View
style={[{ position: "relative" }, itemOuterStyle]}
onMouseEnter={
isWeb && !disableHover
? () => setHoveredKey(itemKey)
: undefined
}
onMouseLeave={
isWeb && !disableHover ? () => setHoveredKey(null) : undefined
}
>
<CreateLyricsHeader
onPress={() => toggleSelect(item, cat)}
tint={sel ? "default" : "dark"}
colors={[Palette.tran, Palette.tran]}
showBorder={!sel}
disableBlur={sel && isWeb}
blurViewStyle={
sel && isWeb
? { paddingHorizontal: 0, paddingVertical: 0 }
: undefined
}
containerStyle={headerContainerStyle}
>
{withWebBlueGradientIfSelected(baseContent, sel)}
</CreateLyricsHeader>
{showHoverOutline && (
<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>
)}
keyExtractor={(item, idx) => `${valueOf(item)}-${idx}`}
/>
);
}
// ============ FLAT (simple/titleDescription/emotion) ============
return (
<FlatList
data={options}
showsVerticalScrollIndicator={false}
contentContainerStyle={contentContainerStyle}
renderItem={({ item }) => {
const val = valueOf(item);
const sel = isSelected(val);
const useEmotionColors = variant === "emotion";
// pour "emotion", on masque le bord gauche si sélectionné
const borderColors = useEmotionColors
? sel
? [Palette.tran, Palette.tran]
: item?.color
: [Palette.tran, Palette.tran];
const tint = useEmotionColors ? "dark" : sel ? "default" : "dark";
const itemKey = buildItemKey(String(val ?? ""));
const isHovered = isWeb && !disableHover && hoveredKey === itemKey;
const showHoverOutline = isHovered && !sel;
const showSelectionOutline =
!isWeb &&
(variant === "simple" || variant === "emotion" || variant === "titleDescription") &&
sel;
const baseContent =
variant === "simple"
? renderSimpleContent(item)
: renderTitleDescriptionContent(item);
const headerContainerStyle = {
borderWidth: 0,
borderColor: "transparent",
borderRadius: 14,
...itemContainerStyle,
};
if (sel && isWeb) headerContainerStyle.backgroundColor = "transparent";
const shouldShowBorder = !sel;
return (
<View
style={[{ position: "relative" }, itemOuterStyle]}
onMouseEnter={
isWeb && !disableHover ? () => setHoveredKey(itemKey) : undefined
}
onMouseLeave={
isWeb && !disableHover ? () => setHoveredKey(null) : undefined
}
>
<CreateLyricsHeader
colors={borderColors}
tint={tint}
onPress={() => toggleSelect(item)}
gradientProps={
useEmotionColors && !sel ? { locations: [0.24, 1] } : undefined
}
showBorder={shouldShowBorder}
disableBlur={sel && isWeb} // web: pas de blur si gradient
blurViewStyle={
sel && isWeb
? { paddingHorizontal: 0, paddingVertical: 0 }
: undefined
}
containerStyle={headerContainerStyle}
>
{withWebBlueGradientIfSelected(baseContent, sel)}
</CreateLyricsHeader>
{(showHoverOutline || showSelectionOutline) && (
<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;