new song structure
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { BlurView } from "expo-blur";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
FlatList,
|
||||
Image,
|
||||
@@ -15,6 +15,7 @@ import { icons } from "../../assets";
|
||||
import { Palette } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import {
|
||||
arraysAreSame,
|
||||
formatStructureLabel,
|
||||
getSegmentMeta,
|
||||
OPTIONAL_STRUCTURE_SEGMENTS,
|
||||
@@ -40,20 +41,28 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
() => sanitizeStructureList(baseStructure),
|
||||
[baseStructure]
|
||||
);
|
||||
const extractValues = useCallback(
|
||||
(items) =>
|
||||
(items || []).map((item) => item?.type || item?.value).filter(Boolean),
|
||||
[]
|
||||
);
|
||||
const [sortableItems, setSortableItems] = useState(() =>
|
||||
buildSortableItems(sanitizedBase)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setSortableItems(buildSortableItems(sanitizedBase));
|
||||
}, [sanitizedBase]);
|
||||
|
||||
setSortableItems((prev) => {
|
||||
const currentValues = extractValues(prev);
|
||||
if (arraysAreSame(currentValues, sanitizedBase)) {
|
||||
return prev;
|
||||
}
|
||||
return buildSortableItems(sanitizedBase);
|
||||
});
|
||||
}, [sanitizedBase, extractValues]);
|
||||
useEffect(() => {
|
||||
const values = sortableItems
|
||||
.map((item) => item?.type || item?.value)
|
||||
.filter(Boolean);
|
||||
const values = extractValues(sortableItems);
|
||||
onChange?.(values);
|
||||
}, [sortableItems, onChange]);
|
||||
}, [sortableItems, onChange, extractValues]);
|
||||
|
||||
const labeledItems = useMemo(() => {
|
||||
const counts = {};
|
||||
@@ -80,40 +89,63 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
[]
|
||||
);
|
||||
|
||||
const selectedTypes = useMemo(() => {
|
||||
const set = new Set();
|
||||
sortableItems.forEach((item) => {
|
||||
if (item?.type) set.add(item.type);
|
||||
else if (item?.value) set.add(item.value);
|
||||
const segmentCounts = useMemo(() => {
|
||||
const counts = {};
|
||||
extractValues(sortableItems).forEach((type) => {
|
||||
counts[type] = (counts[type] || 0) + 1;
|
||||
});
|
||||
return set;
|
||||
}, [sortableItems]);
|
||||
return counts;
|
||||
}, [sortableItems, extractValues]);
|
||||
|
||||
const toggleOptionalSegment = (type) => {
|
||||
setSortableItems((prev) => {
|
||||
const currentValues = prev.map((item) => item.type || item.value);
|
||||
if (currentValues.includes(type)) {
|
||||
const filtered = currentValues.filter((value) => value !== type);
|
||||
return buildSortableItems(filtered);
|
||||
}
|
||||
const handleRemoveItem = useCallback(
|
||||
(itemId) => {
|
||||
setSortableItems((prev) => {
|
||||
const remaining = prev.filter((item) => item.id !== itemId);
|
||||
const sanitized = sanitizeStructureList(extractValues(remaining));
|
||||
return buildSortableItems(sanitized);
|
||||
});
|
||||
},
|
||||
[extractValues]
|
||||
);
|
||||
|
||||
const handleOptionalSegmentPress = useCallback(
|
||||
(type) => {
|
||||
const meta = getSegmentMeta(type);
|
||||
let nextValues = [...currentValues];
|
||||
if (meta?.exclusiveGroup) {
|
||||
nextValues = nextValues.filter((value) => {
|
||||
const itemMeta = getSegmentMeta(value);
|
||||
return itemMeta?.exclusiveGroup !== meta.exclusiveGroup;
|
||||
});
|
||||
}
|
||||
nextValues.push(type);
|
||||
return buildSortableItems(nextValues);
|
||||
});
|
||||
};
|
||||
setSortableItems((prev) => {
|
||||
const currentValues = extractValues(prev);
|
||||
let nextValues = [...currentValues];
|
||||
|
||||
if (meta?.exclusiveGroup) {
|
||||
nextValues = nextValues.filter((value) => {
|
||||
const itemMeta = getSegmentMeta(value);
|
||||
return itemMeta?.exclusiveGroup !== meta.exclusiveGroup;
|
||||
});
|
||||
}
|
||||
|
||||
const allowsMultiple = meta?.allowMultiple !== false;
|
||||
if (!allowsMultiple && nextValues.includes(type)) {
|
||||
const filtered = nextValues.filter((value) => value !== type);
|
||||
const sanitized = sanitizeStructureList(filtered);
|
||||
return buildSortableItems(sanitized);
|
||||
}
|
||||
|
||||
nextValues.push(type);
|
||||
const sanitized = sanitizeStructureList(nextValues);
|
||||
return buildSortableItems(sanitized);
|
||||
});
|
||||
},
|
||||
[extractValues]
|
||||
);
|
||||
|
||||
const Row = ({ item: rowData, onPress }) => {
|
||||
const isButton = typeof onPress === "function";
|
||||
const Wrapper = isButton ? TouchableOpacity : View;
|
||||
const selected = rowData?.selected;
|
||||
const icon = rowData?.icon;
|
||||
const baseType = rowData?.type || rowData?.value;
|
||||
const meta = getSegmentMeta(baseType);
|
||||
const canRemove = rowData?.sortable && meta?.optional === true;
|
||||
const count = rowData?.count ?? 0;
|
||||
return (
|
||||
<Wrapper
|
||||
onPress={onPress}
|
||||
@@ -139,14 +171,29 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
<Text style={styles.rowText}>{rowData?.label || ""}</Text>
|
||||
</View>
|
||||
{rowData?.sortable ? (
|
||||
<Sortable.Handle>
|
||||
<Image source={icons.dragDots} style={styles.handleIcon} />
|
||||
</Sortable.Handle>
|
||||
<View style={styles.rowActions}>
|
||||
<Sortable.Handle>
|
||||
<Image source={icons.dragDots} style={styles.handleIcon} />
|
||||
</Sortable.Handle>
|
||||
{canRemove && (
|
||||
<TouchableOpacity
|
||||
onPress={() => handleRemoveItem(rowData.id)}
|
||||
style={styles.removeButton}
|
||||
hitSlop={{ top: 8, right: 8, bottom: 8, left: 8 }}
|
||||
>
|
||||
<Image source={icons.close} style={styles.removeIcon} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<Image
|
||||
source={icon || (selected ? icons.check : icons.add)}
|
||||
style={[styles.optionIcon]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
styles.countBadge,
|
||||
count > 0 ? styles.countBadgeActive : null,
|
||||
]}
|
||||
>
|
||||
<Text style={styles.countBadgeText}>{count}</Text>
|
||||
</View>
|
||||
)}
|
||||
</BlurView>
|
||||
</View>
|
||||
@@ -166,46 +213,50 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||
contentContainerStyle={{ paddingVertical: 4, paddingBottom: 24 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<>
|
||||
<Sortable.Grid
|
||||
columns={1}
|
||||
rowGap={12}
|
||||
data={labeledItems}
|
||||
renderItem={({ item }) => <Row item={item} />}
|
||||
customHandle
|
||||
scrollableRef={scrollRef}
|
||||
onDragEnd={({ data: newData }) => {
|
||||
const values = (newData || [])
|
||||
.map((it) => it?.type || it?.value)
|
||||
.filter(Boolean);
|
||||
const sanitized = sanitizeStructureList(values);
|
||||
setSortableItems(buildSortableItems(sanitized));
|
||||
}}
|
||||
/>
|
||||
<FlatList
|
||||
data={optionalSegments}
|
||||
gap={12}
|
||||
renderItem={({ item }) => {
|
||||
const selected = selectedTypes.has(item.type);
|
||||
return (
|
||||
<View style={{ marginTop: 12 }}>
|
||||
<Row
|
||||
item={{
|
||||
...item,
|
||||
sortable: false,
|
||||
selected,
|
||||
}}
|
||||
onPress={() => toggleOptionalSegment(item.type)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}}
|
||||
keyExtractor={(item) => item.type}
|
||||
contentContainerStyle={{ paddingBottom: 24 }}
|
||||
scrollEnabled={false}
|
||||
nestedScrollEnabled={false}
|
||||
/>
|
||||
</>
|
||||
<View style={styles.sectionTag}>
|
||||
<Text style={styles.sectionTagText}>Structure actuelle</Text>
|
||||
</View>
|
||||
<Sortable.Grid
|
||||
columns={1}
|
||||
rowGap={12}
|
||||
data={labeledItems}
|
||||
renderItem={({ item }) => <Row item={item} />}
|
||||
customHandle
|
||||
scrollableRef={scrollRef}
|
||||
onDragEnd={({ data: newData }) => {
|
||||
const values = extractValues(newData);
|
||||
const sanitized = sanitizeStructureList(values);
|
||||
setSortableItems(buildSortableItems(sanitized));
|
||||
}}
|
||||
/>
|
||||
<View style={[styles.sectionTag, { marginTop: 24 }]}>
|
||||
<Text style={styles.sectionTagText}>Éléments à ajouter</Text>
|
||||
</View>
|
||||
<FlatList
|
||||
data={optionalSegments}
|
||||
gap={12}
|
||||
renderItem={({ item }) => {
|
||||
const count = segmentCounts[item.type] || 0;
|
||||
const selected = count > 0;
|
||||
return (
|
||||
<View style={{ marginTop: 12 }}>
|
||||
<Row
|
||||
item={{
|
||||
...item,
|
||||
sortable: false,
|
||||
selected,
|
||||
count,
|
||||
}}
|
||||
onPress={() => handleOptionalSegmentPress(item.type)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}}
|
||||
keyExtractor={(item) => item.type}
|
||||
contentContainerStyle={{ paddingBottom: 24 }}
|
||||
scrollEnabled={false}
|
||||
nestedScrollEnabled={false}
|
||||
/>
|
||||
</Animated.ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
@@ -267,10 +318,57 @@ const styles = StyleSheet.create({
|
||||
width: 22,
|
||||
height: 22,
|
||||
tintColor: Palette.white,
|
||||
marginRight: 12,
|
||||
},
|
||||
optionIcon: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
tintColor: Palette.white,
|
||||
},
|
||||
countBadge: {
|
||||
minWidth: 28,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 999,
|
||||
backgroundColor: "rgba(255,255,255,0.08)",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
countBadgeActive: {
|
||||
backgroundColor: Palette.transparentWhite,
|
||||
},
|
||||
countBadgeText: {
|
||||
fontSize: 14,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
},
|
||||
rowActions: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
},
|
||||
removeButton: {
|
||||
padding: 6,
|
||||
borderRadius: 999,
|
||||
backgroundColor: "rgba(255,255,255,0.08)",
|
||||
},
|
||||
removeIcon: {
|
||||
width: 16,
|
||||
height: 16,
|
||||
tintColor: Palette.white,
|
||||
},
|
||||
sectionTag: {
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 999,
|
||||
backgroundColor: "rgba(255,255,255,0.12)",
|
||||
marginBottom: 16,
|
||||
},
|
||||
sectionTagText: {
|
||||
fontSize: 12,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
letterSpacing: 1,
|
||||
textTransform: "uppercase",
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user