chore: update
This commit is contained in:
@@ -104,14 +104,8 @@ func (d *Database) AssignDeliveryPerson(commandID int, livreurUsername string) e
|
||||
// ÉTAPE 3: Vérifier que la commande est assignable
|
||||
// ============================================
|
||||
|
||||
// ✅ Vérifier si déjà assignée à un autre livreur
|
||||
if currentLivreur.Valid && currentLivreur.String != "" && currentLivreur.String != livreurUsername {
|
||||
log.Printf("❌ Commande déjà assignée à: %s", currentLivreur.String)
|
||||
return fmt.Errorf("commande déjà assignée au livreur '%s'", currentLivreur.String)
|
||||
}
|
||||
|
||||
// ✅ Vérifier le statut
|
||||
validStatusesForAssignment := []string{"pending"}
|
||||
// ✅ Vérifier le statut (pending ou assigned pour permettre la réassignation)
|
||||
validStatusesForAssignment := []string{"pending", "assigned"}
|
||||
isValidStatus := false
|
||||
for _, vs := range validStatusesForAssignment {
|
||||
if currentStatus == vs {
|
||||
@@ -135,8 +129,7 @@ func (d *Database) AssignDeliveryPerson(commandID int, livreurUsername string) e
|
||||
status = 'assigned',
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $2
|
||||
AND status IN ('pending')
|
||||
AND (livreur_assign IS NULL OR livreur_assign = '' OR livreur_assign = $1)`
|
||||
AND status IN ('pending', 'assigned')`
|
||||
|
||||
result, err := tx.Exec(updateQuery, livreurUsername, commandID)
|
||||
if err != nil {
|
||||
|
||||
@@ -172,6 +172,48 @@ func (d *Database) NotifyAllAdminCabine(commandID int, clientUsername, deliveryA
|
||||
log.Printf("📬 [ADMIN_NOTIF] Notif Redis + push (%d tokens) pour commande #%d", sent, commandID)
|
||||
}
|
||||
|
||||
// NotifyAllAdminCabineAlert envoie une notification Redis + push à tous les admins/cabines
|
||||
// lors du déclenchement d'une alerte par un livreur.
|
||||
func (d *Database) NotifyAllAdminCabineAlert(alertID int, livreurUsername, alertMessage string) {
|
||||
rows, err := d.Query(
|
||||
`SELECT username, COALESCE(push_token, '') FROM users WHERE role IN ('admin','cabine')`,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("❌ [ALERT_NOTIF] Erreur lecture users admin/cabine: %v", err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
title := "🚨 Alerte livreur"
|
||||
body := fmt.Sprintf("%s — livreur : %s", alertMessage, livreurUsername)
|
||||
|
||||
notification := map[string]interface{}{
|
||||
"alert_id": alertID,
|
||||
"type": "alert",
|
||||
"message": body,
|
||||
"created_at": time.Now().Format(time.RFC3339),
|
||||
"read": false,
|
||||
}
|
||||
notifJSON, _ := json.Marshal(notification)
|
||||
|
||||
sent := 0
|
||||
for rows.Next() {
|
||||
var username, token string
|
||||
if err := rows.Scan(&username, &token); err != nil {
|
||||
continue
|
||||
}
|
||||
notifKey := fmt.Sprintf("notifications:%s", username)
|
||||
Redis.LPush(RedisCtx, notifKey, notifJSON)
|
||||
Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour)
|
||||
|
||||
if token != "" {
|
||||
go sendExpoPushWithChannel(token, title, body, alertID, "alert", "orders")
|
||||
sent++
|
||||
}
|
||||
}
|
||||
log.Printf("🚨 [ALERT_NOTIF] Notif Redis + push (%d tokens) pour alerte #%d de %s", sent, alertID, livreurUsername)
|
||||
}
|
||||
|
||||
// AddDeliveryRating ajoute une note pour un livreur
|
||||
func (d *Database) AddDeliveryRating(livreurUsername string, commandID, rating int, comment string) error {
|
||||
query := `
|
||||
|
||||
@@ -74,6 +74,7 @@ type AppSettings struct {
|
||||
PointsPools []PointsPool `json:"points_pools"` // types de points personnalisés
|
||||
ReferralEnabled bool `json:"referral_enabled"` // activer/désactiver le système de parrainage
|
||||
CryptoPaymentEnabled bool `json:"crypto_payment_enabled"` // activer/désactiver le paiement crypto
|
||||
CryptoOnly bool `json:"crypto_only"` // forcer le paiement crypto uniquement (pas d'espèces)
|
||||
NowPaymentsAPIKey string `json:"nowpayments_api_key"` // clé API NowPayments
|
||||
NowPaymentsIPNSecret string `json:"nowpayments_ipn_secret"` // secret IPN NowPayments
|
||||
NowPaymentsCurrencies []string `json:"nowpayments_currencies"` // cryptos acceptées (ex: ["btc","eth","ltc"])
|
||||
@@ -157,6 +158,8 @@ func (d *Database) GetSettings() (AppSettings, error) {
|
||||
settings.ReferralEnabled = value == "true"
|
||||
case "crypto_payment_enabled":
|
||||
settings.CryptoPaymentEnabled = value == "true"
|
||||
case "crypto_only":
|
||||
settings.CryptoOnly = value == "true"
|
||||
case "nowpayments_api_key":
|
||||
settings.NowPaymentsAPIKey = value
|
||||
case "nowpayments_ipn_secret":
|
||||
@@ -232,6 +235,7 @@ func (d *Database) UpdateSettings(s AppSettings) error {
|
||||
{"points_pools", string(poolsJSON)},
|
||||
{"referral_enabled", boolStr(s.ReferralEnabled)},
|
||||
{"crypto_payment_enabled", boolStr(s.CryptoPaymentEnabled)},
|
||||
{"crypto_only", boolStr(s.CryptoOnly)},
|
||||
{"nowpayments_api_key", s.NowPaymentsAPIKey},
|
||||
{"nowpayments_ipn_secret", s.NowPaymentsIPNSecret},
|
||||
{"nowpayments_currencies", string(currenciesJSON)},
|
||||
|
||||
@@ -36,6 +36,9 @@ func AlertPolice(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Notifier tous les admins/cabines en temps réel
|
||||
go database.NotifyAllAdminCabineAlert(alert.ID, usernameStr, req.Message)
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"success": true,
|
||||
"message": "Police alert created",
|
||||
|
||||
@@ -37,6 +37,7 @@ func GetPublicSettings(c *gin.Context) {
|
||||
"referral_enabled": settings.ReferralEnabled,
|
||||
"delivery_schedule": settings.DeliverySchedule,
|
||||
"crypto_payment_enabled": settings.CryptoPaymentEnabled,
|
||||
"crypto_only": settings.CryptoOnly,
|
||||
"nowpayments_currencies": settings.NowPaymentsCurrencies,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -30,6 +30,19 @@ const ALERT_PHRASES = [
|
||||
{ label: "Guet-apens", icon: "warning-outline" as const },
|
||||
];
|
||||
|
||||
const ALERT_CONFIG: Record<string, { title: string; message: string; successHint: string }> = {
|
||||
"Contrôle de police": {
|
||||
title: "Alerte — Contrôle de police",
|
||||
message: "Vous signalez un contrôle de police. Restez calme, soyez coopératif et ne résistez pas. L'administration sera immédiatement notifiée.",
|
||||
successHint: "L'administration a été alertée. Restez calme, coopérez avec les forces de l'ordre et attendez les instructions.",
|
||||
},
|
||||
"Guet-apens": {
|
||||
title: "Alerte — Guet-apens",
|
||||
message: "Vous signalez un guet-apens. Si possible, éloignez-vous de la zone immédiatement. L'administration sera immédiatement notifiée.",
|
||||
successHint: "L'administration a été alertée. Éloignez-vous du danger si possible et attendez les instructions de l'équipe.",
|
||||
},
|
||||
};
|
||||
|
||||
export default function AlertsScreen() {
|
||||
const { colors } = useTheme();
|
||||
const [alerts, setAlerts] = useState<AlertType[]>([]);
|
||||
@@ -412,17 +425,12 @@ export default function AlertsScreen() {
|
||||
</View>
|
||||
</Animated.View>
|
||||
|
||||
<Text style={styles.modalTitle}>Alerte Police</Text>
|
||||
<Text style={styles.modalTitle}>
|
||||
{ALERT_CONFIG[selectedPhrase]?.title ?? "Alerte"}
|
||||
</Text>
|
||||
<Text style={styles.modalMessage}>
|
||||
Vous êtes sur le point de déclencher une alerte
|
||||
police. Cette action notifiera immédiatement
|
||||
l'administration.
|
||||
{ALERT_CONFIG[selectedPhrase]?.message ?? "Vous êtes sur le point de déclencher une alerte. L'administration sera immédiatement notifiée."}
|
||||
</Text>
|
||||
{selectedPhrase ? (
|
||||
<Text style={[styles.modalWarning, { color: colors.danger, marginBottom: spacing.m }]}>
|
||||
{selectedPhrase}
|
||||
</Text>
|
||||
) : null}
|
||||
<Text style={styles.modalWarning}>
|
||||
Confirmez-vous le déclenchement ?
|
||||
</Text>
|
||||
@@ -480,8 +488,7 @@ export default function AlertsScreen() {
|
||||
{successMessage}
|
||||
</Text>
|
||||
<Text style={styles.successHint}>
|
||||
L'administration a été notifiée. Vous pourrez
|
||||
terminer l'alerte quand la situation sera résolue.
|
||||
{ALERT_CONFIG[selectedPhrase]?.successHint ?? "L'administration a été notifiée. Vous pourrez terminer l'alerte quand la situation sera résolue."}
|
||||
</Text>
|
||||
|
||||
<TouchableOpacity
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
package-lock.json
|
||||
node_modules/
|
||||
dist/
|
||||
|
||||
@@ -13,6 +13,7 @@ import Cart from "./pages/User/Cart";
|
||||
import Checkout from "./pages/User/Checkout";
|
||||
import OrderDetails from "./pages/User/OrderDetails";
|
||||
import Parrainage from "./pages/User/Parrainage";
|
||||
import ProfilePage from "./pages/User/ProfilePage";
|
||||
|
||||
// Pages Login
|
||||
import LoginClient from "./pages/LoginClient/Login";
|
||||
@@ -76,6 +77,10 @@ function App() {
|
||||
path="/user/parrainage"
|
||||
element={<Parrainage />}
|
||||
/>
|
||||
<Route
|
||||
path="/user/profil"
|
||||
element={<ProfilePage />}
|
||||
/>
|
||||
</Routes>
|
||||
</CartProvider>
|
||||
}
|
||||
|
||||
+170
-43
@@ -4,15 +4,21 @@
|
||||
// ✅ AuthResponse inclut access_token
|
||||
// ✅ loginUser et registerUser retournent AuthResponse
|
||||
// ✅ sessionStorage (pas localStorage)
|
||||
|
||||
const API_URL = "http://5.181.0.112/api/v1";
|
||||
const BACKEND_URL = "http://5.181.0.112";
|
||||
|
||||
const API_URL = "/api/v1";
|
||||
const BACKEND_URL = "";
|
||||
export function getMediaUrl(url: string): string {
|
||||
if (!url) return "";
|
||||
if (url.startsWith("http")) return url;
|
||||
return `${BACKEND_URL}${url}`;
|
||||
}
|
||||
|
||||
async function safeJson(response: Response) {
|
||||
const ct = response.headers.get("content-type") || "";
|
||||
if (!response.ok && !ct.includes("application/json")) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
import type {
|
||||
ConfirmReceptionResponse,
|
||||
CheckoutCartResponse,
|
||||
@@ -184,7 +190,7 @@ export const loginUser = async (
|
||||
if (!response.ok) {
|
||||
let errorMessage = "Erreur de connexion";
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
const errorData = await safeJson(response);
|
||||
errorMessage =
|
||||
errorData.error || errorData.message || errorMessage;
|
||||
} catch {
|
||||
@@ -201,7 +207,7 @@ export const loginUser = async (
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const data = await safeJson(response);
|
||||
console.log("📋 [LOGIN] Réponse:", data);
|
||||
|
||||
// ✅ Vérifier access_token
|
||||
@@ -294,7 +300,7 @@ export const changePassword = async (
|
||||
new_password: newPassword,
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
const data = await safeJson(response);
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
@@ -375,16 +381,15 @@ export const getCart = async (username: string): Promise<BasketResponse> => {
|
||||
},
|
||||
});
|
||||
|
||||
const responseData = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
message: responseData.error || "Erreur récupération",
|
||||
message: "Erreur récupération",
|
||||
panier: [],
|
||||
};
|
||||
}
|
||||
|
||||
const responseData = await safeJson(response);
|
||||
return {
|
||||
success: true,
|
||||
panier: responseData.panier || [],
|
||||
@@ -445,7 +450,7 @@ export const addToCart = async (cartItem: {
|
||||
body: JSON.stringify(cartItem),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const data = await safeJson(response);
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
@@ -502,7 +507,7 @@ export const removeFromCart = async (id: number, username: string) => {
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const data = await safeJson(response);
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
@@ -558,7 +563,7 @@ export const clearCart = async (username: string) => {
|
||||
// ✅ Gérer les erreurs HTTP avant de parser JSON
|
||||
if (!response.ok) {
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
const errorData = await safeJson(response);
|
||||
console.error("❌ [CLEAR] Erreur API:", errorData);
|
||||
return {
|
||||
success: false,
|
||||
@@ -575,7 +580,7 @@ export const clearCart = async (username: string) => {
|
||||
}
|
||||
|
||||
// ✅ Parser JSON seulement si response.ok
|
||||
const data = await response.json();
|
||||
const data = await safeJson(response);
|
||||
|
||||
console.log("✅ [CLEAR] Panier vidé:", {
|
||||
stock_released: data.stock_released,
|
||||
@@ -620,7 +625,7 @@ export const getMyOrders = async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const data = await safeJson(response);
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
@@ -651,6 +656,8 @@ export interface CheckoutData {
|
||||
last_name?: string;
|
||||
phone?: string;
|
||||
payment_method?: string;
|
||||
pay_currency?: string;
|
||||
use_referral_balance?: boolean;
|
||||
}
|
||||
|
||||
export const createCheckout = async (checkoutData: CheckoutData) => {
|
||||
@@ -685,16 +692,24 @@ export const createCheckout = async (checkoutData: CheckoutData) => {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...checkoutData,
|
||||
use_referral_balance: checkoutData.use_referral_balance ?? false,
|
||||
use_referral_balance:
|
||||
checkoutData.use_referral_balance ?? false,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const data = await safeJson(response);
|
||||
|
||||
if (!response.ok) {
|
||||
const isZoneError =
|
||||
!!data.postal_code ||
|
||||
(typeof data.error === "string" &&
|
||||
(data.error.includes("code postal") ||
|
||||
data.error.includes("hors zone")));
|
||||
return {
|
||||
success: false,
|
||||
message: data.error || "Erreur création",
|
||||
postal_code: data.postal_code as string | undefined,
|
||||
zone_error: isZoneError as boolean,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -706,6 +721,13 @@ export const createCheckout = async (checkoutData: CheckoutData) => {
|
||||
delivery_address: data.delivery_address,
|
||||
assigned_to: data.assigned_to,
|
||||
queue_info: data.queue_info,
|
||||
payment_method: data.payment_method as string | undefined,
|
||||
payment_status: data.payment_status as string | undefined,
|
||||
pay_address: data.pay_address as string | undefined,
|
||||
pay_amount: data.pay_amount as number | undefined,
|
||||
pay_currency: data.pay_currency as string | undefined,
|
||||
price_amount: data.price_amount as number | undefined,
|
||||
price_currency: data.price_currency as string | undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("❌ [CHECKOUT] Erreur:", error);
|
||||
@@ -743,7 +765,7 @@ export const approveDelivery = async (
|
||||
},
|
||||
);
|
||||
|
||||
const responseData = await response.json();
|
||||
const responseData = await safeJson(response);
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
@@ -797,7 +819,7 @@ export interface Category {
|
||||
export const getCategories = async (): Promise<Category[]> => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/categories`);
|
||||
const data = await response.json();
|
||||
const data = await safeJson(response);
|
||||
return data.categories || [];
|
||||
} catch {
|
||||
return [];
|
||||
@@ -807,7 +829,7 @@ export const getCategories = async (): Promise<Category[]> => {
|
||||
export const getAllProducts = async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/products`);
|
||||
return await response.json();
|
||||
return await safeJson(response);
|
||||
} catch (error) {
|
||||
console.error("❌ [PRODUCTS] Erreur:", error);
|
||||
return { success: false, data: [] };
|
||||
@@ -820,11 +842,10 @@ export const getAllProducts = async () => {
|
||||
*/
|
||||
export const getProductsByCategory = async (category: string) => {
|
||||
try {
|
||||
// ✅ CHANGÉ: De /products?category=X à /products/category/X
|
||||
const response = await fetch(
|
||||
`${API_URL}/products/category/${category}`,
|
||||
);
|
||||
return await response.json();
|
||||
return await safeJson(response);
|
||||
} catch (error) {
|
||||
console.error("❌ [PRODUCTS] Erreur:", error);
|
||||
return { success: false, data: [] };
|
||||
@@ -837,7 +858,7 @@ export const getProductsByCategory = async (category: string) => {
|
||||
export const getProductById = async (id: number) => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/products/${id}`);
|
||||
return await response.json();
|
||||
return await safeJson(response);
|
||||
} catch (error) {
|
||||
console.error("❌ [PRODUCT] Erreur:", error);
|
||||
return { success: false, data: null };
|
||||
@@ -874,15 +895,18 @@ export const getOrderTracking = async (
|
||||
commandId,
|
||||
);
|
||||
|
||||
const response = await fetch(`${API_URL}/commands/${commandId}/track`, {
|
||||
const response = await fetch(
|
||||
`${API_URL}/commands/${commandId}/tracking`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
const data = await safeJson(response);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("❌ [TRACKING] Erreur API:", data);
|
||||
@@ -965,7 +989,7 @@ export const getOrderETA = async (commandId: number): Promise<ETAResponse> => {
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const data = await safeJson(response);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("❌ [ETA] Erreur API:", data);
|
||||
@@ -1040,7 +1064,7 @@ export const getOrdersWithTracking = async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const data = await safeJson(response);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("❌ [ORDERS_TRACKING] Erreur API:", data);
|
||||
@@ -1112,7 +1136,7 @@ export const confirmReception = async (
|
||||
},
|
||||
);
|
||||
|
||||
const responseData = await response.json();
|
||||
const responseData = await safeJson(response);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("❌ [CONFIRM] Erreur API:", responseData);
|
||||
@@ -1165,7 +1189,7 @@ export const getOrderTotal = async (commandId: number): Promise<number> => {
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const data = await safeJson(response);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("❌ [TOTAL] Erreur API:", data);
|
||||
@@ -1237,7 +1261,7 @@ export const getMyCompletedOrders = async (): Promise<HistoryResponse> => {
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const data = await safeJson(response);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("❌ [HISTORY] Erreur API:", data);
|
||||
@@ -1366,7 +1390,7 @@ export const cancelCommand = async (
|
||||
},
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
const data = await safeJson(response);
|
||||
|
||||
// ⚠️ AVERTISSEMENT (409 Conflict) - Livreur assigné
|
||||
if (response.status === 409 && data.warning) {
|
||||
@@ -1468,7 +1492,7 @@ export const getMyCancellationHistory =
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const data = await safeJson(response);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("❌ [CANCEL_HISTORY] Erreur API:", data);
|
||||
@@ -1522,7 +1546,7 @@ export const getOrderDetails = async (commandId: number) => {
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const data = await safeJson(response);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("❌ [ORDER DETAILS] Erreur API:", data);
|
||||
@@ -1587,7 +1611,7 @@ export const getCommandItemsWithDetails = async (commandId: number) => {
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const data = await safeJson(response);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("❌ [COMMAND ITEMS] Erreur API:", data);
|
||||
@@ -1638,7 +1662,7 @@ export const getMyPenalties = async (): Promise<PenaltiesResponse> => {
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const data = await safeJson(response);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("❌ [PENALTIES] Erreur API:", data);
|
||||
@@ -1718,7 +1742,7 @@ export const checkoutCart = async (
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const data = await safeJson(response);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("❌ [CHECKOUT] Erreur API:", data);
|
||||
@@ -1795,7 +1819,7 @@ export const getClientNotifications =
|
||||
unread_count: 0,
|
||||
total: 0,
|
||||
};
|
||||
const data = await response.json();
|
||||
const data = await safeJson(response);
|
||||
return {
|
||||
success: true,
|
||||
notifications: data.notifications || [],
|
||||
@@ -1834,33 +1858,86 @@ export interface PublicSettings {
|
||||
points_enabled: boolean;
|
||||
points_separated: boolean;
|
||||
referral_enabled: boolean;
|
||||
pool_names: string[];
|
||||
crypto_payment_enabled: boolean;
|
||||
crypto_only: boolean;
|
||||
nowpayments_currencies: string[];
|
||||
}
|
||||
|
||||
export const getPublicSettings = async (): Promise<PublicSettings> => {
|
||||
const defaults: PublicSettings = { penalties_enabled: true, show_amende_score: true, points_enabled: true, points_separated: true, referral_enabled: true };
|
||||
const defaults: PublicSettings = {
|
||||
penalties_enabled: true,
|
||||
show_amende_score: true,
|
||||
points_enabled: true,
|
||||
points_separated: true,
|
||||
referral_enabled: true,
|
||||
pool_names: ["Pool 1", "Pool 2"],
|
||||
crypto_payment_enabled: false,
|
||||
crypto_only: false,
|
||||
nowpayments_currencies: [],
|
||||
};
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/app-settings`);
|
||||
if (!response.ok) return defaults;
|
||||
const data = await response.json();
|
||||
const data = await safeJson(response);
|
||||
return {
|
||||
penalties_enabled: data.penalties_enabled ?? true,
|
||||
show_amende_score: data.show_amende_score ?? true,
|
||||
points_enabled: data.points_enabled ?? true,
|
||||
points_separated: data.points_separated ?? true,
|
||||
referral_enabled: data.referral_enabled ?? true,
|
||||
pool_names:
|
||||
Array.isArray(data.pool_names) && data.pool_names.length > 0
|
||||
? data.pool_names
|
||||
: defaults.pool_names,
|
||||
crypto_payment_enabled: data.crypto_payment_enabled ?? false,
|
||||
crypto_only: data.crypto_only ?? false,
|
||||
nowpayments_currencies: Array.isArray(data.nowpayments_currencies)
|
||||
? data.nowpayments_currencies
|
||||
: [],
|
||||
};
|
||||
} catch {
|
||||
return defaults;
|
||||
}
|
||||
};
|
||||
|
||||
export interface CryptoPaymentStatus {
|
||||
command_id: number;
|
||||
payment_status: string;
|
||||
pay_address: string;
|
||||
pay_amount: number;
|
||||
pay_currency: string;
|
||||
price_amount: number;
|
||||
price_currency: string;
|
||||
}
|
||||
|
||||
export const getCryptoPaymentStatus = async (
|
||||
commandId: number,
|
||||
): Promise<CryptoPaymentStatus | null> => {
|
||||
const token = getAuthToken();
|
||||
if (!token) return null;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_URL}/commands/${commandId}/payment-status`,
|
||||
{
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
},
|
||||
);
|
||||
if (!response.ok) return null;
|
||||
return await safeJson(response);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export interface ReferralBalanceResponse {
|
||||
success: boolean;
|
||||
balance: number;
|
||||
referral_enabled?: boolean;
|
||||
}
|
||||
|
||||
export const getReferralBalance = async (): Promise<ReferralBalanceResponse> => {
|
||||
export const getReferralBalance =
|
||||
async (): Promise<ReferralBalanceResponse> => {
|
||||
const token = getAuthToken();
|
||||
if (!token) return { success: false, balance: 0 };
|
||||
try {
|
||||
@@ -1868,9 +1945,59 @@ export const getReferralBalance = async (): Promise<ReferralBalanceResponse> =>
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!response.ok) return { success: false, balance: 0 };
|
||||
const data = await response.json();
|
||||
return { success: true, balance: data.balance ?? 0, referral_enabled: data.referral_enabled };
|
||||
const data = await safeJson(response);
|
||||
return {
|
||||
success: true,
|
||||
balance: data.balance ?? 0,
|
||||
referral_enabled: data.referral_enabled,
|
||||
};
|
||||
} catch {
|
||||
return { success: false, balance: 0 };
|
||||
}
|
||||
};
|
||||
|
||||
export const getMyProfile = async (): Promise<{
|
||||
success: boolean;
|
||||
client?: {
|
||||
nom: string;
|
||||
prenom: string;
|
||||
telephone: string;
|
||||
username: string;
|
||||
};
|
||||
message?: string;
|
||||
}> => {
|
||||
const token = getAuthToken();
|
||||
if (!token) return { success: false, message: "Non authentifié" };
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/profile`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
const data = await safeJson(response);
|
||||
return data;
|
||||
} catch {
|
||||
return { success: false, message: "Erreur de connexion" };
|
||||
}
|
||||
};
|
||||
|
||||
export const updateMyProfile = async (fields: {
|
||||
nom?: string;
|
||||
prenom?: string;
|
||||
telephone?: string;
|
||||
}): Promise<{ success: boolean; message?: string }> => {
|
||||
const token = getAuthToken();
|
||||
if (!token) return { success: false, message: "Non authentifié" };
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/profile/update`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(fields),
|
||||
});
|
||||
const data = await safeJson(response);
|
||||
return data;
|
||||
} catch {
|
||||
return { success: false, message: "Erreur de connexion" };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -35,7 +35,7 @@ export interface UserResponse {
|
||||
session_id?: string;
|
||||
command?: number;
|
||||
point?: number;
|
||||
point_zipette?: number; // ✅ AJOUTER CETTE LIGNE
|
||||
pool_points?: number[];
|
||||
amende?: number;
|
||||
}
|
||||
|
||||
@@ -581,7 +581,8 @@ export interface ClientStats {
|
||||
telephone?: string;
|
||||
total_commands: number;
|
||||
points: number;
|
||||
points_zipette: number; // ✅ AJOUTER CETTE LIGNE
|
||||
pool_points: number[];
|
||||
pool_names: string[];
|
||||
penalties: number;
|
||||
}
|
||||
|
||||
@@ -709,8 +710,8 @@ export interface PenaltyInfo {
|
||||
total_penalty: number;
|
||||
cancellations_count: number;
|
||||
has_penalties: boolean;
|
||||
points?: number; // ✅ AJOUTER CETTE LIGNE (points weed/hash)
|
||||
points_zipette?: number; // ✅ AJOUTER CETTE LIGNE (points zipette)
|
||||
pool_points?: number[];
|
||||
pool_names?: string[];
|
||||
cancellation_history?: {
|
||||
current_amende: number;
|
||||
next_penalty: number;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { useCart } from "../context/CartContext";
|
||||
import { useCart } from "../context/useCart";
|
||||
import "./Navbar.css";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
@@ -14,10 +14,11 @@ import {
|
||||
faTimes,
|
||||
faBell,
|
||||
faGift,
|
||||
faUserCircle,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faTelegram } from "@fortawesome/free-brands-svg-icons";
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import { getClientNotifications, markNotificationsRead } from "../api/api";
|
||||
import { getClientNotifications, markNotificationsRead, getPublicSettings } from "../api/api";
|
||||
import type { ClientNotification } from "../api/api";
|
||||
|
||||
interface MenuItem {
|
||||
@@ -33,6 +34,7 @@ function Navbar() {
|
||||
const [notifications, setNotifications] = useState<ClientNotification[]>([]);
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
const [showNotifPanel, setShowNotifPanel] = useState(false);
|
||||
const [referralEnabled, setReferralEnabled] = useState(true);
|
||||
const seenKeysRef = useRef<Set<string>>(new Set());
|
||||
const isFirstLoadRef = useRef(true);
|
||||
const notifPanelRef = useRef<HTMLDivElement>(null);
|
||||
@@ -67,6 +69,10 @@ function Navbar() {
|
||||
return () => clearInterval(interval);
|
||||
}, [fetchNotifications]);
|
||||
|
||||
useEffect(() => {
|
||||
getPublicSettings().then((s) => setReferralEnabled(s.referral_enabled));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (notifPanelRef.current && !notifPanelRef.current.contains(e.target as Node)) {
|
||||
@@ -92,7 +98,8 @@ function Navbar() {
|
||||
{ id: "panier", label: "Mon Panier", icon: faShoppingCart, path: "/user/panier" },
|
||||
{ id: "suivi", label: "Suivi Livraison", icon: faTruck, path: "/user/suivi-livraison" },
|
||||
{ id: "historique", label: "Historique", icon: faClockRotateLeft, path: "/user/consultation-historique" },
|
||||
{ id: "parrainage", label: "Parrainage", icon: faGift, path: "/user/parrainage" },
|
||||
...(referralEnabled ? [{ id: "parrainage", label: "Parrainage", icon: faGift, path: "/user/parrainage" } as MenuItem] : []),
|
||||
{ id: "profil", label: "Mon Profil", icon: faUserCircle, path: "/user/profil" },
|
||||
];
|
||||
|
||||
const toggleMenu = () => setIsMenuOpen((v) => !v);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.product-card {
|
||||
background-color: #1a1a1a;
|
||||
border: 2px solid white;
|
||||
border: 2px solid var(--category-color, white);
|
||||
border-radius: 15px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
.product-card:hover {
|
||||
transform: scale(1.08);
|
||||
box-shadow: 0 8px 25px rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 8px 25px color-mix(in srgb, var(--category-color, white) 40%, transparent);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useCart } from "../context/CartContext";
|
||||
import { useCart } from "../context/useCart";
|
||||
import "./ProductCard.css";
|
||||
|
||||
function getTextColor(hex: string): string {
|
||||
const h = hex.replace("#", "");
|
||||
const full = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
|
||||
const r = parseInt(full.slice(0, 2), 16);
|
||||
const g = parseInt(full.slice(2, 4), 16);
|
||||
const b = parseInt(full.slice(4, 6), 16);
|
||||
return (r * 299 + g * 587 + b * 114) / 1000 > 128 ? "#000000" : "#ffffff";
|
||||
}
|
||||
|
||||
interface ProductCardProps {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -108,8 +117,13 @@ function ProductCard({
|
||||
}
|
||||
};
|
||||
|
||||
const cardColor = categoryColor || "#ffffff";
|
||||
|
||||
return (
|
||||
<div className={`product-card ${isOutOfStock ? "out-of-stock" : ""}`}>
|
||||
<div
|
||||
className={`product-card ${isOutOfStock ? "out-of-stock" : ""}`}
|
||||
style={{ "--category-color": cardColor } as React.CSSProperties}
|
||||
>
|
||||
<div className="product-card-content">
|
||||
<div className="product-image-container">
|
||||
<img
|
||||
@@ -166,7 +180,7 @@ function ProductCard({
|
||||
disabled={isOutOfStock}
|
||||
style={
|
||||
categoryColor && !isOutOfStock
|
||||
? { background: categoryColor }
|
||||
? { background: categoryColor, color: getTextColor(categoryColor) }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
useEffect,
|
||||
type ReactNode,
|
||||
@@ -55,7 +54,7 @@ interface ToastMessage {
|
||||
type: "success" | "error" | "warning" | "info";
|
||||
}
|
||||
|
||||
const CartContext = createContext<CartContextType | undefined>(undefined);
|
||||
export const CartContext = createContext<CartContextType | undefined>(undefined);
|
||||
|
||||
export function CartProvider({ children }: { children: ReactNode }) {
|
||||
const [cartItems, setCartItems] = useState<CartItem[]>([]);
|
||||
@@ -201,9 +200,10 @@ export function CartProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
const requestData = {
|
||||
username,
|
||||
product_id: item.product_id || 0,
|
||||
name_product: cleanName,
|
||||
category: category,
|
||||
quantity: Number(item.quantity), // ✨ Grammes (5, 10, 25)
|
||||
quantity: Number(item.quantity),
|
||||
price: Number(item.price) || 0,
|
||||
};
|
||||
|
||||
@@ -345,10 +345,3 @@ export function CartProvider({ children }: { children: ReactNode }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function useCart() {
|
||||
const context = useContext(CartContext);
|
||||
if (!context) {
|
||||
throw new Error("useCart must be used within a CartProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { useContext } from "react";
|
||||
import { CartContext } from "./CartContext";
|
||||
|
||||
export function useCart() {
|
||||
const context = useContext(CartContext);
|
||||
if (!context) {
|
||||
throw new Error("useCart must be used within a CartProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -1,10 +1,24 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import ProductCard from "../../components/ProductCard";
|
||||
import Navbar from "../../components/Navbar";
|
||||
import { getAllProducts, getProductsByCategory, getMediaUrl, getCategories } from "../../api/api";
|
||||
import {
|
||||
getAllProducts,
|
||||
getProductsByCategory,
|
||||
getMediaUrl,
|
||||
getCategories,
|
||||
} from "../../api/api";
|
||||
import type { Product, Category } from "../../api/api";
|
||||
import "./UserAccueil.css";
|
||||
|
||||
function getTextColor(hex: string): string {
|
||||
const h = hex.replace("#", "");
|
||||
const full = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
|
||||
const r = parseInt(full.slice(0, 2), 16);
|
||||
const g = parseInt(full.slice(2, 4), 16);
|
||||
const b = parseInt(full.slice(4, 6), 16);
|
||||
return (r * 299 + g * 587 + b * 114) / 1000 > 128 ? "#000000" : "#ffffff";
|
||||
}
|
||||
|
||||
function UserAccueil() {
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>("tous");
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
@@ -42,10 +56,7 @@ function UserAccueil() {
|
||||
if (response.success && response.data) {
|
||||
setProducts(response.data);
|
||||
} else {
|
||||
setError(
|
||||
response.message ||
|
||||
"Erreur lors du chargement des produits",
|
||||
);
|
||||
setProducts([]);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Erreur lors du chargement des produits");
|
||||
@@ -104,13 +115,29 @@ function UserAccueil() {
|
||||
return "https://via.placeholder.com/400x400/1a1a1a/ffffff?text=No+Image";
|
||||
};
|
||||
|
||||
const selectedCategoryObj = categories.find((c) => c.name === selectedCategory);
|
||||
const selectedCategoryObj = categories.find(
|
||||
(c) => c.name === selectedCategory,
|
||||
);
|
||||
const isSelectedComingSoon = selectedCategoryObj?.is_coming_soon ?? false;
|
||||
|
||||
<div className="category-header">
|
||||
<h2 className="category-title">
|
||||
{selectedCategory === "tous"
|
||||
? "Tous les produits"
|
||||
: selectedCategory}
|
||||
</h2>
|
||||
</div>;
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<div className="user-page-container">
|
||||
<div className="category-header">
|
||||
<h2 className="category-title">
|
||||
{selectedCategory === "tous"
|
||||
? "Tous les produits"
|
||||
: selectedCategory}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="category-filter">
|
||||
<button
|
||||
className={`category-button ${selectedCategory === "tous" ? "active" : ""}`}
|
||||
@@ -119,6 +146,7 @@ function UserAccueil() {
|
||||
>
|
||||
Tous
|
||||
</button>
|
||||
|
||||
{categories.map((category) => {
|
||||
const isActive = selectedCategory === category.name;
|
||||
const catColor = category.color || "#7c3aed";
|
||||
@@ -131,11 +159,13 @@ function UserAccueil() {
|
||||
? {
|
||||
backgroundColor: catColor,
|
||||
borderColor: catColor,
|
||||
color: "#ffffff",
|
||||
color: getTextColor(catColor),
|
||||
}
|
||||
: { borderColor: `${catColor}66` }
|
||||
}
|
||||
onClick={() => handleCategoryChange(category.name)}
|
||||
onClick={() =>
|
||||
handleCategoryChange(category.name)
|
||||
}
|
||||
>
|
||||
{category.name}
|
||||
</button>
|
||||
@@ -143,12 +173,6 @@ function UserAccueil() {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="category-header">
|
||||
<h2 className="category-title">
|
||||
{selectedCategory === "tous" ? "Tous les produits" : selectedCategory}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{isSelectedComingSoon ? (
|
||||
<div className="coming-soon-overlay">
|
||||
<span className="coming-soon-text">Prochainement</span>
|
||||
@@ -156,15 +180,22 @@ function UserAccueil() {
|
||||
Les produits de cette catégorie arrivent bientôt !
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{loading && (
|
||||
) : loading ? (
|
||||
<div className="loading-container">
|
||||
<p>Chargement des produits...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && products.length > 0 && (
|
||||
) : error ? (
|
||||
<div className="error-container">
|
||||
<p className="error-message">{error}</p>
|
||||
<button onClick={loadProducts}>Réessayer</button>
|
||||
</div>
|
||||
) : products.length === 0 ? (
|
||||
<div className="empty-container">
|
||||
<p>
|
||||
Il n'y a pas de produit disponible pour l'instant.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="products-grid">
|
||||
{products.map((product) => (
|
||||
<div
|
||||
@@ -184,7 +215,9 @@ function UserAccueil() {
|
||||
videoUrl={getProductVideoUrl(product)}
|
||||
categoryColor={
|
||||
categories.find(
|
||||
(c) => c.name.toLowerCase() === product.category?.toLowerCase(),
|
||||
(c) =>
|
||||
c.name.toLowerCase() ===
|
||||
product.category?.toLowerCase(),
|
||||
)?.color
|
||||
}
|
||||
/>
|
||||
@@ -192,8 +225,6 @@ function UserAccueil() {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
margin-bottom: clamp(1.5rem, 4vw, 2rem);
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
margin-top: -65px;
|
||||
}
|
||||
|
||||
.cart-header h1 {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
// ✅ Pour acheter 2× le même produit, l'ajouter 2 fois
|
||||
// ✅ Vérification continue de l'authentification
|
||||
|
||||
import { useCart } from "../../context/CartContext";
|
||||
import { useCart } from "../../context/useCart";
|
||||
import Navbar from "../../components/Navbar";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
.form-prefill-hint {
|
||||
font-size: 0.75rem;
|
||||
color: #6ee7b7;
|
||||
margin: 0.3rem 0 0;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.checkout-container {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
@@ -327,11 +334,11 @@
|
||||
background: rgba(0, 0, 0, 0.92);
|
||||
backdrop-filter: blur(10px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
animation: fadeIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
padding: 1rem;
|
||||
padding: 2rem 1rem;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
@@ -351,8 +358,6 @@
|
||||
border-radius: 20px;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 25px 70px rgba(91, 33, 182, 0.5), 0 0 120px rgba(91, 33, 182, 0.3);
|
||||
animation: slideUp 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
position: relative;
|
||||
@@ -639,13 +644,12 @@
|
||||
|
||||
/* Modal responsive compacte */
|
||||
.confirmation-modal-overlay {
|
||||
padding: 0.75rem;
|
||||
align-items: center;
|
||||
padding: 1rem 0.75rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.confirmation-modal {
|
||||
max-width: 100%;
|
||||
max-height: 80vh;
|
||||
border-radius: 12px;
|
||||
border-width: 1px;
|
||||
}
|
||||
@@ -727,11 +731,10 @@
|
||||
|
||||
/* Modal encore plus compacte sur mobile */
|
||||
.confirmation-modal-overlay {
|
||||
padding: 0.5rem;
|
||||
padding: 0.75rem 0.5rem;
|
||||
}
|
||||
|
||||
.confirmation-modal {
|
||||
max-height: 85vh;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
@@ -972,3 +975,172 @@
|
||||
.referral-switch input:checked + .referral-switch-slider::before {
|
||||
transform: translateX(22px);
|
||||
}
|
||||
|
||||
/* ============================================ */
|
||||
/* PAIEMENT - Sélecteur de méthode */
|
||||
/* ============================================ */
|
||||
.payment-method-selector {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.payment-method-btn {
|
||||
flex: 1;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 2px solid #2d2d2d;
|
||||
border-radius: 10px;
|
||||
background: #1a1a1a;
|
||||
color: #9ca3af;
|
||||
font-size: 0.95rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.payment-method-btn:hover {
|
||||
border-color: #4b5563;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.payment-method-btn.active {
|
||||
border-color: #f7931a;
|
||||
color: #f7931a;
|
||||
background: rgba(247, 147, 26, 0.08);
|
||||
}
|
||||
|
||||
/* ============================================ */
|
||||
/* CRYPTO - Sélecteur de monnaie */
|
||||
/* ============================================ */
|
||||
.crypto-currency-selector {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.crypto-currency-selector label {
|
||||
display: block;
|
||||
font-size: 0.85rem;
|
||||
color: #9ca3af;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.crypto-currency-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.crypto-currency-btn {
|
||||
padding: 0.4rem 0.9rem;
|
||||
border: 2px solid #2d2d2d;
|
||||
border-radius: 8px;
|
||||
background: #1a1a1a;
|
||||
color: #9ca3af;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.crypto-currency-btn:hover {
|
||||
border-color: #4b5563;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.crypto-currency-btn.active {
|
||||
border-color: #f7931a;
|
||||
color: #f7931a;
|
||||
background: rgba(247, 147, 26, 0.08);
|
||||
}
|
||||
|
||||
.crypto-info-hint {
|
||||
font-size: 0.78rem;
|
||||
color: #6b7280;
|
||||
margin-top: 0.6rem;
|
||||
}
|
||||
|
||||
/* ============================================ */
|
||||
/* CRYPTO - Modal de paiement */
|
||||
/* ============================================ */
|
||||
.crypto-address-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
background: #111;
|
||||
border: 1px solid #2d2d2d;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 1rem;
|
||||
margin-top: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.crypto-address {
|
||||
font-family: monospace;
|
||||
font-size: 0.78rem;
|
||||
color: #d1d5db;
|
||||
word-break: break-all;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.crypto-copy-btn {
|
||||
padding: 0.35rem 0.75rem;
|
||||
border: 1px solid #374151;
|
||||
border-radius: 6px;
|
||||
background: #1f2937;
|
||||
color: #9ca3af;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.crypto-copy-btn:hover {
|
||||
background: #374151;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.crypto-equiv {
|
||||
color: #6b7280;
|
||||
font-size: 0.85rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.crypto-status--waiting { color: #fbbf24; }
|
||||
.crypto-status--confirming { color: #60a5fa; }
|
||||
.crypto-status--confirmed,
|
||||
.crypto-status--finished { color: #34d399; }
|
||||
.crypto-status--failed,
|
||||
.crypto-status--expired { color: #f87171; }
|
||||
|
||||
.crypto-polling-info {
|
||||
text-align: center;
|
||||
color: #6b7280;
|
||||
font-size: 0.82rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.crypto-success-msg {
|
||||
text-align: center;
|
||||
color: #34d399;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.75rem;
|
||||
background: rgba(52, 211, 153, 0.08);
|
||||
border-radius: 8px;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.crypto-only-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: rgba(247, 147, 26, 0.08);
|
||||
border: 1px solid rgba(247, 147, 26, 0.3);
|
||||
border-radius: 10px;
|
||||
color: #f7931a;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useCart } from '../../context/CartContext';
|
||||
import { createCheckout, clearCart, extractUsernameFromToken, isUserAuthenticated, getReferralBalance, getPublicSettings } from '../../api/api';
|
||||
import type { CheckoutData } from '../../api/api';
|
||||
import { useCart } from '../../context/useCart';
|
||||
import { createCheckout, clearCart, extractUsernameFromToken, isUserAuthenticated, getReferralBalance, getPublicSettings, getMyProfile, getCryptoPaymentStatus } from '../../api/api';
|
||||
import type { CheckoutData, CryptoPaymentStatus } from '../../api/api';
|
||||
import Navbar from '../../components/Navbar';
|
||||
import './Checkout.css';
|
||||
|
||||
@@ -42,6 +42,10 @@ function Checkout() {
|
||||
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||
const [confirmationData, setConfirmationData] = useState<ConfirmationData | null>(null);
|
||||
|
||||
// État pour le modal zone non desservie
|
||||
const [showZoneModal, setShowZoneModal] = useState(false);
|
||||
const [zoneErrorMsg, setZoneErrorMsg] = useState('');
|
||||
|
||||
// Informations personnelles
|
||||
const [firstName, setFirstName] = useState('');
|
||||
const [lastName, setLastName] = useState('');
|
||||
@@ -55,6 +59,17 @@ function Checkout() {
|
||||
const [referralEnabled, setReferralEnabled] = useState(false);
|
||||
const [useReferral, setUseReferral] = useState(false);
|
||||
|
||||
// Crypto
|
||||
const [cryptoEnabled, setCryptoEnabled] = useState(false);
|
||||
const [cryptoOnly, setCryptoOnly] = useState(false);
|
||||
const [cryptoCurrencies, setCryptoCurrencies] = useState<string[]>([]);
|
||||
const [paymentMethod, setPaymentMethod] = useState<'especes' | 'crypto'>('especes');
|
||||
const [payCurrency, setPayCurrency] = useState('');
|
||||
const [cryptoPaymentData, setCryptoPaymentData] = useState<CryptoPaymentStatus | null>(null);
|
||||
const [showCryptoModal, setShowCryptoModal] = useState(false);
|
||||
const [cryptoPolling, setCryptoPolling] = useState(false);
|
||||
const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
// ✅ VÉRIFICATION INITIALE DE L'AUTHENTIFICATION
|
||||
useEffect(() => {
|
||||
const checkAuth = () => {
|
||||
@@ -79,7 +94,22 @@ function Checkout() {
|
||||
return () => clearInterval(authInterval);
|
||||
}, [navigate]);
|
||||
|
||||
// Charger solde parrainage si activé
|
||||
// Charger les infos par défaut depuis le profil
|
||||
useEffect(() => {
|
||||
const savedAddress = localStorage.getItem('profile_default_address');
|
||||
const savedPhone = localStorage.getItem('profile_default_phone');
|
||||
if (savedAddress) setAddress(savedAddress);
|
||||
if (savedPhone) setPhone(savedPhone);
|
||||
|
||||
// Pré-remplir nom depuis le backend
|
||||
getMyProfile().then((res) => {
|
||||
if (res.success && res.client) {
|
||||
if (res.client.nom) setLastName(res.client.nom);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Charger settings publics (parrainage + crypto)
|
||||
useEffect(() => {
|
||||
getPublicSettings().then((settings) => {
|
||||
if (settings.referral_enabled) {
|
||||
@@ -88,9 +118,25 @@ function Checkout() {
|
||||
if (res.success) setReferralBalance(res.balance);
|
||||
});
|
||||
}
|
||||
if (settings.crypto_payment_enabled && settings.nowpayments_currencies.length > 0) {
|
||||
setCryptoEnabled(true);
|
||||
setCryptoCurrencies(settings.nowpayments_currencies);
|
||||
setPayCurrency(settings.nowpayments_currencies[0]);
|
||||
if (settings.crypto_only) {
|
||||
setCryptoOnly(true);
|
||||
setPaymentMethod('crypto');
|
||||
}
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Nettoyage du polling au démontage
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (pollIntervalRef.current) clearInterval(pollIntervalRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* ✅ Récupérer le username du JWT
|
||||
*/
|
||||
@@ -135,6 +181,39 @@ function Checkout() {
|
||||
});
|
||||
};
|
||||
|
||||
const startCryptoPolling = (commandId: number) => {
|
||||
setCryptoPolling(true);
|
||||
pollIntervalRef.current = setInterval(async () => {
|
||||
const status = await getCryptoPaymentStatus(commandId);
|
||||
if (!status) return;
|
||||
setCryptoPaymentData(status);
|
||||
if (status.payment_status === 'finished' || status.payment_status === 'confirmed') {
|
||||
stopCryptoPolling();
|
||||
const username = extractUsernameFromToken();
|
||||
if (username) {
|
||||
await clearCart(username);
|
||||
await clearCartContext();
|
||||
}
|
||||
} else if (status.payment_status === 'failed' || status.payment_status === 'expired') {
|
||||
stopCryptoPolling();
|
||||
}
|
||||
}, 10000);
|
||||
};
|
||||
|
||||
const stopCryptoPolling = () => {
|
||||
setCryptoPolling(false);
|
||||
if (pollIntervalRef.current) {
|
||||
clearInterval(pollIntervalRef.current);
|
||||
pollIntervalRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCryptoModalClose = () => {
|
||||
stopCryptoPolling();
|
||||
setShowCryptoModal(false);
|
||||
navigate('/user/suivi-livraison');
|
||||
};
|
||||
|
||||
/**
|
||||
* ✅ Gérer la soumission de la commande
|
||||
*/
|
||||
@@ -174,7 +253,8 @@ function Checkout() {
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
phone,
|
||||
payment_method: 'especes',
|
||||
payment_method: paymentMethod === 'crypto' ? 'crypto' : 'especes',
|
||||
pay_currency: paymentMethod === 'crypto' ? payCurrency : undefined,
|
||||
use_referral_balance: useReferral && referralBalance > 0,
|
||||
};
|
||||
|
||||
@@ -183,6 +263,28 @@ function Checkout() {
|
||||
const response = await createCheckout(checkoutData);
|
||||
console.log('📥 Réponse checkout:', response);
|
||||
|
||||
// Paiement crypto : afficher le modal avec l'adresse wallet
|
||||
if (response.success && response.payment_method === 'crypto') {
|
||||
setCryptoPaymentData({
|
||||
command_id: response.command_id!,
|
||||
payment_status: response.payment_status!,
|
||||
pay_address: response.pay_address!,
|
||||
pay_amount: response.pay_amount!,
|
||||
pay_currency: response.pay_currency!,
|
||||
price_amount: response.price_amount!,
|
||||
price_currency: response.price_currency!,
|
||||
});
|
||||
setShowCryptoModal(true);
|
||||
startCryptoPolling(response.command_id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.success && response.zone_error) {
|
||||
setZoneErrorMsg(response.message);
|
||||
setShowZoneModal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.success && response.command_id) {
|
||||
const { command_id, assigned_to, queue_info, delivery_address } = response;
|
||||
|
||||
@@ -337,6 +439,9 @@ function Checkout() {
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
{localStorage.getItem('profile_default_address') && (
|
||||
<p className="form-prefill-hint"><i className="fas fa-map-marker-alt" /> Pré-rempli depuis votre profil — modifiez si vous êtes ailleurs</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
@@ -350,14 +455,71 @@ function Checkout() {
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
{localStorage.getItem('profile_default_phone') && (
|
||||
<p className="form-prefill-hint"><i className="fas fa-phone" /> Pré-rempli depuis votre profil</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Méthode de paiement */}
|
||||
{cryptoEnabled && (
|
||||
<div className="form-section">
|
||||
<h3>Méthode de paiement</h3>
|
||||
|
||||
{cryptoOnly ? (
|
||||
<div className="crypto-only-badge">
|
||||
<i className="fas fa-coins" /> Paiement uniquement en cryptomonnaie
|
||||
</div>
|
||||
) : (
|
||||
<div className="payment-method-selector">
|
||||
<button
|
||||
type="button"
|
||||
className={`payment-method-btn ${paymentMethod === 'especes' ? 'active' : ''}`}
|
||||
onClick={() => setPaymentMethod('especes')}
|
||||
disabled={loading}
|
||||
>
|
||||
<i className="fas fa-money-bill-wave" /> Espèces
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`payment-method-btn ${paymentMethod === 'crypto' ? 'active' : ''}`}
|
||||
onClick={() => setPaymentMethod('crypto')}
|
||||
disabled={loading}
|
||||
>
|
||||
<i className="fas fa-coins" /> Crypto
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(paymentMethod === 'crypto') && (
|
||||
<div className="crypto-currency-selector">
|
||||
<label>Cryptomonnaie</label>
|
||||
<div className="crypto-currency-grid">
|
||||
{cryptoCurrencies.map((currency) => (
|
||||
<button
|
||||
key={currency}
|
||||
type="button"
|
||||
className={`crypto-currency-btn ${payCurrency === currency ? 'active' : ''}`}
|
||||
onClick={() => setPayCurrency(currency)}
|
||||
disabled={loading}
|
||||
>
|
||||
{currency.toUpperCase()}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="crypto-info-hint">
|
||||
<i className="fas fa-info-circle" /> Vous recevrez l'adresse de paiement après validation
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Toggle parrainage */}
|
||||
{referralEnabled && referralBalance > 0 && (
|
||||
<div className="referral-toggle-box">
|
||||
<div className="referral-toggle-info">
|
||||
<span className="referral-toggle-icon">🎁</span>
|
||||
<span className="referral-toggle-icon"><i className="fas fa-gift"></i></span>
|
||||
<div>
|
||||
<p className="referral-toggle-label">Solde parrainage</p>
|
||||
<p className="referral-toggle-balance">{referralBalance.toFixed(2)} € disponible</p>
|
||||
@@ -397,6 +559,111 @@ function Checkout() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ============================================ */}
|
||||
{/* MODAL PAIEMENT CRYPTO */}
|
||||
{/* ============================================ */}
|
||||
{showCryptoModal && cryptoPaymentData && (
|
||||
<div className="confirmation-modal-overlay">
|
||||
<div className="confirmation-modal">
|
||||
<div className="confirmation-modal-header" style={{ background: 'linear-gradient(135deg, #f7931a, #c2620a)' }}>
|
||||
<i className="fas fa-coins confirmation-icon" />
|
||||
<h2>Paiement Crypto</h2>
|
||||
</div>
|
||||
<div className="confirmation-modal-body">
|
||||
<div className="confirmation-section">
|
||||
<div className="confirmation-section-title">
|
||||
<i className="fas fa-receipt icon" /> Commande #{cryptoPaymentData.command_id}
|
||||
</div>
|
||||
<div className="confirmation-detail">
|
||||
<strong>Statut :</strong>{' '}
|
||||
<span className={`crypto-status crypto-status--${cryptoPaymentData.payment_status}`}>
|
||||
{cryptoPaymentData.payment_status === 'waiting' && '⏳ En attente de paiement'}
|
||||
{cryptoPaymentData.payment_status === 'confirming' && '🔄 Confirmation en cours...'}
|
||||
{cryptoPaymentData.payment_status === 'confirmed' && '✅ Confirmé'}
|
||||
{cryptoPaymentData.payment_status === 'finished' && '✅ Paiement reçu !'}
|
||||
{cryptoPaymentData.payment_status === 'failed' && '❌ Paiement échoué'}
|
||||
{cryptoPaymentData.payment_status === 'expired' && '⌛ Expiré'}
|
||||
{!['waiting','confirming','confirmed','finished','failed','expired'].includes(cryptoPaymentData.payment_status) && cryptoPaymentData.payment_status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="confirmation-section">
|
||||
<div className="confirmation-section-title">
|
||||
<i className="fas fa-wallet icon" /> Adresse de paiement
|
||||
</div>
|
||||
<div className="crypto-address-box">
|
||||
<code className="crypto-address">{cryptoPaymentData.pay_address}</code>
|
||||
<button
|
||||
type="button"
|
||||
className="crypto-copy-btn"
|
||||
onClick={() => navigator.clipboard.writeText(cryptoPaymentData.pay_address)}
|
||||
>
|
||||
<i className="fas fa-copy" /> Copier
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="confirmation-section">
|
||||
<div className="confirmation-section-title">
|
||||
<i className="fas fa-coins icon" /> Montant à envoyer
|
||||
</div>
|
||||
<div className="confirmation-detail">
|
||||
<strong>{cryptoPaymentData.pay_amount} {cryptoPaymentData.pay_currency.toUpperCase()}</strong>
|
||||
<span className="crypto-equiv"> ≈ {cryptoPaymentData.price_amount.toFixed(2)} {cryptoPaymentData.price_currency.toUpperCase()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{cryptoPolling && (
|
||||
<div className="crypto-polling-info">
|
||||
<i className="fas fa-spinner fa-spin" /> Vérification automatique toutes les 10 secondes...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(cryptoPaymentData.payment_status === 'finished' || cryptoPaymentData.payment_status === 'confirmed') && (
|
||||
<div className="crypto-success-msg">
|
||||
<i className="fas fa-check-circle" /> Paiement confirmé ! Votre commande est en cours de traitement.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="confirmation-modal-actions">
|
||||
<button className="confirmation-button" style={{ background: '#374151' }} onClick={handleCryptoModalClose}>
|
||||
<i className="fas fa-location-arrow" /> Suivre ma commande
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ============================================ */}
|
||||
{/* MODAL ZONE NON DESSERVIE */}
|
||||
{/* ============================================ */}
|
||||
{showZoneModal && (
|
||||
<div className="confirmation-modal-overlay" onClick={() => setShowZoneModal(false)}>
|
||||
<div className="confirmation-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="confirmation-modal-header" style={{ background: 'linear-gradient(135deg, #dc2626, #991b1b)' }}>
|
||||
<i className="fas fa-map-marker-alt confirmation-icon"></i>
|
||||
<h2>Zone non desservie</h2>
|
||||
</div>
|
||||
<div className="confirmation-modal-body">
|
||||
<div className="confirmation-section">
|
||||
<div className="confirmation-detail" style={{ textAlign: 'center', padding: '1rem 0' }}>
|
||||
<p style={{ fontSize: '1rem', marginBottom: '0.75rem' }}>{zoneErrorMsg}</p>
|
||||
<p style={{ color: '#9ca3af', fontSize: '0.875rem' }}>
|
||||
Vérifiez l'adresse saisie ou contactez-nous pour connaître les zones de livraison disponibles.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="confirmation-modal-actions">
|
||||
<button className="confirmation-button" style={{ background: '#374151' }} onClick={() => setShowZoneModal(false)}>
|
||||
<i className="fas fa-arrow-left"></i> Modifier l'adresse
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ============================================ */}
|
||||
{/* MODAL DE CONFIRMATION STYLISÉ */}
|
||||
{/* ============================================ */}
|
||||
@@ -463,6 +730,17 @@ function Checkout() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Suivi */}
|
||||
<div className="confirmation-section">
|
||||
<div className="confirmation-section-title">
|
||||
<i className="fas fa-map-marker-alt icon"></i>
|
||||
Suivi de livraison
|
||||
</div>
|
||||
<div className="confirmation-detail">
|
||||
Suivez votre livraison en temps réel depuis la page Suivi
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Total */}
|
||||
<div className="confirmation-total">
|
||||
<div className="confirmation-total-label">
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
.history-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
margin-top: -65px;
|
||||
}
|
||||
|
||||
.history-title {
|
||||
@@ -195,7 +196,8 @@
|
||||
}
|
||||
|
||||
@keyframes pulse-penalty {
|
||||
0%, 100% {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(109, 40, 217, 0.7);
|
||||
}
|
||||
50% {
|
||||
@@ -450,7 +452,7 @@
|
||||
.order-id {
|
||||
color: white !important;
|
||||
font-weight: 700;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-family: "Courier New", monospace;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@@ -643,7 +645,7 @@
|
||||
}
|
||||
|
||||
.clickable-row::before {
|
||||
content: '→';
|
||||
content: "→";
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
color: #a78bfa;
|
||||
|
||||
@@ -25,7 +25,10 @@ import { Package, MapPin, User, TrendingUp } from 'lucide-react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faCannabis,
|
||||
faWind,
|
||||
faPills,
|
||||
faFlask,
|
||||
faMortarPestle,
|
||||
faStar,
|
||||
faTrophy,
|
||||
faExclamationTriangle,
|
||||
faCheckCircle,
|
||||
@@ -38,7 +41,7 @@ function ConsultationHistorique() {
|
||||
const [orders, setOrders] = useState<CompletedOrder[]>([]);
|
||||
const [clientStats, setClientStats] = useState<ClientStats | null>(null);
|
||||
const [penalties, setPenalties] = useState<PenaltyInfo | null>(null);
|
||||
const [appSettings, setAppSettings] = useState<PublicSettings>({ penalties_enabled: true, show_amende_score: true, points_enabled: true, points_separated: true, referral_enabled: true });
|
||||
const [appSettings, setAppSettings] = useState<PublicSettings>({ penalties_enabled: true, show_amende_score: true, points_enabled: true, points_separated: true, referral_enabled: true, pool_names: ['Pool 1', 'Pool 2'], crypto_payment_enabled: false, crypto_only: false, nowpayments_currencies: [] });
|
||||
const [referralBalance, setReferralBalance] = useState<number>(0);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string>('');
|
||||
@@ -98,7 +101,6 @@ function ConsultationHistorique() {
|
||||
|
||||
console.log('🔍 [DEBUG] result.client_stats:', result.client_stats);
|
||||
console.log('🔍 [DEBUG] points:', result.client_stats?.points);
|
||||
console.log('🔍 [DEBUG] points_zipette:', result.client_stats?.points_zipette);
|
||||
|
||||
setOrders(result.commands);
|
||||
setClientStats(result.client_stats || null);
|
||||
@@ -190,36 +192,48 @@ function ConsultationHistorique() {
|
||||
</div>
|
||||
|
||||
{/* Cartes points - affichées uniquement si le système de points est activé */}
|
||||
{appSettings.points_enabled && (
|
||||
appSettings.points_separated ? (
|
||||
{appSettings.points_enabled && (() => {
|
||||
const poolNames = clientStats.pool_names?.length ? clientStats.pool_names : appSettings.pool_names;
|
||||
const poolPoints = clientStats.pool_points ?? [clientStats.points];
|
||||
const poolIcons = [faCannabis, faPills, faFlask, faMortarPestle, faStar];
|
||||
const poolClasses = poolNames.map((_, i) => `points-pool-${i}`);
|
||||
const poolIconClasses = poolNames.map((_, i) => `icon-pool-${i}`);
|
||||
|
||||
if (poolNames.length <= 1) {
|
||||
return (
|
||||
<div className="stat-card2 points-total">
|
||||
<div className="stat-icon icon-total">
|
||||
<FontAwesomeIcon icon={faTrophy} size="lg" />
|
||||
</div>
|
||||
<div className="stat-content">
|
||||
<p className="stat-label">
|
||||
<FontAwesomeIcon icon={faTrophy} style={{ marginRight: '0.5rem' }} />
|
||||
Points {poolNames[0] ?? 'Points'}
|
||||
</p>
|
||||
<p className="stat-value">{poolPoints[0] || 0}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const total = poolPoints.reduce((s, v) => s + (v || 0), 0);
|
||||
return (
|
||||
<>
|
||||
<div className="stat-card2 points-weed">
|
||||
<div className="stat-icon icon-weed">
|
||||
<FontAwesomeIcon icon={faCannabis} size="lg" />
|
||||
{poolNames.map((name, i) => (
|
||||
<div key={i} className={`stat-card2 ${poolClasses[i] ?? 'points-extra'}`}>
|
||||
<div className={`stat-icon ${poolIconClasses[i] ?? 'icon-total'}`}>
|
||||
<FontAwesomeIcon icon={poolIcons[i] ?? faTrophy} size="lg" />
|
||||
</div>
|
||||
<div className="stat-content">
|
||||
<p className="stat-label">
|
||||
<FontAwesomeIcon icon={faCannabis} style={{ marginRight: '0.5rem' }} />
|
||||
Points Weed/Hash
|
||||
<FontAwesomeIcon icon={poolIcons[i] ?? faTrophy} style={{ marginRight: '0.5rem' }} />
|
||||
Points {name}
|
||||
</p>
|
||||
<p className="stat-value">{clientStats.points || 0}</p>
|
||||
<p className="stat-value">{poolPoints[i] || 0}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card2 points-zipette">
|
||||
<div className="stat-icon icon-zipette">
|
||||
<FontAwesomeIcon icon={faWind} size="lg" />
|
||||
</div>
|
||||
<div className="stat-content">
|
||||
<p className="stat-label">
|
||||
<FontAwesomeIcon icon={faWind} style={{ marginRight: '0.5rem' }} />
|
||||
Points Zipette
|
||||
</p>
|
||||
<p className="stat-value">{clientStats.points_zipette || 0}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(clientStats.points || 0) > 0 && (clientStats.points_zipette || 0) > 0 && (
|
||||
))}
|
||||
{total > 0 && poolNames.length > 1 && (
|
||||
<div className="stat-card2 points-total">
|
||||
<div className="stat-icon icon-total">
|
||||
<FontAwesomeIcon icon={faTrophy} size="lg" />
|
||||
@@ -229,28 +243,13 @@ function ConsultationHistorique() {
|
||||
<FontAwesomeIcon icon={faTrophy} style={{ marginRight: '0.5rem' }} />
|
||||
Total Points
|
||||
</p>
|
||||
<p className="stat-value">
|
||||
{(clientStats.points || 0) + (clientStats.points_zipette || 0)}
|
||||
</p>
|
||||
<p className="stat-value">{total}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="stat-card2 points-total">
|
||||
<div className="stat-icon icon-total">
|
||||
<FontAwesomeIcon icon={faTrophy} size="lg" />
|
||||
</div>
|
||||
<div className="stat-content">
|
||||
<p className="stat-label">
|
||||
<FontAwesomeIcon icon={faTrophy} style={{ marginRight: '0.5rem' }} />
|
||||
Points
|
||||
</p>
|
||||
<p className="stat-value">{clientStats.points || 0}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Carte 5: Commandes Livrées */}
|
||||
<div className="stat-card2 completed-orders">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Navbar from '../../components/Navbar';
|
||||
import { getReferralBalance, isUserAuthenticated } from '../../api/api';
|
||||
import { getReferralBalance, isUserAuthenticated, getPublicSettings } from '../../api/api';
|
||||
import './Parrainage.css';
|
||||
|
||||
const TELEGRAM_URL = 'https://t.me/';
|
||||
@@ -11,25 +11,25 @@ const steps = [
|
||||
num: 1,
|
||||
title: 'Parrainez un ami',
|
||||
desc: 'Recommandez nos services à un proche. Il doit nous contacter directement sur Telegram pour s\'inscrire.',
|
||||
icon: '👥',
|
||||
icon: 'fa-users',
|
||||
},
|
||||
{
|
||||
num: 2,
|
||||
title: 'Il passe sa 1ère commande',
|
||||
desc: 'Une fois votre ami inscrit et sa première commande validée, signalez-le nous sur Telegram.',
|
||||
icon: '✅',
|
||||
icon: 'fa-circle-check',
|
||||
},
|
||||
{
|
||||
num: 3,
|
||||
title: 'Nous créditons votre compte',
|
||||
desc: 'L\'admin vérifie et crédite manuellement votre solde de parrainage. Vous êtes notifié dès que c\'est fait.',
|
||||
icon: '💰',
|
||||
icon: 'fa-coins',
|
||||
},
|
||||
{
|
||||
num: 4,
|
||||
title: 'Utilisez votre solde',
|
||||
desc: 'Au moment du checkout, choisissez d\'utiliser votre solde ou de le cumuler pour une prochaine commande.',
|
||||
icon: '🛒',
|
||||
icon: 'fa-cart-shopping',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -43,10 +43,16 @@ export default function Parrainage() {
|
||||
navigate('/login/client', { replace: true });
|
||||
return;
|
||||
}
|
||||
getPublicSettings().then((s) => {
|
||||
if (!s.referral_enabled) {
|
||||
navigate('/user/accueil', { replace: true });
|
||||
return;
|
||||
}
|
||||
getReferralBalance().then((res) => {
|
||||
if (res.success) setBalance(res.balance);
|
||||
setLoading(false);
|
||||
});
|
||||
});
|
||||
}, [navigate]);
|
||||
|
||||
return (
|
||||
@@ -55,7 +61,7 @@ export default function Parrainage() {
|
||||
<div className="parrainage-container">
|
||||
{/* En-tête */}
|
||||
<div className="parrainage-hero">
|
||||
<div className="parrainage-hero-icon">🎁</div>
|
||||
<div className="parrainage-hero-icon"><i className="fas fa-gift"></i></div>
|
||||
<h1 className="parrainage-title">Programme de Parrainage</h1>
|
||||
<p className="parrainage-subtitle">
|
||||
Parrainez vos amis et cumulez du crédit sur votre compte
|
||||
@@ -85,7 +91,7 @@ export default function Parrainage() {
|
||||
<div className="parrainage-steps">
|
||||
{steps.map((step) => (
|
||||
<div key={step.num} className="step-card">
|
||||
<div className="step-icon">{step.icon}</div>
|
||||
<div className="step-icon"><i className={`fas ${step.icon}`}></i></div>
|
||||
<div className="step-num">Étape {step.num}</div>
|
||||
<h3 className="step-title">{step.title}</h3>
|
||||
<p className="step-desc">{step.desc}</p>
|
||||
@@ -96,7 +102,7 @@ export default function Parrainage() {
|
||||
|
||||
{/* Règle zone minimum */}
|
||||
<div className="parrainage-warning-card">
|
||||
<div className="warning-icon">⚠️</div>
|
||||
<div className="warning-icon"><i className="fas fa-triangle-exclamation"></i></div>
|
||||
<div className="warning-content">
|
||||
<h3 className="warning-title">Règle du minimum de zone</h3>
|
||||
<p className="warning-text">
|
||||
@@ -127,7 +133,7 @@ export default function Parrainage() {
|
||||
rel="noopener noreferrer"
|
||||
className="telegram-btn"
|
||||
>
|
||||
<span className="telegram-icon">✈</span>
|
||||
<i className="fab fa-telegram telegram-icon"></i>
|
||||
Contacter sur Telegram
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -291,7 +291,7 @@
|
||||
.grams-dropdown option:checked {
|
||||
background-color: var(--cat-color, #7c3aed);
|
||||
background: var(--cat-color, #7c3aed);
|
||||
color: white;
|
||||
color: var(--cat-text-color, white);
|
||||
}
|
||||
|
||||
.grams-dropdown option:focus {
|
||||
@@ -344,7 +344,7 @@
|
||||
.add-to-cart-button {
|
||||
width: 100%;
|
||||
background: var(--cat-color, #7c3aed);
|
||||
color: white;
|
||||
color: var(--cat-text-color, white);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: clamp(1.2rem, 3vw, 1.5rem);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useParams, useNavigate } from "react-router-dom";
|
||||
import { useState, useEffect } from "react";
|
||||
import { getProductById, getCategories, isUserAuthenticated } from "../../api/api";
|
||||
import type { Product } from "../../api/api";
|
||||
import { useCart } from "../../context/CartContext";
|
||||
import { useCart } from "../../context/useCart";
|
||||
import Navbar from "../../components/Navbar";
|
||||
import Toast from "../../components/Toast";
|
||||
import "./ProductDetail.css";
|
||||
@@ -208,6 +208,13 @@ function ProductDetail() {
|
||||
};
|
||||
const catColorRgb = hexToRgb(catColor);
|
||||
|
||||
// Texte contrasté (noir sur fond clair, blanc sur fond foncé)
|
||||
const h = catColor.replace("#", "");
|
||||
const r = parseInt(h.slice(0, 2), 16);
|
||||
const g = parseInt(h.slice(2, 4), 16);
|
||||
const b = parseInt(h.slice(4, 6), 16);
|
||||
const catTextColor = (r * 299 + g * 587 + b * 114) / 1000 > 128 ? "#000000" : "#ffffff";
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
@@ -224,7 +231,7 @@ function ProductDetail() {
|
||||
|
||||
<div
|
||||
className="product-detail-container"
|
||||
style={{ "--cat-color": catColor, "--cat-color-rgb": catColorRgb } as React.CSSProperties}
|
||||
style={{ "--cat-color": catColor, "--cat-color-rgb": catColorRgb, "--cat-text-color": catTextColor } as React.CSSProperties}
|
||||
>
|
||||
<button onClick={() => navigate(-1)} className="back-button">
|
||||
← Retour
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
.profile-container {
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1rem 4rem;
|
||||
}
|
||||
|
||||
.profile-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 4rem;
|
||||
}
|
||||
|
||||
.profile-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #7c3aed, #4f46e5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.6rem;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.profile-title {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
color: #f0f0f0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.profile-username {
|
||||
font-size: 0.9rem;
|
||||
color: #9ca3af;
|
||||
margin: 0.2rem 0 0;
|
||||
}
|
||||
|
||||
/* Alerts */
|
||||
.profile-alert {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.8rem 1.1rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
.profile-alert--success {
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
border: 1px solid rgba(16, 185, 129, 0.4);
|
||||
color: #6ee7b7;
|
||||
}
|
||||
.profile-alert--error {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
border: 1px solid rgba(239, 68, 68, 0.4);
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.profile-card {
|
||||
background: #1e1e2e;
|
||||
border: 1px solid #2d2d40;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
.profile-card-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #e2e8f0;
|
||||
margin: 0 0 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.profile-card-icon { color: #7c3aed; }
|
||||
.profile-card-icon--address { color: #10b981; }
|
||||
.profile-card-icon--phone { color: #3b82f6; }
|
||||
|
||||
.profile-hint {
|
||||
font-size: 0.82rem;
|
||||
color: #6b7280;
|
||||
margin: -0.5rem 0 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Fields */
|
||||
.profile-fields { display: flex; flex-direction: column; gap: 0.9rem; }
|
||||
|
||||
.profile-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.9rem;
|
||||
}
|
||||
|
||||
.profile-group { display: flex; flex-direction: column; gap: 0.35rem; }
|
||||
|
||||
.profile-group label {
|
||||
font-size: 0.82rem;
|
||||
font-weight: 500;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.profile-group input {
|
||||
background: #12121f;
|
||||
border: 1px solid #2d2d40;
|
||||
border-radius: 8px;
|
||||
padding: 0.65rem 0.9rem;
|
||||
color: #e2e8f0;
|
||||
font-size: 0.9rem;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.profile-group input:focus { border-color: #7c3aed; }
|
||||
.profile-group input::placeholder { color: #4b5563; }
|
||||
|
||||
/* Buttons */
|
||||
.profile-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1.2rem;
|
||||
padding: 0.65rem 1.3rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
border: none;
|
||||
background: #7c3aed;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.profile-btn--secondary {
|
||||
background: #1a3a5c;
|
||||
color: #60a5fa;
|
||||
border: 1px solid #2563eb44;
|
||||
}
|
||||
|
||||
.profile-btn:hover { opacity: 0.85; }
|
||||
.profile-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.profile-row { grid-template-columns: 1fr; }
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Navbar from '../../components/Navbar';
|
||||
import { isUserAuthenticated, extractUsernameFromToken, getMyProfile, updateMyProfile } from '../../api/api';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faUser, faMapMarkerAlt, faPhone, faCommentDots,
|
||||
faSave, faCheckCircle, faExclamationTriangle,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import './ProfilePage.css';
|
||||
|
||||
const STORAGE_ADDRESS = 'profile_default_address';
|
||||
const STORAGE_PHONE = 'profile_default_phone';
|
||||
const STORAGE_SIGNAL = 'profile_signal_pseudo';
|
||||
|
||||
export default function ProfilePage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Données compte (backend)
|
||||
const [nom, setNom] = useState('');
|
||||
const [prenom, setPrenom] = useState('');
|
||||
const [telephone, setTelephone] = useState('');
|
||||
const [loadingProfile, setLoadingProfile] = useState(true);
|
||||
|
||||
// Données locales (localStorage)
|
||||
const [defaultAddress, setDefaultAddress] = useState('');
|
||||
const [defaultPhone, setDefaultPhone] = useState('');
|
||||
const [signalPseudo, setSignalPseudo] = useState('');
|
||||
|
||||
// Feedback
|
||||
const [savingContact, setSavingContact] = useState(false);
|
||||
const [successMsg, setSuccessMsg] = useState('');
|
||||
const [errorMsg, setErrorMsg] = useState('');
|
||||
|
||||
const username = extractUsernameFromToken() ?? '';
|
||||
|
||||
useEffect(() => {
|
||||
if (!isUserAuthenticated()) {
|
||||
navigate('/login/client', { replace: true });
|
||||
return;
|
||||
}
|
||||
// Charger depuis localStorage
|
||||
setDefaultAddress(localStorage.getItem(STORAGE_ADDRESS) ?? '');
|
||||
setDefaultPhone(localStorage.getItem(STORAGE_PHONE) ?? '');
|
||||
setSignalPseudo(localStorage.getItem(STORAGE_SIGNAL) || username);
|
||||
|
||||
// Charger depuis backend
|
||||
getMyProfile().then((res) => {
|
||||
if (res.success && res.client) {
|
||||
setNom(res.client.nom ?? '');
|
||||
setPrenom(res.client.prenom ?? '');
|
||||
setTelephone(res.client.telephone ?? '');
|
||||
// Initialiser le téléphone par défaut si pas encore défini
|
||||
if (!localStorage.getItem(STORAGE_PHONE) && res.client.telephone) {
|
||||
setDefaultPhone(res.client.telephone);
|
||||
}
|
||||
}
|
||||
setLoadingProfile(false);
|
||||
});
|
||||
}, [navigate]);
|
||||
|
||||
const showSuccess = (msg: string) => {
|
||||
setSuccessMsg(msg);
|
||||
setErrorMsg('');
|
||||
setTimeout(() => setSuccessMsg(''), 3000);
|
||||
};
|
||||
const showError = (msg: string) => {
|
||||
setErrorMsg(msg);
|
||||
setSuccessMsg('');
|
||||
setTimeout(() => setErrorMsg(''), 4000);
|
||||
};
|
||||
|
||||
const saveLocal = () => {
|
||||
localStorage.setItem(STORAGE_ADDRESS, defaultAddress.trim());
|
||||
localStorage.setItem(STORAGE_PHONE, defaultPhone.trim());
|
||||
localStorage.setItem(STORAGE_SIGNAL, signalPseudo.trim());
|
||||
showSuccess('Informations par défaut enregistrées');
|
||||
};
|
||||
|
||||
const saveContact = async () => {
|
||||
setSavingContact(true);
|
||||
const res = await updateMyProfile({ nom: nom.trim(), prenom: prenom.trim(), telephone: telephone.trim() });
|
||||
setSavingContact(false);
|
||||
if (res.success) {
|
||||
showSuccess('Profil mis à jour');
|
||||
} else {
|
||||
showError(res.message ?? 'Erreur lors de la mise à jour');
|
||||
}
|
||||
};
|
||||
|
||||
if (loadingProfile) {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<div className="profile-container">
|
||||
<div className="profile-loading"><div className="spinner" /></div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<div className="profile-container">
|
||||
<div className="profile-header">
|
||||
<div className="profile-avatar">
|
||||
<FontAwesomeIcon icon={faUser} />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="profile-title">Mon Profil</h1>
|
||||
<p className="profile-username">@{username}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{successMsg && (
|
||||
<div className="profile-alert profile-alert--success">
|
||||
<FontAwesomeIcon icon={faCheckCircle} /> {successMsg}
|
||||
</div>
|
||||
)}
|
||||
{errorMsg && (
|
||||
<div className="profile-alert profile-alert--error">
|
||||
<FontAwesomeIcon icon={faExclamationTriangle} /> {errorMsg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Section compte */}
|
||||
<div className="profile-card">
|
||||
<h2 className="profile-card-title">
|
||||
<FontAwesomeIcon icon={faUser} className="profile-card-icon" />
|
||||
Mon compte
|
||||
</h2>
|
||||
<div className="profile-fields">
|
||||
<div className="profile-row">
|
||||
<div className="profile-group">
|
||||
<label>Prénom</label>
|
||||
<input
|
||||
type="text"
|
||||
value={prenom}
|
||||
onChange={(e) => setPrenom(e.target.value)}
|
||||
placeholder="Votre prénom"
|
||||
/>
|
||||
</div>
|
||||
<div className="profile-group">
|
||||
<label>Nom</label>
|
||||
<input
|
||||
type="text"
|
||||
value={nom}
|
||||
onChange={(e) => setNom(e.target.value)}
|
||||
placeholder="Votre nom"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="profile-group">
|
||||
<label>Téléphone (compte)</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={telephone}
|
||||
onChange={(e) => setTelephone(e.target.value)}
|
||||
placeholder="+33 6 12 34 56 78"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button className="profile-btn" onClick={saveContact} disabled={savingContact}>
|
||||
<FontAwesomeIcon icon={faSave} />
|
||||
{savingContact ? ' Enregistrement...' : ' Enregistrer le compte'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Section adresse par défaut */}
|
||||
<div className="profile-card">
|
||||
<h2 className="profile-card-title">
|
||||
<FontAwesomeIcon icon={faMapMarkerAlt} className="profile-card-icon profile-card-icon--address" />
|
||||
Adresse par défaut
|
||||
</h2>
|
||||
<p className="profile-hint">
|
||||
Sera pré-remplie dans le formulaire de commande. Vous pourrez la modifier si vous n'êtes pas à cette adresse.
|
||||
</p>
|
||||
<div className="profile-group">
|
||||
<label>Adresse</label>
|
||||
<input
|
||||
type="text"
|
||||
value={defaultAddress}
|
||||
onChange={(e) => setDefaultAddress(e.target.value)}
|
||||
placeholder="Numéro, rue, ville, code postal"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section contact commande */}
|
||||
<div className="profile-card">
|
||||
<h2 className="profile-card-title">
|
||||
<FontAwesomeIcon icon={faPhone} className="profile-card-icon profile-card-icon--phone" />
|
||||
Contact livraison
|
||||
</h2>
|
||||
<p className="profile-hint">
|
||||
Numéro utilisé par le livreur lors de la livraison. Peut être différent du numéro de votre compte.
|
||||
</p>
|
||||
<div className="profile-group">
|
||||
<label>Téléphone par défaut</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={defaultPhone}
|
||||
onChange={(e) => setDefaultPhone(e.target.value)}
|
||||
placeholder="+33 6 12 34 56 78"
|
||||
/>
|
||||
</div>
|
||||
<div className="profile-group" style={{ marginTop: '1rem' }}>
|
||||
<label>
|
||||
<FontAwesomeIcon icon={faCommentDots} style={{ marginRight: '0.4rem' }} />
|
||||
Pseudo Signal (optionnel)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={signalPseudo}
|
||||
onChange={(e) => setSignalPseudo(e.target.value)}
|
||||
placeholder="@votre.pseudo.signal"
|
||||
/>
|
||||
</div>
|
||||
<button className="profile-btn profile-btn--secondary" onClick={saveLocal}>
|
||||
<FontAwesomeIcon icon={faSave} /> Enregistrer les infos par défaut
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -31,7 +31,7 @@
|
||||
|
||||
.coming-soon-text {
|
||||
font-family: "Reach fill & Outline", sans-serif;
|
||||
font-size: clamp(1.5rem, 8vw, 4rem);
|
||||
font-size: clamp(1.5rem, 8vw, 1rem);
|
||||
font-weight: 400;
|
||||
color: #8e8fe8;
|
||||
letter-spacing: 4px;
|
||||
@@ -102,6 +102,7 @@
|
||||
|
||||
/* ===== CATEGORY HEADER ===== */
|
||||
.category-header {
|
||||
margin-top: -65px;
|
||||
margin-bottom: clamp(2rem, 5vw, 3rem);
|
||||
animation: headerFadeIn 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
@@ -232,7 +233,6 @@
|
||||
background-color: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.loading-container,
|
||||
|
||||
@@ -4,12 +4,16 @@ import react from "@vitejs/plugin-react";
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
chunkSizeWarningLimit: 1000, // Augmenter la limite à 1000 KB
|
||||
chunkSizeWarningLimit: 1000,
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "https://uber-stup.club",
|
||||
target: "https://5.181.0.112.nip.io",
|
||||
changeOrigin: true,
|
||||
},
|
||||
"/uploads": {
|
||||
target: "https://5.181.0.112.nip.io",
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user