chore: add color for category
This commit is contained in:
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user