chore: features category
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
"simulator": true
|
||||
},
|
||||
"env": {
|
||||
"API_URL": "https://uber-stup.club"
|
||||
"API_URL": "http://5.181.0.112"
|
||||
}
|
||||
},
|
||||
"preview": {
|
||||
@@ -24,7 +24,7 @@
|
||||
"buildType": "apk"
|
||||
},
|
||||
"env": {
|
||||
"API_URL": "https://uber-stup.club"
|
||||
"API_URL": "http://5.181.0.112"
|
||||
}
|
||||
},
|
||||
"production": {
|
||||
@@ -34,7 +34,7 @@
|
||||
"buildType": "apk"
|
||||
},
|
||||
"env": {
|
||||
"API_URL": "https://uber-stup.club"
|
||||
"API_URL": "http://5.181.0.112"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import type {
|
||||
Alert,
|
||||
} from "./types";
|
||||
|
||||
const V2 = "https://uber-stup.club/api/v2";
|
||||
const CABINE_URL = "https://uber-stup.club/api/v1/cabine";
|
||||
const V2 = "http://5.181.0.112/api/v2";
|
||||
const CABINE_URL = "http://5.181.0.112/api/v1/cabine";
|
||||
|
||||
// ============================================
|
||||
// AUTH
|
||||
@@ -580,3 +580,73 @@ export const getCommandCountByStatus = async (
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// CATÉGORIES
|
||||
// ============================================
|
||||
|
||||
const V1_PUBLIC = "https://uber-stup.club/api/v1";
|
||||
|
||||
export interface Category {
|
||||
id: number;
|
||||
name: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export const getCategories = async (): Promise<Category[]> => {
|
||||
try {
|
||||
const { data } = await apiClient.get(`${V1_PUBLIC}/categories`);
|
||||
return data.categories || [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const createCategoryAdmin = async (
|
||||
name: string,
|
||||
): Promise<{ success: boolean; category?: Category; error?: string }> => {
|
||||
try {
|
||||
const { data } = await apiClient.post(
|
||||
`${V2}/admin/protected/categories`,
|
||||
{ name },
|
||||
);
|
||||
return { success: true, category: data.category };
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.error || "Erreur",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const updateCategoryAdmin = async (
|
||||
id: number,
|
||||
name: string,
|
||||
): Promise<{ success: boolean; category?: Category; error?: string }> => {
|
||||
try {
|
||||
const { data } = await apiClient.put(
|
||||
`${V2}/admin/protected/categories/${id}`,
|
||||
{ name },
|
||||
);
|
||||
return { success: true, category: data.category };
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.error || "Erreur",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteCategoryAdmin = async (
|
||||
id: number,
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
try {
|
||||
await apiClient.delete(`${V2}/admin/protected/categories/${id}`);
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.error || "Erreur",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,8 +6,8 @@ import type {
|
||||
Alert,
|
||||
} from "./types";
|
||||
|
||||
const API = "https://uber-stup.club/api/v1/cabine";
|
||||
const V2 = "https://uber-stup.club/api/v2";
|
||||
const API = "http://5.181.0.112/api/v1/cabine";
|
||||
const V2 = "http://5.181.0.112/api/v2";
|
||||
|
||||
// ============================================
|
||||
// ITEMS
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
Alert,
|
||||
} from "./types";
|
||||
|
||||
const API = "https://uber-stup.club/api/v1/livreur";
|
||||
const API = "http://5.181.0.112/api/v1/livreur";
|
||||
|
||||
// ============================================
|
||||
// STATUT
|
||||
|
||||
@@ -2,7 +2,7 @@ import axios from "axios";
|
||||
import { getToken, getAdminToken } from "../auth/tokenStorage";
|
||||
|
||||
// Change this to your server IP/domain
|
||||
export const API_BASE_URL = "https://uber-stup.club";
|
||||
export const API_BASE_URL = "http://5.181.0.112";
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
|
||||
@@ -14,6 +14,7 @@ import OrdersScreen from "../screens/admin/OrdersScreen";
|
||||
import OrderDetailScreen from "../screens/admin/OrderDetailScreen";
|
||||
import UsersScreen from "../screens/admin/UsersScreen";
|
||||
import ProductsScreen from "../screens/admin/ProductsScreen";
|
||||
import CategoriesScreen from "../screens/admin/CategoriesScreen";
|
||||
import DeliveryScreen from "../screens/admin/DeliveryScreen";
|
||||
import AlertsScreen from "../screens/admin/AlertsScreen";
|
||||
import AddressScreen from "../screens/admin/AddressScreen";
|
||||
@@ -128,6 +129,20 @@ function AdminTabs() {
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Categories"
|
||||
component={CategoriesScreen}
|
||||
options={{
|
||||
title: "Catégories",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons
|
||||
name="pricetag-outline"
|
||||
size={size}
|
||||
color={color}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Delivery"
|
||||
component={DeliveryScreen}
|
||||
|
||||
@@ -10,6 +10,7 @@ export type AdminTabParamList = {
|
||||
Orders: undefined;
|
||||
Users: undefined;
|
||||
Products: undefined;
|
||||
Categories: undefined;
|
||||
Delivery: undefined;
|
||||
Alerts: undefined;
|
||||
Addresses: undefined;
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
FlatList,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
Modal,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { spacing, fontSize, borderRadius } from "../../theme";
|
||||
import { useTheme } from "../../context/ThemeContext";
|
||||
import {
|
||||
getCategories,
|
||||
createCategoryAdmin,
|
||||
updateCategoryAdmin,
|
||||
deleteCategoryAdmin,
|
||||
} from "../../api/api_admin";
|
||||
import type { Category } from "../../api/api_admin";
|
||||
import LoadingSpinner from "../../components/ui/LoadingSpinner";
|
||||
import Card from "../../components/ui/Card";
|
||||
import AlertModal from "../../components/ui/AlertModal";
|
||||
import { useAlert } from "../../hooks/useAlert";
|
||||
|
||||
export default function CategoriesScreen() {
|
||||
const { colors } = useTheme();
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [editingCategory, setEditingCategory] = useState<Category | null>(null);
|
||||
const [name, setName] = useState("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const { alert, showError, showSuccess, showConfirm, hideAlert } = useAlert();
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
const data = await getCategories();
|
||||
setCategories(data);
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
const openCreate = () => {
|
||||
setEditingCategory(null);
|
||||
setName("");
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const openEdit = (cat: Category) => {
|
||||
setEditingCategory(cat);
|
||||
setName(cat.name);
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setModalVisible(false);
|
||||
setName("");
|
||||
setEditingCategory(null);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed) {
|
||||
showError("Erreur", "Le nom est requis");
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
if (editingCategory) {
|
||||
const res = await updateCategoryAdmin(editingCategory.id, trimmed);
|
||||
if (res.success) {
|
||||
showSuccess("Succès", "Catégorie modifiée");
|
||||
closeModal();
|
||||
load();
|
||||
} else {
|
||||
showError("Erreur", res.error || "Erreur");
|
||||
}
|
||||
} else {
|
||||
const res = await createCategoryAdmin(trimmed);
|
||||
if (res.success) {
|
||||
showSuccess("Succès", "Catégorie créée");
|
||||
closeModal();
|
||||
load();
|
||||
} else {
|
||||
showError("Erreur", res.error || "Erreur");
|
||||
}
|
||||
}
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
const handleDelete = (cat: Category) => {
|
||||
showConfirm(
|
||||
"Supprimer",
|
||||
`Supprimer la catégorie "${cat.name}" ?`,
|
||||
async () => {
|
||||
const res = await deleteCategoryAdmin(cat.id);
|
||||
if (res.success) {
|
||||
showSuccess("Succès", "Catégorie supprimée");
|
||||
load();
|
||||
} else {
|
||||
showError("Erreur", res.error || "Erreur");
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: colors.background },
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: spacing.m,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
},
|
||||
title: { fontSize: fontSize.xl, fontWeight: "700", color: colors.text },
|
||||
addBtn: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
backgroundColor: colors.accent,
|
||||
paddingHorizontal: spacing.m,
|
||||
paddingVertical: spacing.s,
|
||||
borderRadius: borderRadius.m,
|
||||
},
|
||||
addBtnText: { color: "#fff", fontWeight: "600", fontSize: fontSize.s },
|
||||
list: { padding: spacing.m, gap: spacing.s },
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
catName: { fontSize: fontSize.m, color: colors.text, fontWeight: "600", textTransform: "capitalize" },
|
||||
catDate: { fontSize: fontSize.xs, color: colors.textSecondary, marginTop: 2 },
|
||||
actions: { flexDirection: "row", gap: spacing.s },
|
||||
actionBtn: { padding: 8 },
|
||||
overlay: {
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(0,0,0,0.5)",
|
||||
justifyContent: "center",
|
||||
padding: spacing.l,
|
||||
},
|
||||
modal: {
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: borderRadius.l,
|
||||
padding: spacing.l,
|
||||
gap: spacing.m,
|
||||
},
|
||||
modalTitle: { fontSize: fontSize.l, fontWeight: "700", color: colors.text },
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderRadius: borderRadius.m,
|
||||
padding: spacing.m,
|
||||
color: colors.text,
|
||||
fontSize: fontSize.m,
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
modalBtns: { flexDirection: "row", gap: spacing.s },
|
||||
cancelBtn: {
|
||||
flex: 1,
|
||||
padding: spacing.m,
|
||||
borderRadius: borderRadius.m,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
alignItems: "center",
|
||||
},
|
||||
cancelBtnText: { color: colors.text, fontWeight: "600" },
|
||||
saveBtn: {
|
||||
flex: 1,
|
||||
padding: spacing.m,
|
||||
borderRadius: borderRadius.m,
|
||||
backgroundColor: colors.accent,
|
||||
alignItems: "center",
|
||||
},
|
||||
saveBtnText: { color: "#fff", fontWeight: "600" },
|
||||
empty: { textAlign: "center", color: colors.textSecondary, marginTop: spacing.xl },
|
||||
});
|
||||
|
||||
if (loading) return <LoadingSpinner />;
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>Catégories</Text>
|
||||
<TouchableOpacity style={styles.addBtn} onPress={openCreate}>
|
||||
<Ionicons name="add" size={18} color="#fff" />
|
||||
<Text style={styles.addBtnText}>Ajouter</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<FlatList
|
||||
data={categories}
|
||||
keyExtractor={(item) => String(item.id)}
|
||||
contentContainerStyle={styles.list}
|
||||
ListEmptyComponent={
|
||||
<Text style={styles.empty}>Aucune catégorie. Créez-en une !</Text>
|
||||
}
|
||||
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>
|
||||
<View style={styles.actions}>
|
||||
<TouchableOpacity
|
||||
style={styles.actionBtn}
|
||||
onPress={() => openEdit(item)}
|
||||
>
|
||||
<Ionicons name="pencil-outline" size={20} color={colors.info} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={styles.actionBtn}
|
||||
onPress={() => handleDelete(item)}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={20} color={colors.danger} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</Card>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Modal visible={modalVisible} transparent animationType="fade">
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : undefined}
|
||||
style={styles.overlay}
|
||||
>
|
||||
<View style={styles.modal}>
|
||||
<Text style={styles.modalTitle}>
|
||||
{editingCategory ? "Modifier la catégorie" : "Nouvelle catégorie"}
|
||||
</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Nom de la catégorie"
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
<View style={styles.modalBtns}>
|
||||
<TouchableOpacity style={styles.cancelBtn} onPress={closeModal}>
|
||||
<Text style={styles.cancelBtnText}>Annuler</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={styles.saveBtn}
|
||||
onPress={handleSave}
|
||||
disabled={saving}
|
||||
>
|
||||
<Text style={styles.saveBtnText}>
|
||||
{saving ? "..." : "Enregistrer"}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</Modal>
|
||||
|
||||
<AlertModal
|
||||
visible={alert.visible}
|
||||
type={alert.type}
|
||||
title={alert.title}
|
||||
message={alert.message}
|
||||
onConfirm={alert.onConfirm || hideAlert}
|
||||
onCancel={alert.type === "confirm" ? hideAlert : undefined}
|
||||
confirmText={alert.type === "confirm" ? "Confirmer" : "OK"}
|
||||
cancelText="Annuler"
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -25,7 +25,9 @@ import {
|
||||
deleteProductAdmin,
|
||||
uploadProductMediaAdmin,
|
||||
deleteProductMediaAdmin,
|
||||
getCategories,
|
||||
} from "../../api/api_admin";
|
||||
import type { Category } from "../../api/api_admin";
|
||||
import type { Product } from "../../api/types";
|
||||
import LoadingSpinner from "../../components/ui/LoadingSpinner";
|
||||
import Card from "../../components/ui/Card";
|
||||
@@ -54,11 +56,7 @@ interface PendingMedia {
|
||||
mediaType: "image" | "video";
|
||||
}
|
||||
|
||||
const CATEGORIES = [
|
||||
{ value: "weed&hash", label: "Weed & Hash" },
|
||||
{ value: "zipette&co", label: "Zipette & Co" },
|
||||
{ value: "gros&semi", label: "Gros & Semi" },
|
||||
];
|
||||
// Les catégories sont chargées dynamiquement depuis l'API
|
||||
|
||||
const UNITS = [
|
||||
{ value: "u", label: "u" },
|
||||
@@ -79,9 +77,9 @@ interface FormState {
|
||||
prices: PriceRow[];
|
||||
}
|
||||
|
||||
const emptyForm = (): FormState => ({
|
||||
const emptyForm = (firstCategory = ""): FormState => ({
|
||||
name: "",
|
||||
category: "weed&hash",
|
||||
category: firstCategory,
|
||||
description: "",
|
||||
stock: "",
|
||||
unit: "u",
|
||||
@@ -94,6 +92,7 @@ const emptyForm = (): FormState => ({
|
||||
export default function ProductsScreen() {
|
||||
const { colors } = useTheme();
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
@@ -117,8 +116,12 @@ export default function ProductsScreen() {
|
||||
// --------------------------------------------------
|
||||
const loadData = useCallback(async () => {
|
||||
try {
|
||||
const result = await getAllProductsAdmin();
|
||||
const [result, cats] = await Promise.all([
|
||||
getAllProductsAdmin(),
|
||||
getCategories(),
|
||||
]);
|
||||
setProducts(result.data);
|
||||
setCategories(cats);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
@@ -139,7 +142,7 @@ export default function ProductsScreen() {
|
||||
// --------------------------------------------------
|
||||
const openCreate = () => {
|
||||
setEditingProduct(null);
|
||||
setForm(emptyForm());
|
||||
setForm(emptyForm(categories[0]?.name || ""));
|
||||
setExistingMedia([]);
|
||||
setMediaToDelete([]);
|
||||
setPendingMedia([]);
|
||||
@@ -896,29 +899,29 @@ export default function ProductsScreen() {
|
||||
{/* Catégorie */}
|
||||
<Text style={styles.label}>Catégorie *</Text>
|
||||
<View style={styles.catRow}>
|
||||
{CATEGORIES.map((c) => (
|
||||
{categories.map((c) => (
|
||||
<TouchableOpacity
|
||||
key={c.value}
|
||||
key={c.id}
|
||||
style={[
|
||||
styles.catBtn,
|
||||
form.category === c.value &&
|
||||
form.category === c.name &&
|
||||
styles.catBtnActive,
|
||||
]}
|
||||
onPress={() =>
|
||||
setForm((f) => ({
|
||||
...f,
|
||||
category: c.value,
|
||||
category: c.name,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.catBtnText,
|
||||
form.category === c.value &&
|
||||
form.category === c.name &&
|
||||
styles.catBtnTextActive,
|
||||
]}
|
||||
>
|
||||
{c.label}
|
||||
{c.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
|
||||
+3
-3
@@ -15,7 +15,7 @@
|
||||
"simulator": true
|
||||
},
|
||||
"env": {
|
||||
"API_URL": "https://uber-stup.club"
|
||||
"API_URL": "http://5.181.0.112"
|
||||
},
|
||||
"channel": "development"
|
||||
},
|
||||
@@ -25,7 +25,7 @@
|
||||
"buildType": "apk"
|
||||
},
|
||||
"env": {
|
||||
"API_URL": "https://uber-stup.club"
|
||||
"API_URL": "http://5.181.0.112"
|
||||
},
|
||||
"channel": "preview"
|
||||
},
|
||||
@@ -36,7 +36,7 @@
|
||||
"buildType": "apk"
|
||||
},
|
||||
"env": {
|
||||
"API_URL": "https://uber-stup.club"
|
||||
"API_URL": "http://5.181.0.112"
|
||||
},
|
||||
"channel": "production"
|
||||
}
|
||||
|
||||
+16
-1
@@ -11,7 +11,7 @@ import type {
|
||||
import { getToken } from "../auth/tokenStorage";
|
||||
import { extractUsernameFromToken } from "../auth/jwtUtils";
|
||||
|
||||
const V1 = "https://uber-stup.club/api/v1";
|
||||
const V1 = "http://5.181.0.112/api/v1";
|
||||
|
||||
export const getJwtUsername = async (): Promise<string | null> => {
|
||||
const token = await getToken();
|
||||
@@ -113,6 +113,21 @@ export const logoutUser = async (): Promise<void> => {
|
||||
// PRODUCTS
|
||||
// ============================================
|
||||
|
||||
export interface Category {
|
||||
id: number;
|
||||
name: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export const getCategories = async (): Promise<Category[]> => {
|
||||
try {
|
||||
const { data } = await apiClient.get(`${V1}/categories`);
|
||||
return data.categories || [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const getAllProducts = async () => {
|
||||
try {
|
||||
const { data } = await apiClient.get(`${V1}/products`);
|
||||
|
||||
@@ -2,7 +2,7 @@ import axios from "axios";
|
||||
import { getToken, getAdminToken } from "../auth/tokenStorage";
|
||||
|
||||
// Change this to your server IP/domain
|
||||
export const API_BASE_URL = "https://uber-stup.club";
|
||||
export const API_BASE_URL = "http://5.181.0.112";
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
|
||||
@@ -18,7 +18,8 @@ import {
|
||||
} from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { getAllProducts, getProductsByCategory } from "../../api/api";
|
||||
import { getAllProducts, getProductsByCategory, getCategories } from "../../api/api";
|
||||
import type { Category } from "../../api/api";
|
||||
import type { Product } from "../../api/api_types";
|
||||
import ProductCard from "../../components/ProductCard";
|
||||
import CategoryPill from "../../components/CategoryPill";
|
||||
@@ -33,25 +34,14 @@ const logoGrosSemi = require("../../../assets/logo-gros-semi.png");
|
||||
const { width: SCREEN_WIDTH } = Dimensions.get("window");
|
||||
const CARD_WIDTH = SCREEN_WIDTH - 48;
|
||||
|
||||
const CATEGORIES = [
|
||||
{ label: "Tous", value: "tous" },
|
||||
{ label: "Weed&Hash", value: "weed&hash" },
|
||||
{ label: "Zipette&Co", value: "zipette&co" },
|
||||
{ label: "Gros&Semi", value: "gros&semi" },
|
||||
];
|
||||
|
||||
const CATEGORY_TITLES: Record<string, string> = {
|
||||
tous: "Tous les produits",
|
||||
"weed&hash": "Weed & Hash",
|
||||
"zipette&co": "Zipette & Co",
|
||||
"gros&semi": "Gros & Semi",
|
||||
};
|
||||
// Les catégories sont chargées dynamiquement depuis l'API
|
||||
|
||||
type Nav = NativeStackNavigationProp<ClientStackParamList>;
|
||||
|
||||
export default function ProductsScreen() {
|
||||
const { colors } = useTheme();
|
||||
const navigation = useNavigation<Nav>();
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [selectedCategory, setSelectedCategory] = useState("tous");
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -60,6 +50,10 @@ export default function ProductsScreen() {
|
||||
const carouselRef = useRef<FlatList>(null);
|
||||
const scaleAnim = useRef(new Animated.Value(1)).current;
|
||||
|
||||
useEffect(() => {
|
||||
getCategories().then(setCategories);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const pulse = Animated.loop(
|
||||
Animated.sequence([
|
||||
@@ -190,7 +184,7 @@ export default function ProductsScreen() {
|
||||
|
||||
return (
|
||||
<ImageBackground
|
||||
source={selectedCategory === "gros&semi" ? logoGrosSemi : undefined}
|
||||
source={undefined}
|
||||
style={styles.container}
|
||||
imageStyle={styles.bgImage}
|
||||
>
|
||||
@@ -200,19 +194,24 @@ export default function ProductsScreen() {
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.filters}
|
||||
>
|
||||
{CATEGORIES.map((cat) => (
|
||||
<CategoryPill
|
||||
key={cat.value}
|
||||
label={cat.label}
|
||||
active={selectedCategory === cat.value}
|
||||
onPress={() => setSelectedCategory(cat.value)}
|
||||
label="Tous"
|
||||
active={selectedCategory === "tous"}
|
||||
onPress={() => setSelectedCategory("tous")}
|
||||
/>
|
||||
{categories.map((cat) => (
|
||||
<CategoryPill
|
||||
key={cat.id}
|
||||
label={cat.name}
|
||||
active={selectedCategory === cat.name}
|
||||
onPress={() => setSelectedCategory(cat.name)}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
<View style={styles.categoryHeader}>
|
||||
<Text style={styles.categoryTitle}>
|
||||
{CATEGORY_TITLES[selectedCategory]}
|
||||
{selectedCategory === "tous" ? "Tous les produits" : selectedCategory}
|
||||
</Text>
|
||||
</View>
|
||||
{error ? (
|
||||
@@ -258,18 +257,6 @@ export default function ProductsScreen() {
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{selectedCategory === "gros&semi" && (
|
||||
<View style={styles.comingSoonWrapper}>
|
||||
<Animated.Text
|
||||
style={[
|
||||
styles.comingSoonText,
|
||||
{ transform: [{ scale: scaleAnim }] },
|
||||
]}
|
||||
>
|
||||
Prochainement
|
||||
</Animated.Text>
|
||||
</View>
|
||||
)}
|
||||
</ImageBackground>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user