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 {
id: number;
name: string;
color: string;
created_at: string;
}
@@ -604,11 +605,12 @@ export const getCategories = async (): Promise<Category[]> => {
export const createCategoryAdmin = async (
name: string,
color: string,
): Promise<{ success: boolean; category?: Category; error?: string }> => {
try {
const { data } = await apiClient.post(
`${V2}/admin/protected/categories`,
{ name },
{ name, color },
);
return { success: true, category: data.category };
} catch (error: any) {
@@ -622,11 +624,12 @@ export const createCategoryAdmin = async (
export const updateCategoryAdmin = async (
id: number,
name: string,
color: string,
): Promise<{ success: boolean; category?: Category; error?: string }> => {
try {
const { data } = await apiClient.put(
`${V2}/admin/protected/categories/${id}`,
{ name },
{ name, color },
);
return { success: true, category: data.category };
} catch (error: any) {
@@ -9,6 +9,7 @@ import {
Modal,
KeyboardAvoidingView,
Platform,
ScrollView,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { spacing, fontSize, borderRadius } from "../../theme";
@@ -25,6 +26,15 @@ import Card from "../../components/ui/Card";
import AlertModal from "../../components/ui/AlertModal";
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() {
const { colors } = useTheme();
const [categories, setCategories] = useState<Category[]>([]);
@@ -32,6 +42,8 @@ export default function CategoriesScreen() {
const [modalVisible, setModalVisible] = useState(false);
const [editingCategory, setEditingCategory] = useState<Category | null>(null);
const [name, setName] = useState("");
const [color, setColor] = useState("#7c3aed");
const [hexInput, setHexInput] = useState("#7c3aed");
const [saving, setSaving] = useState(false);
const { alert, showError, showSuccess, showConfirm, hideAlert } = useAlert();
@@ -46,15 +58,29 @@ export default function CategoriesScreen() {
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 = () => {
setEditingCategory(null);
setName("");
selectColor("#7c3aed");
setModalVisible(true);
};
const openEdit = (cat: Category) => {
setEditingCategory(cat);
setName(cat.name);
selectColor(cat.color || "#7c3aed");
setModalVisible(true);
};
@@ -70,9 +96,13 @@ export default function CategoriesScreen() {
showError("Erreur", "Le nom est requis");
return;
}
if (!/^#[0-9A-Fa-f]{6}$/.test(color)) {
showError("Erreur", "Couleur invalide (format: #RRGGBB)");
return;
}
setSaving(true);
if (editingCategory) {
const res = await updateCategoryAdmin(editingCategory.id, trimmed);
const res = await updateCategoryAdmin(editingCategory.id, trimmed, color);
if (res.success) {
showSuccess("Succès", "Catégorie modifiée");
closeModal();
@@ -81,7 +111,7 @@ export default function CategoriesScreen() {
showError("Erreur", res.error || "Erreur");
}
} else {
const res = await createCategoryAdmin(trimmed);
const res = await createCategoryAdmin(trimmed, color);
if (res.success) {
showSuccess("Succès", "Catégorie créée");
closeModal();
@@ -136,6 +166,8 @@ export default function CategoriesScreen() {
alignItems: "center",
justifyContent: "space-between",
},
catLeft: { flexDirection: "row", alignItems: "center", gap: spacing.s, flex: 1 },
colorDot: { width: 16, height: 16, borderRadius: 8 },
catName: {
fontSize: fontSize.md,
color: colors.textPrimary,
@@ -158,6 +190,7 @@ export default function CategoriesScreen() {
gap: spacing.m,
},
modalTitle: { fontSize: fontSize.lg, fontWeight: "700", color: colors.textPrimary },
label: { fontSize: fontSize.sm, color: colors.textSecondary, marginBottom: 4 },
input: {
borderWidth: 1,
borderColor: colors.border,
@@ -167,6 +200,36 @@ export default function CategoriesScreen() {
fontSize: fontSize.md,
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 },
cancelBtn: {
flex: 1,
@@ -214,12 +277,20 @@ export default function CategoriesScreen() {
renderItem={({ item }) => (
<Card>
<View style={styles.row}>
<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 style={styles.catLeft}>
<View
style={[
styles.colorDot,
{ backgroundColor: item.color || "#7c3aed" },
]}
/>
<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 style={styles.actions}>
<TouchableOpacity
@@ -253,10 +324,15 @@ export default function CategoriesScreen() {
behavior={Platform.OS === "ios" ? "padding" : undefined}
style={styles.overlay}
>
<View style={styles.modal}>
<ScrollView
contentContainerStyle={styles.modal}
keyboardShouldPersistTaps="handled"
>
<Text style={styles.modalTitle}>
{editingCategory ? "Modifier la catégorie" : "Nouvelle catégorie"}
</Text>
<Text style={styles.label}>Nom</Text>
<TextInput
style={styles.input}
placeholder="Nom de la catégorie"
@@ -265,6 +341,43 @@ export default function CategoriesScreen() {
onChangeText={setName}
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}>
<TouchableOpacity style={styles.cancelBtn} onPress={closeModal}>
<Text style={styles.cancelBtnText}>Annuler</Text>
@@ -279,7 +392,7 @@ export default function CategoriesScreen() {
</Text>
</TouchableOpacity>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
</Modal>
+1
View File
@@ -116,6 +116,7 @@ export const logoutUser = async (): Promise<void> => {
export interface Category {
id: number;
name: string;
color: string;
created_at: string;
}
+3 -1
View File
@@ -8,15 +8,17 @@ interface CategoryPillProps {
label: string;
active: boolean;
onPress: () => void;
color?: string;
}
export default function CategoryPill({
label,
active,
onPress,
color,
}: CategoryPillProps) {
const { colors } = useTheme();
const catColor = getCategoryColor(label, colors);
const catColor = color || getCategoryColor(label, colors);
return (
<TouchableOpacity
onPress={onPress}
@@ -205,6 +205,7 @@ export default function ProductsScreen() {
label={cat.name}
active={selectedCategory === cat.name}
onPress={() => setSelectedCategory(cat.name)}
color={cat.color}
/>
))}
</ScrollView>