chore: add color for category

This commit is contained in:
2026-03-07 15:48:27 +01:00
parent 8edc14ca55
commit 125d066dcb
5 changed files with 133 additions and 13 deletions
+5 -2
View File
@@ -590,6 +590,7 @@ const V1_PUBLIC = "https://uber-stup.club/api/v1";
export interface Category { export interface Category {
id: number; id: number;
name: string; name: string;
color: string;
created_at: string; created_at: string;
} }
@@ -604,11 +605,12 @@ export const getCategories = async (): Promise<Category[]> => {
export const createCategoryAdmin = async ( export const createCategoryAdmin = async (
name: string, name: string,
color: string,
): Promise<{ success: boolean; category?: Category; error?: string }> => { ): Promise<{ success: boolean; category?: Category; error?: string }> => {
try { try {
const { data } = await apiClient.post( const { data } = await apiClient.post(
`${V2}/admin/protected/categories`, `${V2}/admin/protected/categories`,
{ name }, { name, color },
); );
return { success: true, category: data.category }; return { success: true, category: data.category };
} catch (error: any) { } catch (error: any) {
@@ -622,11 +624,12 @@ export const createCategoryAdmin = async (
export const updateCategoryAdmin = async ( export const updateCategoryAdmin = async (
id: number, id: number,
name: string, name: string,
color: string,
): Promise<{ success: boolean; category?: Category; error?: string }> => { ): Promise<{ success: boolean; category?: Category; error?: string }> => {
try { try {
const { data } = await apiClient.put( const { data } = await apiClient.put(
`${V2}/admin/protected/categories/${id}`, `${V2}/admin/protected/categories/${id}`,
{ name }, { name, color },
); );
return { success: true, category: data.category }; return { success: true, category: data.category };
} catch (error: any) { } catch (error: any) {
@@ -9,6 +9,7 @@ import {
Modal, Modal,
KeyboardAvoidingView, KeyboardAvoidingView,
Platform, Platform,
ScrollView,
} from "react-native"; } from "react-native";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { spacing, fontSize, borderRadius } from "../../theme"; import { spacing, fontSize, borderRadius } from "../../theme";
@@ -25,6 +26,15 @@ import Card from "../../components/ui/Card";
import AlertModal from "../../components/ui/AlertModal"; import AlertModal from "../../components/ui/AlertModal";
import { useAlert } from "../../hooks/useAlert"; import { useAlert } from "../../hooks/useAlert";
const PRESET_COLORS = [
"#7c3aed", "#9333ea", "#6366f1",
"#3b82f6", "#0ea5e9", "#06b6d4",
"#10b981", "#22c55e", "#84cc16",
"#f59e0b", "#f97316", "#ef4444",
"#ec4899", "#f472b6", "#ffffff",
"#a3a3a3", "#1a1a1a", "#000000",
];
export default function CategoriesScreen() { export default function CategoriesScreen() {
const { colors } = useTheme(); const { colors } = useTheme();
const [categories, setCategories] = useState<Category[]>([]); const [categories, setCategories] = useState<Category[]>([]);
@@ -32,6 +42,8 @@ export default function CategoriesScreen() {
const [modalVisible, setModalVisible] = useState(false); const [modalVisible, setModalVisible] = useState(false);
const [editingCategory, setEditingCategory] = useState<Category | null>(null); const [editingCategory, setEditingCategory] = useState<Category | null>(null);
const [name, setName] = useState(""); const [name, setName] = useState("");
const [color, setColor] = useState("#7c3aed");
const [hexInput, setHexInput] = useState("#7c3aed");
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const { alert, showError, showSuccess, showConfirm, hideAlert } = useAlert(); const { alert, showError, showSuccess, showConfirm, hideAlert } = useAlert();
@@ -46,15 +58,29 @@ export default function CategoriesScreen() {
load(); load();
}, [load]); }, [load]);
const selectColor = (c: string) => {
setColor(c);
setHexInput(c);
};
const handleHexInput = (val: string) => {
setHexInput(val);
if (/^#[0-9A-Fa-f]{6}$/.test(val)) {
setColor(val);
}
};
const openCreate = () => { const openCreate = () => {
setEditingCategory(null); setEditingCategory(null);
setName(""); setName("");
selectColor("#7c3aed");
setModalVisible(true); setModalVisible(true);
}; };
const openEdit = (cat: Category) => { const openEdit = (cat: Category) => {
setEditingCategory(cat); setEditingCategory(cat);
setName(cat.name); setName(cat.name);
selectColor(cat.color || "#7c3aed");
setModalVisible(true); setModalVisible(true);
}; };
@@ -70,9 +96,13 @@ export default function CategoriesScreen() {
showError("Erreur", "Le nom est requis"); showError("Erreur", "Le nom est requis");
return; return;
} }
if (!/^#[0-9A-Fa-f]{6}$/.test(color)) {
showError("Erreur", "Couleur invalide (format: #RRGGBB)");
return;
}
setSaving(true); setSaving(true);
if (editingCategory) { if (editingCategory) {
const res = await updateCategoryAdmin(editingCategory.id, trimmed); const res = await updateCategoryAdmin(editingCategory.id, trimmed, color);
if (res.success) { if (res.success) {
showSuccess("Succès", "Catégorie modifiée"); showSuccess("Succès", "Catégorie modifiée");
closeModal(); closeModal();
@@ -81,7 +111,7 @@ export default function CategoriesScreen() {
showError("Erreur", res.error || "Erreur"); showError("Erreur", res.error || "Erreur");
} }
} else { } else {
const res = await createCategoryAdmin(trimmed); const res = await createCategoryAdmin(trimmed, color);
if (res.success) { if (res.success) {
showSuccess("Succès", "Catégorie créée"); showSuccess("Succès", "Catégorie créée");
closeModal(); closeModal();
@@ -136,6 +166,8 @@ export default function CategoriesScreen() {
alignItems: "center", alignItems: "center",
justifyContent: "space-between", justifyContent: "space-between",
}, },
catLeft: { flexDirection: "row", alignItems: "center", gap: spacing.s, flex: 1 },
colorDot: { width: 16, height: 16, borderRadius: 8 },
catName: { catName: {
fontSize: fontSize.md, fontSize: fontSize.md,
color: colors.textPrimary, color: colors.textPrimary,
@@ -158,6 +190,7 @@ export default function CategoriesScreen() {
gap: spacing.m, gap: spacing.m,
}, },
modalTitle: { fontSize: fontSize.lg, fontWeight: "700", color: colors.textPrimary }, modalTitle: { fontSize: fontSize.lg, fontWeight: "700", color: colors.textPrimary },
label: { fontSize: fontSize.sm, color: colors.textSecondary, marginBottom: 4 },
input: { input: {
borderWidth: 1, borderWidth: 1,
borderColor: colors.border, borderColor: colors.border,
@@ -167,6 +200,36 @@ export default function CategoriesScreen() {
fontSize: fontSize.md, fontSize: fontSize.md,
backgroundColor: colors.bgInput, backgroundColor: colors.bgInput,
}, },
colorSection: { gap: spacing.s },
previewRow: { flexDirection: "row", alignItems: "center", gap: spacing.m },
previewDot: {
width: 36,
height: 36,
borderRadius: 18,
borderWidth: 2,
borderColor: colors.border,
},
hexInput: {
flex: 1,
borderWidth: 1,
borderColor: colors.border,
borderRadius: borderRadius.md,
padding: spacing.s,
color: colors.textPrimary,
fontSize: fontSize.md,
backgroundColor: colors.bgInput,
},
paletteGrid: {
flexDirection: "row",
flexWrap: "wrap",
gap: spacing.s,
},
colorSwatch: {
width: 32,
height: 32,
borderRadius: borderRadius.sm,
borderWidth: 2,
},
modalBtns: { flexDirection: "row", gap: spacing.s }, modalBtns: { flexDirection: "row", gap: spacing.s },
cancelBtn: { cancelBtn: {
flex: 1, flex: 1,
@@ -214,12 +277,20 @@ export default function CategoriesScreen() {
renderItem={({ item }) => ( renderItem={({ item }) => (
<Card> <Card>
<View style={styles.row}> <View style={styles.row}>
<View> <View style={styles.catLeft}>
<Text style={styles.catName}>{item.name}</Text> <View
<Text style={styles.catDate}> style={[
Créée le{" "} styles.colorDot,
{new Date(item.created_at).toLocaleDateString("fr-FR")} { backgroundColor: item.color || "#7c3aed" },
</Text> ]}
/>
<View>
<Text style={styles.catName}>{item.name}</Text>
<Text style={styles.catDate}>
Créée le{" "}
{new Date(item.created_at).toLocaleDateString("fr-FR")}
</Text>
</View>
</View> </View>
<View style={styles.actions}> <View style={styles.actions}>
<TouchableOpacity <TouchableOpacity
@@ -253,10 +324,15 @@ export default function CategoriesScreen() {
behavior={Platform.OS === "ios" ? "padding" : undefined} behavior={Platform.OS === "ios" ? "padding" : undefined}
style={styles.overlay} style={styles.overlay}
> >
<View style={styles.modal}> <ScrollView
contentContainerStyle={styles.modal}
keyboardShouldPersistTaps="handled"
>
<Text style={styles.modalTitle}> <Text style={styles.modalTitle}>
{editingCategory ? "Modifier la catégorie" : "Nouvelle catégorie"} {editingCategory ? "Modifier la catégorie" : "Nouvelle catégorie"}
</Text> </Text>
<Text style={styles.label}>Nom</Text>
<TextInput <TextInput
style={styles.input} style={styles.input}
placeholder="Nom de la catégorie" placeholder="Nom de la catégorie"
@@ -265,6 +341,43 @@ export default function CategoriesScreen() {
onChangeText={setName} onChangeText={setName}
autoCapitalize="none" autoCapitalize="none"
/> />
<View style={styles.colorSection}>
<Text style={styles.label}>Couleur du filtre</Text>
<View style={styles.previewRow}>
<View
style={[styles.previewDot, { backgroundColor: color }]}
/>
<TextInput
style={styles.hexInput}
value={hexInput}
onChangeText={handleHexInput}
placeholder="#RRGGBB"
placeholderTextColor={colors.textSecondary}
autoCapitalize="none"
maxLength={7}
/>
</View>
<View style={styles.paletteGrid}>
{PRESET_COLORS.map((c) => (
<TouchableOpacity
key={c}
style={[
styles.colorSwatch,
{
backgroundColor: c,
borderColor:
color === c
? colors.textWhite
: "transparent",
},
]}
onPress={() => selectColor(c)}
/>
))}
</View>
</View>
<View style={styles.modalBtns}> <View style={styles.modalBtns}>
<TouchableOpacity style={styles.cancelBtn} onPress={closeModal}> <TouchableOpacity style={styles.cancelBtn} onPress={closeModal}>
<Text style={styles.cancelBtnText}>Annuler</Text> <Text style={styles.cancelBtnText}>Annuler</Text>
@@ -279,7 +392,7 @@ export default function CategoriesScreen() {
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </ScrollView>
</KeyboardAvoidingView> </KeyboardAvoidingView>
</Modal> </Modal>
+1
View File
@@ -116,6 +116,7 @@ export const logoutUser = async (): Promise<void> => {
export interface Category { export interface Category {
id: number; id: number;
name: string; name: string;
color: string;
created_at: string; created_at: string;
} }
+3 -1
View File
@@ -8,15 +8,17 @@ interface CategoryPillProps {
label: string; label: string;
active: boolean; active: boolean;
onPress: () => void; onPress: () => void;
color?: string;
} }
export default function CategoryPill({ export default function CategoryPill({
label, label,
active, active,
onPress, onPress,
color,
}: CategoryPillProps) { }: CategoryPillProps) {
const { colors } = useTheme(); const { colors } = useTheme();
const catColor = getCategoryColor(label, colors); const catColor = color || getCategoryColor(label, colors);
return ( return (
<TouchableOpacity <TouchableOpacity
onPress={onPress} onPress={onPress}
@@ -205,6 +205,7 @@ export default function ProductsScreen() {
label={cat.name} label={cat.name}
active={selectedCategory === cat.name} active={selectedCategory === cat.name}
onPress={() => setSelectedCategory(cat.name)} onPress={() => setSelectedCategory(cat.name)}
color={cat.color}
/> />
))} ))}
</ScrollView> </ScrollView>