chore: build
This commit is contained in:
Generated
-15
@@ -62,7 +62,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@@ -874,7 +873,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.1.0.tgz",
|
||||
"integrity": "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "7.1.0"
|
||||
},
|
||||
@@ -1463,7 +1461,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
||||
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@@ -1473,7 +1470,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
|
||||
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -1530,7 +1526,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.0.tgz",
|
||||
"integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.48.0",
|
||||
"@typescript-eslint/types": "8.48.0",
|
||||
@@ -1769,7 +1764,6 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -1869,7 +1863,6 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.25",
|
||||
"caniuse-lite": "^1.0.30001754",
|
||||
@@ -2108,7 +2101,6 @@
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
|
||||
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -2677,7 +2669,6 @@
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-1.13.2.tgz",
|
||||
"integrity": "sha512-CPjtWygL+f7naL+sGHoC2JQR0DG7u+9ik6WdkjjVmz2uy0kBC2l+aKfdi3ZzUR7VKSQJ6Mc/CeCN+6iVNah+ww==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@mapbox/geojson-rewind": "^0.5.0",
|
||||
"@mapbox/geojson-types": "^1.0.2",
|
||||
@@ -2887,7 +2878,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -2960,7 +2950,6 @@
|
||||
"version": "19.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -2969,7 +2958,6 @@
|
||||
"version": "19.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
||||
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -3228,7 +3216,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -3319,7 +3306,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.4.tgz",
|
||||
"integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -3460,7 +3446,6 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz",
|
||||
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
@@ -41,12 +41,14 @@ html, body, #root {
|
||||
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
top: calc(var(--topbar-h, 60px) + 10px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
z-index: 9999;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
z-index: 950;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -92,9 +92,6 @@ export const extractUsernameFromToken = (): string | null => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* ✅ Synchroniser sessionStorage avec JWT
|
||||
*/
|
||||
export const syncUsernameFromJWT = (): string | null => {
|
||||
const jwtUsername = extractUsernameFromToken();
|
||||
|
||||
@@ -700,6 +697,8 @@ export const createCheckout = async (checkoutData: CheckoutData) => {
|
||||
message: data.error || "Erreur création",
|
||||
postal_code: data.postal_code as string | undefined,
|
||||
zone_error: isZoneError as boolean,
|
||||
invalid_address: !!data.corrected_address,
|
||||
suggested_address: data.corrected_address as string | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1863,6 +1862,13 @@ export interface PublicSettings {
|
||||
shop_name: string;
|
||||
two_fa_enabled: boolean;
|
||||
contact_telegram: string;
|
||||
client_color_primary: string;
|
||||
client_color_secondary: string;
|
||||
client_color_success: string;
|
||||
client_color_danger: string;
|
||||
client_color_warning: string;
|
||||
client_title_gradient_from: string;
|
||||
client_title_gradient_to: string;
|
||||
}
|
||||
|
||||
export const getPublicSettings = async (): Promise<PublicSettings> => {
|
||||
@@ -1880,6 +1886,13 @@ export const getPublicSettings = async (): Promise<PublicSettings> => {
|
||||
shop_name: "Milieu-Nantais",
|
||||
two_fa_enabled: false,
|
||||
contact_telegram: "",
|
||||
client_color_primary: "#8b5cf6",
|
||||
client_color_secondary: "#22d3ee",
|
||||
client_color_success: "#22c55e",
|
||||
client_color_danger: "#ef4444",
|
||||
client_color_warning: "#f59e0b",
|
||||
client_title_gradient_from: "#a78bfa",
|
||||
client_title_gradient_to: "#22d3ee",
|
||||
};
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/app-settings`);
|
||||
@@ -1904,6 +1917,15 @@ export const getPublicSettings = async (): Promise<PublicSettings> => {
|
||||
shop_name: data.shop_name || "Milieu-Nantais",
|
||||
two_fa_enabled: data.two_fa_enabled ?? false,
|
||||
contact_telegram: data.contact_telegram || "",
|
||||
client_color_primary: data.client_color_primary || "#8b5cf6",
|
||||
client_color_secondary: data.client_color_secondary || "#22d3ee",
|
||||
client_color_success: data.client_color_success || "#22c55e",
|
||||
client_color_danger: data.client_color_danger || "#ef4444",
|
||||
client_color_warning: data.client_color_warning || "#f59e0b",
|
||||
client_title_gradient_from:
|
||||
data.client_title_gradient_from || "#a78bfa",
|
||||
client_title_gradient_to:
|
||||
data.client_title_gradient_to || "#22d3ee",
|
||||
};
|
||||
} catch {
|
||||
return defaults;
|
||||
@@ -2152,6 +2174,13 @@ export type RewardCategoryConfig = {
|
||||
amount: number;
|
||||
};
|
||||
|
||||
export type RewardItemConfig = {
|
||||
product_id: number;
|
||||
product_name: string;
|
||||
quantity: number;
|
||||
price: number;
|
||||
};
|
||||
|
||||
export type PointsPoolInfo = {
|
||||
key: string;
|
||||
name: string;
|
||||
@@ -2160,13 +2189,7 @@ export type PointsPoolInfo = {
|
||||
rewards_claimed: number;
|
||||
rewards_available: number;
|
||||
eligible_configs: RewardCategoryConfig[];
|
||||
};
|
||||
|
||||
export type RewardItemConfig = {
|
||||
product_id: number;
|
||||
product_name: string;
|
||||
quantity: number;
|
||||
price: number;
|
||||
eligible_reward_items: RewardItemConfig[];
|
||||
};
|
||||
|
||||
export type PointsRewardConfig = {
|
||||
@@ -2183,12 +2206,14 @@ export const getMyPointsRewards = async (): Promise<{
|
||||
reward: PointsRewardConfig | null;
|
||||
}> => {
|
||||
const token = getAuthToken();
|
||||
if (!token) return { success: false, enabled: false, pools: [], reward: null };
|
||||
if (!token)
|
||||
return { success: false, enabled: false, pools: [], reward: null };
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/points/rewards`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!response.ok) return { success: false, enabled: false, pools: [], reward: null };
|
||||
if (!response.ok)
|
||||
return { success: false, enabled: false, pools: [], reward: null };
|
||||
const data = await safeJson(response);
|
||||
return {
|
||||
success: true,
|
||||
@@ -2201,12 +2226,58 @@ export const getMyPointsRewards = async (): Promise<{
|
||||
}
|
||||
};
|
||||
|
||||
export const claimMyReward = async (poolKey: string): Promise<{
|
||||
export const submitLivreurRating = async (
|
||||
orderId: number,
|
||||
rating: number,
|
||||
comment: string,
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
const token = getAuthToken();
|
||||
if (!token) return { success: false, error: "Non authentifié" };
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/orders/${orderId}/rate`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ rating, comment }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const data = await safeJson(response);
|
||||
return { success: false, error: data.error || "Erreur" };
|
||||
}
|
||||
return { success: true };
|
||||
} catch {
|
||||
return { success: false, error: "Erreur de connexion" };
|
||||
}
|
||||
};
|
||||
|
||||
export const getOrderRatingStatus = async (
|
||||
orderId: number,
|
||||
): Promise<{ rated: boolean; rating?: number; comment?: string }> => {
|
||||
const token = getAuthToken();
|
||||
if (!token) return { rated: false };
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/orders/${orderId}/rating`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!response.ok) return { rated: false };
|
||||
const data = await safeJson(response);
|
||||
return data;
|
||||
} catch {
|
||||
return { rated: false };
|
||||
}
|
||||
};
|
||||
|
||||
export const claimMyReward = async (
|
||||
poolKey: string,
|
||||
productId?: number,
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
description?: string;
|
||||
remaining_rewards?: number;
|
||||
product_added?: boolean;
|
||||
product_name?: string;
|
||||
product_names?: string[];
|
||||
error?: string;
|
||||
}> => {
|
||||
const token = getAuthToken();
|
||||
@@ -2218,16 +2289,20 @@ export const claimMyReward = async (poolKey: string): Promise<{
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ pool_key: poolKey }),
|
||||
body: JSON.stringify({
|
||||
pool_key: poolKey,
|
||||
product_id: productId ?? 0,
|
||||
}),
|
||||
});
|
||||
const data = await safeJson(response);
|
||||
if (!response.ok) return { success: false, error: data.error || "Erreur" };
|
||||
if (!response.ok)
|
||||
return { success: false, error: data.error || "Erreur" };
|
||||
return {
|
||||
success: true,
|
||||
description: data.description,
|
||||
remaining_rewards: data.remaining_rewards,
|
||||
product_added: data.product_added,
|
||||
product_name: data.product_name,
|
||||
product_names: data.product_names,
|
||||
};
|
||||
} catch {
|
||||
return { success: false, error: "Erreur de connexion" };
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// ============================================
|
||||
// api/api_TYPES.ts - TOUTES LES INTERFACES
|
||||
// ============================================
|
||||
|
||||
export interface ApiResponse {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
@@ -91,6 +95,10 @@ export interface RegisterAdminRequest {
|
||||
role: "admin" | "cabine" | "livreur";
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 🛒 PANIER - TYPES
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Item du panier côté frontend
|
||||
*/
|
||||
|
||||
@@ -1,388 +1,388 @@
|
||||
/* ============================================
|
||||
ITEMS MODAL - PROFESSIONAL DARK THEME
|
||||
============================================ */
|
||||
|
||||
/* Overlay */
|
||||
.items-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(12px);
|
||||
padding: 1.5rem;
|
||||
animation: items-fadeIn 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(12px);
|
||||
padding: 1.5rem;
|
||||
animation: items-fadeIn 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
@keyframes items-fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Modal Content */
|
||||
.items-modal-content {
|
||||
background: linear-gradient(to bottom, var(--surface), var(--bg));
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 25px 50px -12px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(124, 58, 237, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: items-slideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background: linear-gradient(to bottom, var(--surface), var(--bg));
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 25px 50px -12px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(124, 58, 237, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: items-slideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
@keyframes items-slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(16px) scale(0.98);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(16px) scale(0.98);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.items-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem 2rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--surface-2);
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem 2rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--surface-2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.items-modal-header h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
color: var(--text);
|
||||
font-size: 1.25rem;
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
color: var(--text);
|
||||
font-size: 1.25rem;
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.items-modal-header h3 svg {
|
||||
color: #a78bfa;
|
||||
flex-shrink: 0;
|
||||
color: #a78bfa;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.items-modal-header h3 span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.command-badge {
|
||||
background: rgba(124, 58, 237, 0.15);
|
||||
color: #c4b5fd;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
border: 1px solid rgba(124, 58, 237, 0.3);
|
||||
background: rgba(124, 58, 237, 0.15);
|
||||
color: #c4b5fd;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
border: 1px solid rgba(124, 58, 237, 0.3);
|
||||
}
|
||||
|
||||
.items-modal-close {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 400;
|
||||
flex-shrink: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 400;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.items-modal-close:hover {
|
||||
background: var(--surface-2);
|
||||
color: var(--text);
|
||||
background: var(--surface-2);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Body */
|
||||
.items-modal-body {
|
||||
padding: 2rem;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
background: var(--bg);
|
||||
padding: 2rem;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.items-modal-body::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.items-modal-body::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.items-modal-body::-webkit-scrollbar-thumb {
|
||||
background: rgba(124, 58, 237, 0.3);
|
||||
border-radius: 10px;
|
||||
background: rgba(124, 58, 237, 0.3);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.items-modal-body::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(124, 58, 237, 0.5);
|
||||
background: rgba(124, 58, 237, 0.5);
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.items-modal-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem 2rem;
|
||||
color: var(--text-muted);
|
||||
gap: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem 2rem;
|
||||
color: var(--text-muted);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.items-modal-empty svg {
|
||||
color: var(--text-muted);
|
||||
opacity: 0.5;
|
||||
color: var(--text-muted);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.items-modal-empty p {
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Summary */
|
||||
.items-modal-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 1.25rem;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 1.25rem;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.summary-item:hover {
|
||||
background: var(--surface-2);
|
||||
border-color: rgba(124, 58, 237, 0.3);
|
||||
background: var(--surface-2);
|
||||
border-color: rgba(124, 58, 237, 0.3);
|
||||
}
|
||||
|
||||
.summary-item svg {
|
||||
color: #a78bfa;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: #a78bfa;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.summary-item span {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1;
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.summary-item::before {
|
||||
content: attr(data-label);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
content: attr(data-label);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
/* Items List */
|
||||
.items-modal-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* Item Card */
|
||||
.items-modal-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
gap: 1.25rem;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.items-modal-card:hover {
|
||||
background: var(--surface-2);
|
||||
border-color: rgba(124, 58, 237, 0.4);
|
||||
box-shadow: 0 8px 24px rgba(124, 58, 237, 0.15);
|
||||
background: var(--surface-2);
|
||||
border-color: rgba(124, 58, 237, 0.4);
|
||||
box-shadow: 0 8px 24px rgba(124, 58, 237, 0.15);
|
||||
}
|
||||
|
||||
/* Item Number */
|
||||
.item-number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: rgba(124, 58, 237, 0.15);
|
||||
color: #a78bfa;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid rgba(124, 58, 237, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: rgba(124, 58, 237, 0.15);
|
||||
color: #a78bfa;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid rgba(124, 58, 237, 0.2);
|
||||
}
|
||||
|
||||
/* Item Content */
|
||||
.item-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
color: var(--text);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
color: var(--text);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Item Status */
|
||||
.item-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.item-status.status-pending {
|
||||
background: rgba(251, 146, 60, 0.15);
|
||||
color: #fdba74;
|
||||
border: 1px solid rgba(251, 146, 60, 0.3);
|
||||
background: rgba(251, 146, 60, 0.15);
|
||||
color: #fdba74;
|
||||
border: 1px solid rgba(251, 146, 60, 0.3);
|
||||
}
|
||||
|
||||
.item-status.status-prepared,
|
||||
.item-status.status-packed {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: #93c5fd;
|
||||
border: 1px solid rgba(59, 130, 246, 0.3);
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: #93c5fd;
|
||||
border: 1px solid rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.item-status.status-delivered {
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
color: #6ee7b7;
|
||||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
color: #6ee7b7;
|
||||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
/* Item Details */
|
||||
.item-details {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1.5rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.item-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: var(--text);
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail-value.detail-total {
|
||||
color: #34d399;
|
||||
font-size: 1.25rem;
|
||||
color: #34d399;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
/* Item Meta */
|
||||
.item-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding-top: 0.75rem;
|
||||
margin-top: 0.75rem;
|
||||
border-top: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding-top: 0.75rem;
|
||||
margin-top: 0.75rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.item-id {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8125rem;
|
||||
font-family: "SF Mono", "Monaco", "Courier New", monospace;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8125rem;
|
||||
font-family: 'SF Mono', 'Monaco', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* Total */
|
||||
.items-modal-total {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(124, 58, 237, 0.1),
|
||||
rgba(124, 58, 237, 0.05)
|
||||
);
|
||||
border: 2px solid rgba(124, 58, 237, 0.3);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.2);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(135deg, rgba(124, 58, 237, 0.1), rgba(124, 58, 237, 0.05));
|
||||
border: 2px solid rgba(124, 58, 237, 0.3);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.2);
|
||||
}
|
||||
|
||||
.items-modal-total span {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.items-modal-total strong {
|
||||
color: #c4b5fd;
|
||||
font-size: 1.875rem;
|
||||
font-weight: 700;
|
||||
color: #c4b5fd;
|
||||
font-size: 1.875rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
@@ -391,147 +391,147 @@
|
||||
|
||||
/* Tablet */
|
||||
@media (max-width: 768px) {
|
||||
.items-modal-overlay {
|
||||
padding: 1rem;
|
||||
}
|
||||
.items-modal-overlay {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.items-modal-content {
|
||||
max-height: 92vh;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 200px;
|
||||
}
|
||||
.items-modal-content {
|
||||
max-height: 92vh;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 200px;
|
||||
}
|
||||
|
||||
.items-modal-header {
|
||||
padding: 1.25rem 1.5rem;
|
||||
}
|
||||
.items-modal-header {
|
||||
padding: 1.25rem 1.5rem;
|
||||
}
|
||||
|
||||
.items-modal-header h3 {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
.items-modal-header h3 {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.command-badge {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.command-badge {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.items-modal-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.items-modal-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.items-modal-summary {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.items-modal-summary {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
padding: 1rem;
|
||||
}
|
||||
.summary-item {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.items-modal-card {
|
||||
padding: 1.25rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
.items-modal-card {
|
||||
padding: 1.25rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.item-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.item-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.item-details {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
.item-details {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.items-modal-total {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
.items-modal-total {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.items-modal-total strong {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
.items-modal-total strong {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 480px) {
|
||||
.items-modal-overlay {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
.items-modal-overlay {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.items-modal-header {
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
.items-modal-header {
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
|
||||
.items-modal-header h3 {
|
||||
font-size: 1rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.items-modal-header h3 {
|
||||
font-size: 1rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.command-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.625rem;
|
||||
}
|
||||
.command-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.625rem;
|
||||
}
|
||||
|
||||
.items-modal-body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
.items-modal-body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.items-modal-card {
|
||||
padding: 1rem;
|
||||
gap: 0.875rem;
|
||||
}
|
||||
.items-modal-card {
|
||||
padding: 1rem;
|
||||
gap: 0.875rem;
|
||||
}
|
||||
|
||||
.item-number {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.item-number {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
.item-name {
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.detail-value {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.detail-value.detail-total {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
.detail-value.detail-total {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.items-modal-total {
|
||||
padding: 1rem;
|
||||
}
|
||||
.items-modal-total {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.items-modal-total strong {
|
||||
font-size: 1.375rem;
|
||||
}
|
||||
.items-modal-total strong {
|
||||
font-size: 1.375rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Small Mobile */
|
||||
@media (max-width: 360px) {
|
||||
.items-modal-content {
|
||||
border-radius: 8px;
|
||||
}
|
||||
.items-modal-content {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.items-modal-header h3 {
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
.items-modal-header h3 {
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.item-details {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.item-details {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Touch Devices */
|
||||
@media (hover: none) {
|
||||
.items-modal-card:hover {
|
||||
background: var(--surface);
|
||||
border-color: var(--border);
|
||||
box-shadow: none;
|
||||
}
|
||||
.items-modal-card:hover {
|
||||
background: var(--surface);
|
||||
border-color: var(--border);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.summary-item:hover {
|
||||
background: var(--surface);
|
||||
border-color: var(--border);
|
||||
}
|
||||
}
|
||||
.summary-item:hover {
|
||||
background: var(--surface);
|
||||
border-color: var(--border);
|
||||
}
|
||||
}
|
||||
@@ -2,47 +2,63 @@
|
||||
Tokens
|
||||
============================================================ */
|
||||
:root {
|
||||
--topbar-h: 60px;
|
||||
--sidebar-w: 260px;
|
||||
--bg: #09090b;
|
||||
--surface: #111115;
|
||||
--surface-2: #1c1c22;
|
||||
--border: rgba(255, 255, 255, 0.07);
|
||||
--primary: #8b5cf6;
|
||||
--primary-soft: rgba(139, 92, 246, 0.12);
|
||||
--primary-glow: rgba(139, 92, 246, 0.25);
|
||||
--cyan: #22d3ee;
|
||||
--cyan-soft: rgba(34, 211, 238, 0.1);
|
||||
--red: #ef4444;
|
||||
--text: #f4f4f5;
|
||||
--text-muted: #71717a;
|
||||
--radius: 10px;
|
||||
--transition: 0.22s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--topbar-h: 60px;
|
||||
--sidebar-w: 260px;
|
||||
--bg: #09090b;
|
||||
--surface: #111115;
|
||||
--surface-2: #1c1c22;
|
||||
--border: rgba(255, 255, 255, 0.07);
|
||||
--primary: #8b5cf6;
|
||||
--primary-soft: rgba(139, 92, 246, 0.12);
|
||||
--primary-glow: rgba(139, 92, 246, 0.25);
|
||||
--cyan: #22d3ee;
|
||||
--cyan-soft: rgba(34, 211, 238, 0.1);
|
||||
--red: #ef4444;
|
||||
--text: #f4f4f5;
|
||||
--text-muted: #71717a;
|
||||
--radius: 10px;
|
||||
--transition: 0.22s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--title-gradient-from: #a78bfa;
|
||||
--title-gradient-to: #22d3ee;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Light theme overrides
|
||||
============================================================ */
|
||||
[data-theme="light"] {
|
||||
--bg: #f4f4f8;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #ebebf2;
|
||||
--border: rgba(0, 0, 0, 0.08);
|
||||
--primary: #7c3aed;
|
||||
--bg: #f4f4f8;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #ebebf2;
|
||||
--border: rgba(0, 0, 0, 0.08);
|
||||
--primary: #7c3aed;
|
||||
--primary-soft: rgba(124, 58, 237, 0.08);
|
||||
--primary-glow: rgba(124, 58, 237, 0.18);
|
||||
--cyan: #0891b2;
|
||||
--cyan-soft: rgba(8, 145, 178, 0.1);
|
||||
--red: #dc2626;
|
||||
--text: #18181b;
|
||||
--text-muted: #52525b;
|
||||
--cyan: #0891b2;
|
||||
--cyan-soft: rgba(8, 145, 178, 0.1);
|
||||
--red: #dc2626;
|
||||
--text: #18181b;
|
||||
--text-muted: #52525b;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Light mode overrides — header / sidebar footer forcés en blanc
|
||||
============================================================ */
|
||||
[data-theme="light"] .topbar {
|
||||
background: #ffffff;
|
||||
border-bottom-color: var(--border);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Body offset
|
||||
============================================================ */
|
||||
:root {
|
||||
--bottomnav-h: 66px;
|
||||
--bottomnav-offset: 15px;
|
||||
}
|
||||
|
||||
body {
|
||||
padding-top: var(--topbar-h);
|
||||
padding-bottom: calc(var(--bottomnav-h) + var(--bottomnav-offset) + 12px);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
@@ -59,10 +75,10 @@ body {
|
||||
align-items: center;
|
||||
padding: 0 0.75rem;
|
||||
gap: 0.5rem;
|
||||
background: color-mix(in srgb, var(--bg) 92%, transparent);
|
||||
background: color-mix(in srgb, var(--cyan) 18%, var(--bg));
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--cyan) 30%, transparent);
|
||||
box-shadow: 0 1px 24px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
@@ -96,7 +112,11 @@ body {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
background: linear-gradient(135deg, #a78bfa, #7c3aed);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--title-gradient-from),
|
||||
var(--title-gradient-to)
|
||||
);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
@@ -186,260 +206,146 @@ body {
|
||||
/* ============================================================
|
||||
Overlay
|
||||
============================================================ */
|
||||
.sidebar-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 950;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(2px);
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Sidebar
|
||||
Bottom nav — floating pill
|
||||
============================================================ */
|
||||
.sidebar {
|
||||
.bottom-nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: var(--sidebar-w);
|
||||
height: 100dvh;
|
||||
z-index: 1000;
|
||||
bottom: var(--bottomnav-offset);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: calc(100% - 30px);
|
||||
max-width: 640px;
|
||||
z-index: 900;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg);
|
||||
border-right: 1px solid var(--border);
|
||||
transform: translateX(-100%);
|
||||
transition: transform var(--transition);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar.open {
|
||||
transform: translateX(0);
|
||||
box-shadow: 4px 0 40px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
/* ── Header ── */
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1.25rem 1rem 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.sidebar-brand-icon {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 10px;
|
||||
background: var(--primary-soft);
|
||||
border: 1px solid rgba(139, 92, 246, 0.25);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--primary);
|
||||
font-size: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-brand-name {
|
||||
margin: 0;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.sidebar-brand-sub {
|
||||
margin: 0;
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.sidebar-close {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-close:hover {
|
||||
background: var(--surface-2);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ── Nav ── */
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.75rem 0.75rem;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.sidebar-nav::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
background: color-mix(in srgb, var(--primary) 30%, rgba(6, 6, 18, 0.88));
|
||||
backdrop-filter: blur(50px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(50px) saturate(180%);
|
||||
border-radius: 22px;
|
||||
padding: 6px;
|
||||
gap: 2px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow:
|
||||
0 8px 40px rgba(0, 0, 0, 0.45),
|
||||
0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.85rem;
|
||||
padding: 0.7rem 0.9rem;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: all var(--transition);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
position: relative;
|
||||
[data-theme="light"] .bottom-nav {
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--primary) 14%,
|
||||
rgba(245, 242, 255, 0.94)
|
||||
);
|
||||
border-color: rgba(0, 0, 0, 0.06);
|
||||
box-shadow:
|
||||
0 8px 40px rgba(0, 0, 0, 0.12),
|
||||
0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.menu-item:hover:not(:disabled) {
|
||||
background: var(--surface);
|
||||
border-color: var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.menu-item.active {
|
||||
background: var(--primary-soft);
|
||||
border-color: rgba(139, 92, 246, 0.2);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.menu-item:active:not(:disabled) {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.menu-item:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
background: var(--surface);
|
||||
font-size: 0.85rem;
|
||||
flex-shrink: 0;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.menu-item.active .menu-icon {
|
||||
background: rgba(139, 92, 246, 0.2);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
/* ── Tabs ── */
|
||||
.bottom-tab {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.menu-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary);
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 0 8px var(--primary-glow);
|
||||
}
|
||||
|
||||
/* ── Footer ── */
|
||||
.sidebar-footer {
|
||||
padding: 0.75rem;
|
||||
border-top: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 3px;
|
||||
padding: 8px 4px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
cursor: pointer;
|
||||
transition: all 0.18s ease;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sidebar-footer-btn {
|
||||
width: 100%;
|
||||
[data-theme="light"] .bottom-tab {
|
||||
color: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.bottom-tab:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
[data-theme="light"] .bottom-tab:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.bottom-tab.active {
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
[data-theme="light"] .bottom-tab.active {
|
||||
background: rgba(255, 255, 255, 0.65);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.bottom-tab:active {
|
||||
transform: scale(0.93);
|
||||
}
|
||||
|
||||
/* ── Icon wrapper ── */
|
||||
.bottom-tab-icon-wrap {
|
||||
position: relative;
|
||||
font-size: 1.05rem;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.7rem 1rem;
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
/* ── Cart badge ── */
|
||||
.bottom-tab-badge {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
right: -8px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 999px;
|
||||
background: var(--red);
|
||||
color: #fff;
|
||||
font-size: 0.58rem;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 3px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* ── Labels ── */
|
||||
.bottom-tab-label {
|
||||
font-size: 0.6rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
letter-spacing: 0.01em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.sidebar-footer-btn:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
/* Hide labels on very narrow screens */
|
||||
@media (max-width: 380px) {
|
||||
.bottom-tab-label {
|
||||
display: none;
|
||||
}
|
||||
.bottom-tab {
|
||||
padding: 10px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-footer-btn.telegram {
|
||||
background: var(--cyan-soft);
|
||||
border: 1px solid rgba(34, 211, 238, 0.2);
|
||||
color: var(--cyan);
|
||||
/* Logout button — couleur rouge subtile */
|
||||
.topbar-logout-btn {
|
||||
color: rgba(239, 68, 68, 0.7);
|
||||
}
|
||||
|
||||
.sidebar-footer-btn.telegram:hover:not(:disabled) {
|
||||
background: rgba(34, 211, 238, 0.15);
|
||||
border-color: rgba(34, 211, 238, 0.35);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 16px rgba(34, 211, 238, 0.15);
|
||||
}
|
||||
|
||||
.sidebar-footer-btn.logout {
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
border: 1px solid rgba(239, 68, 68, 0.18);
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.sidebar-footer-btn.logout:hover:not(:disabled) {
|
||||
background: rgba(239, 68, 68, 0.13);
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.sidebar-footer-btn:active:not(:disabled) {
|
||||
transform: scale(0.97);
|
||||
.topbar-logout-btn:hover {
|
||||
color: var(--red) !important;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
@@ -473,8 +379,12 @@ body {
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { transform: translateY(100%); }
|
||||
to { transform: translateY(0); }
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.notif-modal-header {
|
||||
@@ -519,8 +429,13 @@ body {
|
||||
scrollbar-color: var(--border) transparent;
|
||||
}
|
||||
|
||||
.notif-modal-body::-webkit-scrollbar { width: 4px; }
|
||||
.notif-modal-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
||||
.notif-modal-body::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.notif-modal-body::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.notif-empty {
|
||||
display: flex;
|
||||
@@ -534,7 +449,9 @@ body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.notif-empty p { margin: 0; }
|
||||
.notif-empty p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.notif-item {
|
||||
display: flex;
|
||||
@@ -546,7 +463,9 @@ body {
|
||||
transition: background var(--transition);
|
||||
}
|
||||
|
||||
.notif-item:last-child { border-bottom: none; }
|
||||
.notif-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.notif-unread {
|
||||
background: var(--primary-soft);
|
||||
@@ -569,11 +488,33 @@ body {
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Responsive
|
||||
Safe-area utilitaire — à appliquer au conteneur scrollable
|
||||
d'une page (ou directement à son bouton d'action principal)
|
||||
pour garantir qu'aucun contenu ne se cache sous la bottom-nav
|
||||
flottante (ex: bouton "Commander" du panier, "Valider" d'un
|
||||
formulaire, etc.). Hauteur de la pilule + son offset + une
|
||||
marge de confort.
|
||||
============================================================ */
|
||||
.page-bottomnav-safe-area {
|
||||
padding-bottom: calc(
|
||||
var(--bottomnav-h) + var(--bottomnav-offset) + 16px
|
||||
) !important;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Responsive — touch devices
|
||||
============================================================ */
|
||||
@media (hover: none) {
|
||||
.menu-item:hover { background: transparent; border-color: transparent; color: var(--text-muted); }
|
||||
.menu-item.active:hover { background: var(--primary-soft); color: var(--primary); }
|
||||
.topbar-toggle:hover { background: transparent; border-color: transparent; }
|
||||
.topbar-icon-btn:hover { background: transparent; border-color: transparent; }
|
||||
.bottom-tab:hover {
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
.bottom-tab.active:hover {
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
color: #fff;
|
||||
}
|
||||
.topbar-icon-btn:hover {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
faTruck,
|
||||
faClockRotateLeft,
|
||||
faSignOutAlt,
|
||||
faBars,
|
||||
faTimes,
|
||||
faBell,
|
||||
faGift,
|
||||
@@ -19,7 +18,6 @@ import {
|
||||
faSun,
|
||||
faMoon,
|
||||
} 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, getPublicSettings } from "../api/api";
|
||||
import type { ClientNotification } from "../api/api";
|
||||
@@ -49,7 +47,6 @@ function formatNotifDate(dateStr: string): string {
|
||||
|
||||
function Navbar() {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false);
|
||||
const [isLoggingOut, setIsLoggingOut] = useState<boolean>(false);
|
||||
const [notifications, setNotifications] = useState<ClientNotification[]>([]);
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
@@ -71,9 +68,7 @@ function Navbar() {
|
||||
for (const n of res.notifications) {
|
||||
if (n.read) continue;
|
||||
const key = `${n.command_id}-${n.type}-${n.created_at}`;
|
||||
if (!seenKeysRef.current.has(key)) {
|
||||
seenKeysRef.current.add(key);
|
||||
}
|
||||
if (!seenKeysRef.current.has(key)) seenKeysRef.current.add(key);
|
||||
}
|
||||
} else {
|
||||
for (const n of res.notifications) {
|
||||
@@ -105,26 +100,6 @@ function Navbar() {
|
||||
}
|
||||
};
|
||||
|
||||
const closeNotifPanel = () => setShowNotifPanel(false);
|
||||
|
||||
const menuItems: MenuItem[] = [
|
||||
{ id: "accueil", label: "Accueil", icon: faHome, path: "/user/accueil" },
|
||||
{ id: "produits", label: "Nos Produits", icon: faBox, path: "/user/nos-produits" },
|
||||
{ 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" },
|
||||
...(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);
|
||||
const closeMenu = () => setIsMenuOpen(false);
|
||||
|
||||
const handleNavigation = (path: string) => {
|
||||
navigate(path);
|
||||
closeMenu();
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
if (isLoggingOut) return;
|
||||
setIsLoggingOut(true);
|
||||
@@ -134,10 +109,7 @@ function Navbar() {
|
||||
try {
|
||||
await fetch("/api/v2/admin/auth/logout", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
||||
});
|
||||
} catch { /* noop */ }
|
||||
}
|
||||
@@ -153,154 +125,102 @@ function Navbar() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleTelegram = () => window.open("https://t.me/milieu_nantais", "_blank");
|
||||
const navItems: MenuItem[] = [
|
||||
{ id: "accueil", label: "Accueil", icon: faHome, path: "/user/accueil" },
|
||||
{ id: "produits", label: "Produits", icon: faBox, path: "/user/nos-produits" },
|
||||
{ id: "panier", label: "Panier", icon: faShoppingCart, path: "/user/panier" },
|
||||
{ id: "suivi", label: "Suivi", icon: faTruck, path: "/user/suivi-livraison" },
|
||||
];
|
||||
|
||||
const accountItems: MenuItem[] = [
|
||||
{ id: "historique", label: "Historique", icon: faClockRotateLeft, path: "/user/consultation-historique" },
|
||||
...(referralEnabled ? [{ id: "parrainage", label: "Parrain.", icon: faGift, path: "/user/parrainage" } as MenuItem] : []),
|
||||
{ id: "profil", label: "Profil", icon: faUserCircle, path: "/user/profil" },
|
||||
];
|
||||
|
||||
const allTabs = [...navItems, ...accountItems];
|
||||
|
||||
const renderTab = (item: MenuItem) => {
|
||||
const isActive = location.pathname === item.path;
|
||||
const showBadge = item.id === "panier" && cartCount > 0;
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
className={`bottom-tab ${isActive ? "active" : ""}`}
|
||||
onClick={() => navigate(item.path)}
|
||||
aria-label={item.label}
|
||||
>
|
||||
<span className="bottom-tab-icon-wrap">
|
||||
<FontAwesomeIcon icon={item.icon} />
|
||||
{showBadge && (
|
||||
<span className="bottom-tab-badge">
|
||||
{cartCount > 99 ? "99+" : cartCount}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="bottom-tab-label">{item.label}</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* ── Top Bar ─────────────────────────────────── */}
|
||||
{/* ── Top Bar ── */}
|
||||
<header className="topbar">
|
||||
<button
|
||||
className={`topbar-toggle ${isMenuOpen ? "is-open" : ""}`}
|
||||
onClick={toggleMenu}
|
||||
aria-label="Menu"
|
||||
>
|
||||
<FontAwesomeIcon icon={isMenuOpen ? faTimes : faBars} />
|
||||
</button>
|
||||
|
||||
<span className="topbar-brand">{shopName}</span>
|
||||
|
||||
<div className="topbar-actions">
|
||||
{/* Theme toggle */}
|
||||
<button
|
||||
className="topbar-theme-btn"
|
||||
onClick={toggleTheme}
|
||||
aria-label={theme === "dark" ? "Passer en mode clair" : "Passer en mode sombre"}
|
||||
>
|
||||
<button className="topbar-theme-btn" onClick={toggleTheme}
|
||||
aria-label={theme === "dark" ? "Mode clair" : "Mode sombre"}>
|
||||
<FontAwesomeIcon icon={theme === "dark" ? faSun : faMoon} />
|
||||
</button>
|
||||
|
||||
{/* Notifications */}
|
||||
<button
|
||||
className="topbar-icon-btn"
|
||||
onClick={handleNotifBellClick}
|
||||
aria-label="Notifications"
|
||||
>
|
||||
<button className="topbar-icon-btn" onClick={handleNotifBellClick} aria-label="Notifications">
|
||||
<FontAwesomeIcon icon={faBell} />
|
||||
{unreadCount > 0 && (
|
||||
<span className="topbar-badge">
|
||||
{unreadCount > 99 ? "99+" : unreadCount}
|
||||
</span>
|
||||
<span className="topbar-badge">{unreadCount > 99 ? "99+" : unreadCount}</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Panier */}
|
||||
<button
|
||||
className="topbar-icon-btn"
|
||||
onClick={() => navigate("/user/panier")}
|
||||
aria-label="Panier"
|
||||
className="topbar-icon-btn topbar-logout-btn"
|
||||
onClick={handleLogout}
|
||||
disabled={isLoggingOut}
|
||||
aria-label="Se déconnecter"
|
||||
>
|
||||
<FontAwesomeIcon icon={faShoppingCart} />
|
||||
{cartCount > 0 && (
|
||||
<span className="topbar-badge">{cartCount}</span>
|
||||
)}
|
||||
<FontAwesomeIcon icon={faSignOutAlt} />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* ── Notifications modal ─────────────────────── */}
|
||||
{/* ── Notifications ── */}
|
||||
{showNotifPanel && (
|
||||
<div className="notif-modal-overlay" onClick={closeNotifPanel}>
|
||||
<div className="notif-modal-overlay" onClick={() => setShowNotifPanel(false)}>
|
||||
<div className="notif-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="notif-modal-header">
|
||||
<span className="notif-modal-title">Notifications</span>
|
||||
<button className="notif-modal-close" onClick={closeNotifPanel} aria-label="Fermer">
|
||||
<button className="notif-modal-close" onClick={() => setShowNotifPanel(false)}>
|
||||
<FontAwesomeIcon icon={faTimes} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="notif-modal-body">
|
||||
{notifications.length === 0 ? (
|
||||
<div className="notif-empty">
|
||||
<FontAwesomeIcon icon={faBell} style={{ fontSize: "2rem", marginBottom: "0.75rem", opacity: 0.3 }} />
|
||||
<FontAwesomeIcon icon={faBell} style={{ fontSize: "2rem", opacity: 0.3 }} />
|
||||
<p>Aucune notification</p>
|
||||
</div>
|
||||
) : (
|
||||
notifications.map((n, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`notif-item ${n.read ? "notif-read" : "notif-unread"}`}
|
||||
>
|
||||
<span className="notif-message">{n.message}</span>
|
||||
<span className="notif-time">{formatNotifDate(n.created_at)}</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
) : notifications.map((n, i) => (
|
||||
<div key={i} className={`notif-item ${n.read ? "notif-read" : "notif-unread"}`}>
|
||||
<span className="notif-message">{n.message}</span>
|
||||
<span className="notif-time">{formatNotifDate(n.created_at)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Overlay ─────────────────────────────────── */}
|
||||
{isMenuOpen && <div className="sidebar-overlay" onClick={closeMenu} />}
|
||||
|
||||
{/* ── Sidebar ─────────────────────────────────── */}
|
||||
<aside className={`sidebar ${isMenuOpen ? "open" : ""}`}>
|
||||
<div className="sidebar-header">
|
||||
<div className="sidebar-brand">
|
||||
<div className="sidebar-brand-icon">
|
||||
<FontAwesomeIcon icon={faShoppingCart} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="sidebar-brand-name">{shopName}</p>
|
||||
<p className="sidebar-brand-sub">Mon espace</p>
|
||||
</div>
|
||||
</div>
|
||||
<button className="sidebar-close" onClick={closeMenu} aria-label="Fermer">
|
||||
<FontAwesomeIcon icon={faTimes} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav className="sidebar-nav">
|
||||
<ul className="menu-list">
|
||||
{menuItems.map((item) => {
|
||||
const isActive = location.pathname === item.path;
|
||||
return (
|
||||
<li key={item.id}>
|
||||
<button
|
||||
className={`menu-item ${isActive ? "active" : ""}`}
|
||||
onClick={() => handleNavigation(item.path)}
|
||||
disabled={isLoggingOut}
|
||||
>
|
||||
<span className="menu-icon">
|
||||
<FontAwesomeIcon icon={item.icon} />
|
||||
</span>
|
||||
<span className="menu-label">{item.label}</span>
|
||||
{isActive && <span className="menu-dot" />}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div className="sidebar-footer">
|
||||
<button
|
||||
className="sidebar-footer-btn telegram"
|
||||
onClick={handleTelegram}
|
||||
disabled={isLoggingOut}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTelegram} />
|
||||
<span>Telegram</span>
|
||||
</button>
|
||||
<button
|
||||
className="sidebar-footer-btn logout"
|
||||
onClick={handleLogout}
|
||||
disabled={isLoggingOut}
|
||||
>
|
||||
<FontAwesomeIcon icon={faSignOutAlt} />
|
||||
<span>{isLoggingOut ? "Déconnexion…" : "Déconnexion"}</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
{/* ── Bottom nav ── */}
|
||||
<nav className="bottom-nav">
|
||||
{allTabs.map(renderTab)}
|
||||
</nav>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,7 +57,8 @@ function ProductCard({
|
||||
null,
|
||||
);
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
const [showVideo, setShowVideo] = useState(false); // ✨ État pour afficher/masquer la vidéo
|
||||
const [showVideo, setShowVideo] = useState(false);
|
||||
const [stockWarning, setStockWarning] = useState<{ wanted: number; available: number } | null>(null);
|
||||
|
||||
const isOutOfStock = stock === 0;
|
||||
const isComingSoon = coming_soon === true;
|
||||
@@ -100,16 +101,13 @@ function ProductCard({
|
||||
const selectedGrams = Number(e.target.value);
|
||||
setSelectedQuantity(selectedGrams);
|
||||
|
||||
if (stock > 0 && selectedGrams > stock) {
|
||||
setStockWarning({ wanted: selectedGrams, available: stock });
|
||||
return;
|
||||
}
|
||||
|
||||
const priceOption = prices?.find((p) => p.quantity === selectedGrams);
|
||||
if (priceOption) {
|
||||
console.log("💾 [ProductCard] Ajout au panier:", {
|
||||
id,
|
||||
name,
|
||||
category: normalizedCategory,
|
||||
quantity: selectedGrams,
|
||||
price: priceOption.price,
|
||||
});
|
||||
|
||||
addToCart({
|
||||
product_id: id,
|
||||
name_product: name,
|
||||
@@ -237,6 +235,49 @@ function ProductCard({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modal stock insuffisant */}
|
||||
{stockWarning &&
|
||||
createPortal(
|
||||
<div className="video-modal" onClick={() => setStockWarning(null)}>
|
||||
<div
|
||||
className="video-modal-content"
|
||||
style={{ maxWidth: 340, padding: "2rem", textAlign: "center" }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
className="video-close-btn"
|
||||
onClick={() => setStockWarning(null)}
|
||||
aria-label="Fermer"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
<div style={{ fontSize: "2.5rem", marginBottom: "0.75rem" }}>⚠️</div>
|
||||
<p style={{ fontWeight: 700, fontSize: "1.1rem", marginBottom: "0.5rem" }}>
|
||||
Stock insuffisant
|
||||
</p>
|
||||
<p style={{ color: "#aaa", fontSize: "0.95rem", marginBottom: "1.25rem" }}>
|
||||
Vous avez sélectionné <strong>{stockWarning.wanted}g</strong> mais il ne reste que{" "}
|
||||
<strong style={{ color: "#ef4444" }}>{stockWarning.available}g</strong> disponible pour <em>{name}</em>.
|
||||
</p>
|
||||
<button
|
||||
style={{
|
||||
background: "#ef4444",
|
||||
color: "#fff",
|
||||
border: "none",
|
||||
borderRadius: 8,
|
||||
padding: "0.6rem 1.5rem",
|
||||
fontWeight: 700,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => setStockWarning(null)}
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
|
||||
{/* ✨ Modal vidéo — rendu via Portal pour éviter le clipping du transform:scale sur .product-card */}
|
||||
{showVideo &&
|
||||
videoUrl &&
|
||||
|
||||
@@ -1,132 +1,108 @@
|
||||
/* ===== TOAST PROFESSIONAL STYLES ===== */
|
||||
/* ===== TOAST MINIMAL STYLES ===== */
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 28px;
|
||||
right: 28px;
|
||||
min-width: 360px;
|
||||
max-width: 450px;
|
||||
padding: 14px 18px;
|
||||
border-radius: 12px;
|
||||
backdrop-filter: blur(40px);
|
||||
-webkit-backdrop-filter: blur(40px);
|
||||
top: calc(var(--topbar-h, 60px) + 10px);
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
width: min(340px, calc(100vw - 32px));
|
||||
padding: 9px 14px;
|
||||
border-radius: 10px;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
box-shadow:
|
||||
0 10px 40px rgba(0, 0, 0, 0.2),
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.1);
|
||||
animation: toastSlideIn 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
z-index: 9999;
|
||||
border-left-width: 3px;
|
||||
background: var(--surface, #111115);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
|
||||
animation: toastSlideDown 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
z-index: 950;
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
@keyframes toastSlideIn {
|
||||
@keyframes toastSlideDown {
|
||||
from {
|
||||
transform: translateX(480px);
|
||||
transform: translate(-50%, -12px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
transform: translate(-50%, 0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* SUCCESS - VIOLET */
|
||||
.toast-success {
|
||||
background: linear-gradient(135deg, rgba(124, 58, 237, 0.1) 0%, rgba(124, 58, 237, 0.05) 100%);
|
||||
border-color: rgba(124, 58, 237, 0.3);
|
||||
border-left-color: #7c3aed;
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.toast-success .toast-icon {
|
||||
color: #7c3aed;
|
||||
background: rgba(124, 58, 237, 0.15);
|
||||
border: 1px solid rgba(124, 58, 237, 0.4);
|
||||
}
|
||||
|
||||
.toast-success .toast-progress {
|
||||
background: linear-gradient(90deg, #7c3aed, #a78bfa, transparent);
|
||||
background: #7c3aed;
|
||||
}
|
||||
|
||||
/* ERROR */
|
||||
.toast-error {
|
||||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.08) 0%, rgba(220, 38, 38, 0.04) 100%);
|
||||
border-color: rgba(239, 68, 68, 0.2);
|
||||
border-left-color: #dc2626;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.toast-error .toast-icon {
|
||||
color: #dc2626;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid rgba(239, 68, 68, 0.25);
|
||||
}
|
||||
|
||||
.toast-error .toast-progress {
|
||||
background: linear-gradient(90deg, #dc2626, #b91c1c, transparent);
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
/* WARNING */
|
||||
.toast-warning {
|
||||
background: linear-gradient(135deg, rgba(245, 158, 11, 0.08) 0%, rgba(217, 119, 6, 0.04) 100%);
|
||||
border-color: rgba(245, 158, 11, 0.2);
|
||||
border-left-color: #d97706;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.toast-warning .toast-icon {
|
||||
color: #d97706;
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
border: 1px solid rgba(245, 158, 11, 0.25);
|
||||
}
|
||||
|
||||
.toast-warning .toast-progress {
|
||||
background: linear-gradient(90deg, #d97706, #b45309, transparent);
|
||||
background: #d97706;
|
||||
}
|
||||
|
||||
/* INFO */
|
||||
.toast-info {
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.08) 0%, rgba(37, 99, 235, 0.04) 100%);
|
||||
border-color: rgba(59, 130, 246, 0.2);
|
||||
border-left-color: #2563eb;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.toast-info .toast-icon {
|
||||
color: #2563eb;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border: 1px solid rgba(59, 130, 246, 0.25);
|
||||
}
|
||||
|
||||
.toast-info .toast-progress {
|
||||
background: linear-gradient(90deg, #2563eb, #1d4ed8, transparent);
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.toast-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.toast-icon {
|
||||
font-size: 18px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
min-width: 32px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
.toast:hover .toast-icon {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
font-size: 14px;
|
||||
font-size: 12.5px;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
flex: 1;
|
||||
line-height: 1.4;
|
||||
flex: 1 1 auto;
|
||||
letter-spacing: 0.1px;
|
||||
color: var(--text);
|
||||
}
|
||||
@@ -135,7 +111,8 @@
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 2px;
|
||||
height: 1.5px;
|
||||
opacity: 0.6;
|
||||
animation: toastProgressBar linear forwards;
|
||||
}
|
||||
|
||||
@@ -148,23 +125,3 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 640px) {
|
||||
.toast {
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
left: 20px;
|
||||
min-width: auto;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
.toast {
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.toast:hover .toast-icon {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
import { createContext, useContext, useEffect, useRef, useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { getPublicSettings } from "../api/api";
|
||||
|
||||
type Theme = "dark" | "light";
|
||||
|
||||
@@ -13,18 +14,103 @@ const ThemeContext = createContext<ThemeContextValue>({
|
||||
toggleTheme: () => {},
|
||||
});
|
||||
|
||||
const LS_COLORS_KEY = "client_colors";
|
||||
|
||||
function hexToRgba(hex: string, alpha: number): string {
|
||||
const h = hex.replace("#", "");
|
||||
const r = parseInt(h.substring(0, 2), 16);
|
||||
const g = parseInt(h.substring(2, 4), 16);
|
||||
const b = parseInt(h.substring(4, 6), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
}
|
||||
|
||||
type ColorSettings = {
|
||||
client_color_primary: string;
|
||||
client_color_secondary: string;
|
||||
client_color_success: string;
|
||||
client_color_danger: string;
|
||||
client_color_warning: string;
|
||||
client_title_gradient_from: string;
|
||||
client_title_gradient_to: string;
|
||||
};
|
||||
|
||||
function applyClientColors(s: ColorSettings) {
|
||||
const root = document.documentElement;
|
||||
|
||||
if (s.client_color_primary) {
|
||||
const p = s.client_color_primary;
|
||||
root.style.setProperty("--primary", p);
|
||||
root.style.setProperty("--primary-soft", hexToRgba(p, 0.12));
|
||||
root.style.setProperty("--primary-glow", hexToRgba(p, 0.25));
|
||||
}
|
||||
if (s.client_color_secondary) {
|
||||
root.style.setProperty("--cyan", s.client_color_secondary);
|
||||
root.style.setProperty("--cyan-soft", hexToRgba(s.client_color_secondary, 0.1));
|
||||
}
|
||||
if (s.client_color_danger) {
|
||||
root.style.setProperty("--red", s.client_color_danger);
|
||||
}
|
||||
if (s.client_title_gradient_from) {
|
||||
root.style.setProperty("--title-gradient-from", s.client_title_gradient_from);
|
||||
}
|
||||
if (s.client_title_gradient_to) {
|
||||
root.style.setProperty("--title-gradient-to", s.client_title_gradient_to);
|
||||
}
|
||||
}
|
||||
|
||||
function saveColorsToStorage(s: ColorSettings) {
|
||||
try {
|
||||
localStorage.setItem(LS_COLORS_KEY, JSON.stringify(s));
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
function loadColorsFromStorage(): ColorSettings | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(LS_COLORS_KEY);
|
||||
if (raw) return JSON.parse(raw) as ColorSettings;
|
||||
} catch { /* ignore */ }
|
||||
return null;
|
||||
}
|
||||
|
||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
const [theme, setTheme] = useState<Theme>(() => {
|
||||
const stored = localStorage.getItem("theme");
|
||||
if (stored === "light" || stored === "dark") return stored;
|
||||
return "dark";
|
||||
});
|
||||
const lastSettings = useRef<ColorSettings | null>(null);
|
||||
|
||||
// Appliquer les couleurs sauvegardées immédiatement (avant l'appel API)
|
||||
useEffect(() => {
|
||||
const cached = loadColorsFromStorage();
|
||||
if (cached) {
|
||||
lastSettings.current = cached;
|
||||
applyClientColors(cached);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
localStorage.setItem("theme", theme);
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchColors = async () => {
|
||||
try {
|
||||
const settings = await getPublicSettings();
|
||||
lastSettings.current = settings;
|
||||
saveColorsToStorage(settings);
|
||||
applyClientColors(settings);
|
||||
} catch { /* ignore */ }
|
||||
};
|
||||
fetchColors();
|
||||
const onVisibility = () => {
|
||||
if (document.visibilityState === "visible") fetchColors();
|
||||
};
|
||||
document.addEventListener("visibilitychange", onVisibility);
|
||||
return () => document.removeEventListener("visibilitychange", onVisibility);
|
||||
}, []);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme((prev) => (prev === "dark" ? "light" : "dark"));
|
||||
};
|
||||
|
||||
+85
-83
@@ -1,126 +1,128 @@
|
||||
/* Reset et optimisation mobile */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
color-scheme: dark;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-size: 16px;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
color-scheme: dark;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-size: 16px;
|
||||
|
||||
--bg: #09090b;
|
||||
--surface: #111115;
|
||||
--surface-2: #1c1c22;
|
||||
--border: rgba(255, 255, 255, 0.07);
|
||||
--primary: #8b5cf6;
|
||||
--primary-soft: rgba(139, 92, 246, 0.12);
|
||||
--primary-glow: rgba(139, 92, 246, 0.25);
|
||||
--cyan: #22d3ee;
|
||||
--cyan-soft: rgba(34, 211, 238, 0.1);
|
||||
--red: #ef4444;
|
||||
--text: #f4f4f5;
|
||||
--text-muted: #71717a;
|
||||
--radius: 10px;
|
||||
--transition: 0.22s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--bg: #09090b;
|
||||
--surface: #111115;
|
||||
--surface-2: #1c1c22;
|
||||
--border: rgba(255, 255, 255, 0.07);
|
||||
--primary: #8b5cf6;
|
||||
--primary-soft: rgba(139, 92, 246, 0.12);
|
||||
--primary-glow: rgba(139, 92, 246, 0.25);
|
||||
--cyan: #22d3ee;
|
||||
--cyan-soft: rgba(34, 211, 238, 0.1);
|
||||
--red: #ef4444;
|
||||
--text: #f4f4f5;
|
||||
--text-muted: #71717a;
|
||||
--radius: 10px;
|
||||
--transition: 0.22s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
color-scheme: light;
|
||||
--bg: #f4f4f8;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #ebebf2;
|
||||
--border: rgba(0, 0, 0, 0.08);
|
||||
--primary: #7c3aed;
|
||||
--primary-soft: rgba(124, 58, 237, 0.08);
|
||||
--primary-glow: rgba(124, 58, 237, 0.18);
|
||||
--cyan: #0891b2;
|
||||
--cyan-soft: rgba(8, 145, 178, 0.1);
|
||||
--red: #dc2626;
|
||||
--text: #18181b;
|
||||
--text-muted: #52525b;
|
||||
color-scheme: light;
|
||||
--bg: #f4f4f8;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #ebebf2;
|
||||
--border: rgba(0, 0, 0, 0.08);
|
||||
--primary: #7c3aed;
|
||||
--primary-soft: rgba(124, 58, 237, 0.08);
|
||||
--primary-glow: rgba(124, 58, 237, 0.18);
|
||||
--cyan: #0891b2;
|
||||
--cyan-soft: rgba(8, 145, 178, 0.1);
|
||||
--red: #dc2626;
|
||||
--text: #18181b;
|
||||
--text-muted: #52525b;
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
/* Désactiver le scroll bounce sur iOS */
|
||||
overscroll-behavior: none;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
/* Désactiver le scroll bounce sur iOS */
|
||||
overscroll-behavior: none;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
body {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
a:active {
|
||||
opacity: 0.7;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(1.8rem, 5vw, 3.2rem);
|
||||
line-height: 1.1;
|
||||
font-size: clamp(1.8rem, 5vw, 3.2rem);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: clamp(1.5rem, 4vw, 2.5rem);
|
||||
font-size: clamp(1.5rem, 4vw, 2.5rem);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: clamp(0.9rem, 2.5vw, 1.1rem);
|
||||
font-size: clamp(0.9rem, 2.5vw, 1.1rem);
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.8em 1.5em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: var(--surface);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.8em 1.5em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: var(--surface);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: scale(0.95);
|
||||
opacity: 0.8;
|
||||
transform: scale(0.95);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Désactiver le hover sur mobile */
|
||||
@media (hover: none) {
|
||||
button:hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
button:hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
+158
-151
@@ -1,233 +1,240 @@
|
||||
/* Reset et optimisation mobile */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
/* Empêcher le zoom sur les inputs pour iOS */
|
||||
font-size: 16px;
|
||||
/* Couleurs néon violet */
|
||||
--neon-violet: rgba(138, 43, 226, 1); /* Blueviolet */
|
||||
--neon-violet-light: rgba(186, 85, 211, 1); /* Medium Orchid */
|
||||
--neon-violet-glow: rgba(138, 43, 226, 0.6);
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
/* Empêcher le zoom sur les inputs pour iOS */
|
||||
font-size: 16px;
|
||||
/* Couleurs néon violet */
|
||||
--neon-violet: rgba(138, 43, 226, 1); /* Blueviolet */
|
||||
--neon-violet-light: rgba(186, 85, 211, 1); /* Medium Orchid */
|
||||
--neon-violet-glow: rgba(138, 43, 226, 0.6);
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
/* Désactiver le scroll bounce sur iOS */
|
||||
overscroll-behavior: none;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
/* Ne pas forcer de background pour permettre aux pages d'avoir leur propre style */
|
||||
background-color: transparent !important;
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
/* Désactiver le scroll bounce sur iOS */
|
||||
overscroll-behavior: none;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
body {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Effet de hover sur toute la page - Curseur personnalisé violet */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 2px solid var(--neon-violet-glow);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
opacity: 0;
|
||||
box-shadow: 0 0 10px var(--neon-violet-glow);
|
||||
content: "";
|
||||
position: fixed;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 2px solid var(--neon-violet-glow);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
opacity 0.2s ease;
|
||||
opacity: 0;
|
||||
box-shadow: 0 0 10px var(--neon-violet-glow);
|
||||
}
|
||||
|
||||
body:hover::before {
|
||||
opacity: 1;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Effet de gradient violet qui suit le curseur */
|
||||
body::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
background: radial-gradient(circle, rgba(138, 43, 226, 0.15) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
content: "";
|
||||
position: fixed;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
rgba(138, 43, 226, 0.15) 0%,
|
||||
transparent 70%
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
body:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
body:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
/* Ne pas forcer de background sur root non plus */
|
||||
background-color: transparent !important;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
/* Ne pas forcer de background sur root non plus */
|
||||
background-color: transparent !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Effet de particules/grille au hover avec teinte violette */
|
||||
#root::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image:
|
||||
linear-gradient(rgba(186, 85, 211, 0.05) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(186, 85, 211, 0.05) 1px, transparent 1px);
|
||||
background-size: 50px 50px;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease;
|
||||
z-index: 0;
|
||||
content: "";
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image:
|
||||
linear-gradient(rgba(186, 85, 211, 0.05) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(186, 85, 211, 0.05) 1px, transparent 1px);
|
||||
background-size: 50px 50px;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
#root:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
#root:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
a:active {
|
||||
opacity: 0.7;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(1.8rem, 5vw, 3.2rem);
|
||||
line-height: 1.1;
|
||||
font-size: clamp(1.8rem, 5vw, 3.2rem);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: clamp(1.5rem, 4vw, 2.5rem);
|
||||
font-size: clamp(1.5rem, 4vw, 2.5rem);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: clamp(0.9rem, 2.5vw, 1.1rem);
|
||||
font-size: clamp(0.9rem, 2.5vw, 1.1rem);
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.8em 1.5em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: var(--surface);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.8em 1.5em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: var(--surface);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: scale(0.95);
|
||||
opacity: 0.8;
|
||||
transform: scale(0.95);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Désactiver le hover sur mobile */
|
||||
@media (hover: none) {
|
||||
button:hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
/* Désactiver les effets de hover sur mobile */
|
||||
body::before,
|
||||
body::after,
|
||||
#root::before {
|
||||
display: none;
|
||||
}
|
||||
button:hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
/* Désactiver les effets de hover sur mobile */
|
||||
body::before,
|
||||
body::after,
|
||||
#root::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.home-container2 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: calc(100vh - 60px);
|
||||
padding: 2rem;
|
||||
gap: 2rem;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: calc(100vh - 60px);
|
||||
padding: 2rem;
|
||||
gap: 2rem;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* Container plein écran pour la vidéo */
|
||||
.home-container-fullscreen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
z-index: 1000;
|
||||
background-color: #000;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
z-index: 1000;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
/* Vidéo en plein écran */
|
||||
.home-video-fullscreen {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
transform: translate(-50%, -50%);
|
||||
object-fit: cover;
|
||||
z-index: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
transform: translate(-50%, -50%);
|
||||
object-fit: cover;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Effet au hover (desktop uniquement) */
|
||||
@media (hover: hover) {
|
||||
.home-container-fullscreen:hover {
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.home-container-fullscreen:hover .home-video-fullscreen {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
.home-container-fullscreen:hover {
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.home-container-fullscreen:hover .home-video-fullscreen {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Effet au clic */
|
||||
.home-container-fullscreen:active {
|
||||
opacity: 0.9;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Optimisation mobile */
|
||||
@media (max-width: 768px) {
|
||||
.home-video-fullscreen {
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
.home-video-fullscreen {
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,13 @@ function UserAccueil() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getCategories().then(setCategories);
|
||||
getCategories().then((cats) => {
|
||||
const sorted = [...cats].sort((a, b) => {
|
||||
if (a.is_coming_soon === b.is_coming_soon) return 0;
|
||||
return a.is_coming_soon ? 1 : -1;
|
||||
});
|
||||
setCategories(sorted);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -38,15 +44,11 @@ function UserAccueil() {
|
||||
return;
|
||||
}
|
||||
loadProducts();
|
||||
const interval = setInterval(() => loadProducts(true), 30_000);
|
||||
return () => clearInterval(interval);
|
||||
}, [selectedCategory, categories]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const loadProducts = async (silent = false) => {
|
||||
if (!silent) {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
}
|
||||
const loadProducts = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
let response;
|
||||
@@ -58,25 +60,14 @@ function UserAccueil() {
|
||||
}
|
||||
|
||||
if (response.success && response.data) {
|
||||
if (silent) {
|
||||
setProducts((prev) =>
|
||||
prev.map((p) => {
|
||||
const updated = response.data!.find((r: Product) => r.id === p.id);
|
||||
return updated ? { ...p, stock: updated.stock } : p;
|
||||
})
|
||||
);
|
||||
} else {
|
||||
setProducts(response.data);
|
||||
}
|
||||
} else if (!silent) {
|
||||
setProducts(response.data);
|
||||
} else {
|
||||
setProducts([]);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
if (!silent) {
|
||||
setError(err instanceof Error ? err.message : "Erreur lors du chargement des produits");
|
||||
}
|
||||
setError(err instanceof Error ? err.message : "Erreur lors du chargement des produits");
|
||||
} finally {
|
||||
if (!silent) setLoading(false);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -188,9 +179,9 @@ function UserAccueil() {
|
||||
</div>
|
||||
|
||||
{isSelectedComingSoon ? (
|
||||
<div className="coming-soon-overlay">
|
||||
<span className="coming-soon-text">Prochainement</span>
|
||||
<p className="coming-soon-desc">
|
||||
<div className="category-cs-overlay">
|
||||
<span className="category-cs-text">Prochainement</span>
|
||||
<p className="category-cs-desc">
|
||||
Les produits de cette catégorie arrivent bientôt !
|
||||
</p>
|
||||
</div>
|
||||
@@ -201,7 +192,7 @@ function UserAccueil() {
|
||||
) : error ? (
|
||||
<div className="error-container">
|
||||
<p className="error-message">{error}</p>
|
||||
<button onClick={() => loadProducts()}>Réessayer</button>
|
||||
<button onClick={loadProducts}>Réessayer</button>
|
||||
</div>
|
||||
) : products.length === 0 ? (
|
||||
<div className="empty-container">
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
padding-top: 60px;
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
padding-bottom: 140px;
|
||||
padding-bottom: calc(
|
||||
var(--bottomnav-h, 66px) + var(--bottomnav-offset, 15px) + 150px
|
||||
);
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
@@ -22,6 +24,7 @@
|
||||
align-items: center;
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-top: -70px;
|
||||
}
|
||||
|
||||
.cart-header-left {
|
||||
@@ -177,7 +180,9 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.15s, background 0.15s;
|
||||
transition:
|
||||
opacity 0.15s,
|
||||
background 0.15s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -197,15 +202,22 @@
|
||||
|
||||
.cart-footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
/* Remonté au-dessus de la bottom-nav flottante (qui est en
|
||||
bottom: var(--bottomnav-offset) avec z-index: 900), pour que
|
||||
le bouton "Commander" ne soit jamais masqué par le menu. */
|
||||
bottom: calc(
|
||||
var(--bottomnav-h, 66px) + var(--bottomnav-offset, 15px) + 10px
|
||||
);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 100%;
|
||||
width: calc(100% - 1.5rem);
|
||||
max-width: 640px;
|
||||
background: var(--surface);
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 1rem 1.25rem 1.25rem;
|
||||
z-index: 100;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 1rem 1.25rem 1.1rem;
|
||||
z-index: 850;
|
||||
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.cart-footer-total {
|
||||
@@ -236,7 +248,9 @@
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s, transform 0.15s;
|
||||
transition:
|
||||
opacity 0.2s,
|
||||
transform 0.15s;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
@@ -296,7 +310,9 @@
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s, transform 0.15s;
|
||||
transition:
|
||||
opacity 0.2s,
|
||||
transform 0.15s;
|
||||
}
|
||||
|
||||
.continue-shopping:hover {
|
||||
@@ -320,8 +336,13 @@
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 0.5; }
|
||||
50% { opacity: 1; }
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
@@ -342,8 +363,12 @@
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content2 {
|
||||
@@ -357,8 +382,14 @@
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { opacity: 0; transform: translateY(16px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(16px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-icon-container {
|
||||
@@ -404,7 +435,9 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
transition: opacity 0.15s, transform 0.15s;
|
||||
transition:
|
||||
opacity 0.15s,
|
||||
transform 0.15s;
|
||||
}
|
||||
|
||||
.modal-button:hover:not(:disabled) {
|
||||
@@ -473,7 +506,9 @@
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
font-size: 1.1rem;
|
||||
transition: background 0.2s, transform 0.2s;
|
||||
transition:
|
||||
background 0.2s,
|
||||
transform 0.2s;
|
||||
}
|
||||
|
||||
.video-close-btn:hover {
|
||||
|
||||
@@ -48,6 +48,10 @@ function Checkout() {
|
||||
const [showZoneModal, setShowZoneModal] = useState(false);
|
||||
const [zoneErrorMsg, setZoneErrorMsg] = useState('');
|
||||
|
||||
// État pour le modal adresse invalide (correction suggérée)
|
||||
const [showInvalidAddressModal, setShowInvalidAddressModal] = useState(false);
|
||||
const [suggestedAddress, setSuggestedAddress] = useState('');
|
||||
|
||||
// Informations personnelles
|
||||
const [firstName, setFirstName] = useState('');
|
||||
const [lastName, setLastName] = useState('');
|
||||
@@ -315,6 +319,12 @@ function Checkout() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.success && response.invalid_address && response.suggested_address) {
|
||||
setSuggestedAddress(response.suggested_address);
|
||||
setShowInvalidAddressModal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.success && response.command_id) {
|
||||
const { command_id, assigned_to, queue_info, delivery_address } = response;
|
||||
|
||||
@@ -725,6 +735,46 @@ function Checkout() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ============================================ */}
|
||||
{/* MODAL ADRESSE INVALIDE (CORRECTION SUGGÉRÉE) */}
|
||||
{/* ============================================ */}
|
||||
{showInvalidAddressModal && (
|
||||
<div className="confirmation-modal-overlay" onClick={() => setShowInvalidAddressModal(false)}>
|
||||
<div className="confirmation-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="confirmation-modal-header confirmation-modal-header--error">
|
||||
<i className="fas fa-map-marker-alt confirmation-icon"></i>
|
||||
<h2>Adresse invalide</h2>
|
||||
</div>
|
||||
<div className="confirmation-modal-body">
|
||||
<div className="confirmation-section">
|
||||
<div className="confirmation-detail confirmation-detail--centered">
|
||||
<p style={{ marginBottom: '0.75rem' }}>
|
||||
L'adresse saisie n'est pas reconnue. Voulez-vous utiliser l'adresse correcte suggérée ?
|
||||
</p>
|
||||
<p className="confirmation-detail--muted" style={{ fontWeight: 600 }}>
|
||||
{suggestedAddress}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="confirmation-modal-actions">
|
||||
<button className="confirmation-button confirmation-button--neutral" onClick={() => setShowInvalidAddressModal(false)}>
|
||||
Modifier
|
||||
</button>
|
||||
<button
|
||||
className="confirmation-button"
|
||||
onClick={() => {
|
||||
setAddress(suggestedAddress);
|
||||
setShowInvalidAddressModal(false);
|
||||
}}
|
||||
>
|
||||
Utiliser cette adresse
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ============================================ */}
|
||||
{/* MODAL DE CONFIRMATION STYLISÉ */}
|
||||
{/* ============================================ */}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
margin: 0 0 1.25rem 0;
|
||||
margin-top: -70px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
@@ -47,7 +48,10 @@
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 0.35rem;
|
||||
transition: transform 0.2s, border-color 0.2s, box-shadow 0.2s;
|
||||
transition:
|
||||
transform 0.2s,
|
||||
border-color 0.2s,
|
||||
box-shadow 0.2s;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@@ -79,18 +83,32 @@
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-icon.icon-total-orders { background: linear-gradient(135deg, #7c3aed, #6d28d9); }
|
||||
.stat-icon.icon-total { background: linear-gradient(135deg, #f59e0b, #d97706); }
|
||||
.stat-icon.icon-penalty-ok { background: rgba(100, 116, 139, 0.3); color: var(--text-muted); }
|
||||
.stat-icon.icon-penalty-warning { background: linear-gradient(135deg, #f59e0b, #d97706); }
|
||||
.stat-icon.icon-total-orders {
|
||||
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
||||
}
|
||||
.stat-icon.icon-total {
|
||||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||
}
|
||||
.stat-icon.icon-penalty-ok {
|
||||
background: rgba(100, 116, 139, 0.3);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.stat-icon.icon-penalty-warning {
|
||||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||
}
|
||||
.stat-icon.icon-penalty-critical {
|
||||
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||
animation: pulse-danger 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-danger {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.5); }
|
||||
50% { box-shadow: 0 0 0 8px rgba(239, 68, 68, 0); }
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.5);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 8px rgba(239, 68, 68, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
@@ -101,8 +119,12 @@
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat-value.value-danger { color: #ef4444; }
|
||||
.stat-value.value-warning { color: #f59e0b; }
|
||||
.stat-value.value-danger {
|
||||
color: #ef4444;
|
||||
}
|
||||
.stat-value.value-warning {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--text-muted);
|
||||
@@ -125,7 +147,9 @@
|
||||
padding: 0.875rem 1rem;
|
||||
margin-bottom: 1.25rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, border-color 0.2s;
|
||||
transition:
|
||||
background 0.2s,
|
||||
border-color 0.2s;
|
||||
}
|
||||
|
||||
.referral-btn-row:hover {
|
||||
@@ -328,7 +352,10 @@
|
||||
border-radius: 12px;
|
||||
padding: 1rem 1.125rem;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s, border-color 0.15s, box-shadow 0.15s;
|
||||
transition:
|
||||
transform 0.15s,
|
||||
border-color 0.15s,
|
||||
box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.order-card:hover {
|
||||
@@ -449,7 +476,9 @@
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
@@ -521,7 +550,9 @@
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
transition:
|
||||
opacity 0.2s,
|
||||
transform 0.2s;
|
||||
}
|
||||
|
||||
.browse-button:hover {
|
||||
@@ -529,6 +560,219 @@
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
MODAL — CHOIX DU PRODUIT RÉCOMPENSE
|
||||
============================================ */
|
||||
|
||||
.reward-modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
backdrop-filter: blur(6px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
z-index: 1000;
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
.reward-modal {
|
||||
background: var(--surface);
|
||||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
animation: slideUp 0.25s ease;
|
||||
}
|
||||
|
||||
.reward-modal-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.reward-modal-header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
.reward-modal-icon {
|
||||
color: #f59e0b;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.reward-modal-header h2 {
|
||||
color: var(--text);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.reward-modal-close {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.reward-modal-close:hover {
|
||||
background: var(--surface-2);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.reward-modal-subtitle {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
margin: 0 0 1.1rem 0;
|
||||
}
|
||||
|
||||
.reward-modal-products {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.625rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.reward-product-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
background: rgba(245, 158, 11, 0.05);
|
||||
border: 1.5px solid rgba(245, 158, 11, 0.2);
|
||||
border-radius: 10px;
|
||||
padding: 0.75rem 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
text-align: left;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.reward-product-card:hover {
|
||||
border-color: rgba(245, 158, 11, 0.5);
|
||||
background: rgba(245, 158, 11, 0.09);
|
||||
}
|
||||
|
||||
.reward-product-card.selected {
|
||||
border-color: #f59e0b;
|
||||
background: rgba(245, 158, 11, 0.14);
|
||||
}
|
||||
|
||||
.reward-product-radio {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(245, 158, 11, 0.5);
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #f59e0b;
|
||||
font-size: 0.7rem;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.reward-product-card.selected .reward-product-radio {
|
||||
background: #f59e0b;
|
||||
border-color: #f59e0b;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.reward-product-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.reward-product-name {
|
||||
color: var(--text);
|
||||
font-size: 0.92rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.reward-product-meta {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.reward-product-free {
|
||||
flex-shrink: 0;
|
||||
color: #10b981;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
background: rgba(16, 185, 129, 0.12);
|
||||
border-radius: 6px;
|
||||
padding: 3px 8px;
|
||||
}
|
||||
|
||||
.reward-modal-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.reward-modal-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 10px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition:
|
||||
opacity 0.15s,
|
||||
transform 0.15s;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.reward-modal-btn:hover:not(:disabled) {
|
||||
opacity: 0.88;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.reward-modal-btn-cancel {
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.reward-modal-btn-confirm {
|
||||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.reward-modal-btn-confirm:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.reward-modal {
|
||||
padding: 1.25rem;
|
||||
border-radius: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
RESPONSIVE
|
||||
============================================ */
|
||||
|
||||
@@ -13,7 +13,12 @@ import {
|
||||
getMyPointsRewards,
|
||||
claimMyReward,
|
||||
} from "../../api/api";
|
||||
import type { PublicSettings, PointsPoolInfo, PointsRewardConfig, RewardItemConfig } from "../../api/api";
|
||||
import type {
|
||||
PublicSettings,
|
||||
PointsPoolInfo,
|
||||
PointsRewardConfig,
|
||||
RewardItemConfig,
|
||||
} from "../../api/api";
|
||||
import type {
|
||||
CompletedOrder,
|
||||
ClientStats,
|
||||
@@ -38,6 +43,8 @@ import {
|
||||
faChevronRight,
|
||||
faCheckCircle,
|
||||
faHistory,
|
||||
faTimes,
|
||||
faCheck,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
function ConsultationHistorique() {
|
||||
@@ -60,6 +67,13 @@ function ConsultationHistorique() {
|
||||
shop_name: "Milieu-Nantais",
|
||||
two_fa_enabled: false,
|
||||
contact_telegram: "",
|
||||
client_color_primary: "",
|
||||
client_color_secondary: "",
|
||||
client_color_success: "",
|
||||
client_color_danger: "",
|
||||
client_color_warning: "",
|
||||
client_title_gradient_from: "",
|
||||
client_title_gradient_to: "",
|
||||
});
|
||||
const [referralBalance, setReferralBalance] = useState<number>(0);
|
||||
const [pointsRewards, setPointsRewards] = useState<{
|
||||
@@ -68,10 +82,21 @@ function ConsultationHistorique() {
|
||||
reward: PointsRewardConfig | null;
|
||||
} | null>(null);
|
||||
const [claimingPool, setClaimingPool] = useState<string | null>(null);
|
||||
const [claimFeedback, setClaimFeedback] = useState<{ pool: string; type: "success" | "error"; text: string } | null>(null);
|
||||
const [claimFeedback, setClaimFeedback] = useState<{
|
||||
pool: string;
|
||||
type: "success" | "error";
|
||||
text: string;
|
||||
} | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
// ── Modal de sélection du produit récompense ──
|
||||
const [rewardModalPool, setRewardModalPool] =
|
||||
useState<PointsPoolInfo | null>(null);
|
||||
const [selectedProductId, setSelectedProductId] = useState<number | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isUserAuthenticated())
|
||||
navigate("/login/client", { replace: true });
|
||||
@@ -152,37 +177,51 @@ function ConsultationHistorique() {
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<div className="history-container">
|
||||
<div className="loading-container">
|
||||
<div className="loading-spinner">
|
||||
<div className="spinner"></div>
|
||||
</div>
|
||||
<p className="loading-text">
|
||||
Chargement de l'historique...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
// Ouvre la modal de choix si plusieurs produits sont éligibles,
|
||||
// sinon réclame directement (1 seul produit configuré ou aucun item).
|
||||
const openRewardModal = (pool: PointsPoolInfo) => {
|
||||
const items = pool.eligible_reward_items ?? [];
|
||||
if (items.length > 1) {
|
||||
setSelectedProductId(items[0]?.product_id ?? null);
|
||||
setRewardModalPool(pool);
|
||||
} else {
|
||||
handleClaim(pool.key);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClaim = async (poolKey: string) => {
|
||||
const closeRewardModal = () => {
|
||||
setRewardModalPool(null);
|
||||
setSelectedProductId(null);
|
||||
};
|
||||
|
||||
const confirmRewardChoice = async () => {
|
||||
if (!rewardModalPool) return;
|
||||
const poolKey = rewardModalPool.key;
|
||||
const productId = selectedProductId ?? undefined;
|
||||
closeRewardModal();
|
||||
await handleClaim(poolKey, productId);
|
||||
};
|
||||
|
||||
const handleClaim = async (poolKey: string, productId?: number) => {
|
||||
setClaimingPool(poolKey);
|
||||
setClaimFeedback(null);
|
||||
const res = await claimMyReward(poolKey);
|
||||
const res = await claimMyReward(poolKey, productId);
|
||||
setClaimingPool(null);
|
||||
if (res.success) {
|
||||
const text = res.product_added && res.product_name
|
||||
? `🎁 ${res.product_name} ajouté à votre panier ! Commandez au moins un produit pour en profiter.`
|
||||
: res.description || "Récompense réclamée !";
|
||||
const text =
|
||||
res.product_added && res.product_names?.length
|
||||
? `🎁 ${res.product_names.join(", ")} ajouté${res.product_names.length > 1 ? "s" : ""} à votre panier ! Commandez au moins un produit pour en profiter.`
|
||||
: res.description || "Récompense réclamée !";
|
||||
setClaimFeedback({ pool: poolKey, type: "success", text });
|
||||
getMyPointsRewards().then((r) => { if (r.success) setPointsRewards(r); });
|
||||
getMyPointsRewards().then((r) => {
|
||||
if (r.success) setPointsRewards(r);
|
||||
});
|
||||
} else {
|
||||
setClaimFeedback({ pool: poolKey, type: "error", text: res.error || "Erreur" });
|
||||
setClaimFeedback({
|
||||
pool: poolKey,
|
||||
type: "error",
|
||||
text: res.error || "Erreur",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -202,6 +241,24 @@ function ConsultationHistorique() {
|
||||
"#7c3aed",
|
||||
];
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<div className="history-container">
|
||||
<div className="loading-container">
|
||||
<div className="loading-spinner">
|
||||
<div className="spinner"></div>
|
||||
</div>
|
||||
<p className="loading-text">
|
||||
Chargement de l'historique...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
@@ -298,81 +355,178 @@ function ConsultationHistorique() {
|
||||
</div>
|
||||
|
||||
{/* Section récompenses par palier */}
|
||||
{pointsRewards?.enabled && pointsRewards.reward && pointsRewards.pools.length > 0 && (
|
||||
<div className="rewards-section">
|
||||
<p className="rewards-section-title">
|
||||
<FontAwesomeIcon icon={faTrophy} className="rewards-section-icon" />
|
||||
Récompenses
|
||||
</p>
|
||||
{pointsRewards.reward.description && (
|
||||
<p className="rewards-section-desc">{pointsRewards.reward.description}</p>
|
||||
)}
|
||||
{(pointsRewards.reward.reward_items ?? []).filter((it: RewardItemConfig) => it.product_name).length > 0 && (
|
||||
<div className="reward-eligible-cats">
|
||||
{(pointsRewards.reward.reward_items ?? []).map((it: RewardItemConfig, idx: number) => (
|
||||
<span key={idx} className="reward-eligible-cat">
|
||||
<FontAwesomeIcon icon={faGift} style={{ marginRight: 4 }} />
|
||||
{it.product_name}{it.quantity > 0 && it.quantity !== 1 ? ` ×${it.quantity}` : ""}{it.price > 0 ? ` — ${it.price}€` : ""}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="rewards-pools">
|
||||
{pointsRewards.pools.map((pool) => {
|
||||
const threshold = pointsRewards.reward!.threshold;
|
||||
const progress = Math.min(100, Math.round((pool.points % threshold) / threshold * 100));
|
||||
const remaining = threshold - (pool.points % threshold);
|
||||
const isClaiming = claimingPool === pool.key;
|
||||
const feedback = claimFeedback?.pool === pool.key ? claimFeedback : null;
|
||||
return (
|
||||
<div key={pool.key} className="reward-pool-card">
|
||||
<div className="reward-pool-header">
|
||||
<span className="reward-pool-name">{pool.name}</span>
|
||||
<span className="reward-pool-pts">{pool.points} pts</span>
|
||||
</div>
|
||||
{pool.eligible_configs.length > 0 && (
|
||||
<div className="reward-eligible-cats">
|
||||
{pool.eligible_configs.flatMap((cfg) =>
|
||||
cfg.all_products
|
||||
? [<span key={cfg.category} className="reward-eligible-cat">
|
||||
{cfg.category}
|
||||
</span>]
|
||||
: (cfg.product_names ?? []).map((name) => (
|
||||
<span key={`${cfg.category}-${name}`} className="reward-eligible-cat">
|
||||
{name}
|
||||
</span>
|
||||
))
|
||||
{pointsRewards?.enabled &&
|
||||
pointsRewards.reward &&
|
||||
pointsRewards.pools.length > 0 && (
|
||||
<div className="rewards-section">
|
||||
<p className="rewards-section-title">
|
||||
<FontAwesomeIcon
|
||||
icon={faTrophy}
|
||||
className="rewards-section-icon"
|
||||
/>
|
||||
Récompenses
|
||||
</p>
|
||||
{pointsRewards.reward.description && (
|
||||
<p className="rewards-section-desc">
|
||||
{pointsRewards.reward.description}
|
||||
</p>
|
||||
)}
|
||||
{(pointsRewards.reward.reward_items ?? []).filter(
|
||||
(it: RewardItemConfig) => it.product_name,
|
||||
).length > 0 && (
|
||||
<div className="reward-eligible-cats">
|
||||
{(
|
||||
pointsRewards.reward.reward_items ?? []
|
||||
).map(
|
||||
(it: RewardItemConfig, idx: number) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="reward-eligible-cat"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faGift}
|
||||
style={{ marginRight: 4 }}
|
||||
/>
|
||||
{it.product_name}
|
||||
{it.quantity > 0 &&
|
||||
it.quantity !== 1
|
||||
? ` ×${it.quantity}`
|
||||
: ""}
|
||||
{it.price > 0
|
||||
? ` — ${it.price}€`
|
||||
: ""}
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="rewards-pools">
|
||||
{pointsRewards.pools.map((pool) => {
|
||||
const threshold =
|
||||
pointsRewards.reward!.threshold;
|
||||
const progress = Math.min(
|
||||
100,
|
||||
Math.round(
|
||||
((pool.points % threshold) /
|
||||
threshold) *
|
||||
100,
|
||||
),
|
||||
);
|
||||
const remaining =
|
||||
threshold - (pool.points % threshold);
|
||||
const isClaiming =
|
||||
claimingPool === pool.key;
|
||||
const feedback =
|
||||
claimFeedback?.pool === pool.key
|
||||
? claimFeedback
|
||||
: null;
|
||||
return (
|
||||
<div
|
||||
key={pool.key}
|
||||
className="reward-pool-card"
|
||||
>
|
||||
<div className="reward-pool-header">
|
||||
<span className="reward-pool-name">
|
||||
{pool.name}
|
||||
</span>
|
||||
<span className="reward-pool-pts">
|
||||
{pool.points} pts
|
||||
</span>
|
||||
</div>
|
||||
{pool.eligible_configs.length >
|
||||
0 && (
|
||||
<div className="reward-eligible-cats">
|
||||
{pool.eligible_configs.flatMap(
|
||||
(cfg) =>
|
||||
cfg.all_products
|
||||
? [
|
||||
<span
|
||||
key={
|
||||
cfg.category
|
||||
}
|
||||
className="reward-eligible-cat"
|
||||
>
|
||||
{
|
||||
cfg.category
|
||||
}
|
||||
</span>,
|
||||
]
|
||||
: (
|
||||
cfg.product_names ??
|
||||
[]
|
||||
).map(
|
||||
(
|
||||
name,
|
||||
) => (
|
||||
<span
|
||||
key={`${cfg.category}-${name}`}
|
||||
className="reward-eligible-cat"
|
||||
>
|
||||
{
|
||||
name
|
||||
}
|
||||
</span>
|
||||
),
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="reward-progress-bar">
|
||||
<div
|
||||
className="reward-progress-fill"
|
||||
style={{
|
||||
width: `${progress}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="reward-pool-info">
|
||||
{pool.rewards_available > 0 ? (
|
||||
<span className="reward-available">
|
||||
{pool.rewards_available}{" "}
|
||||
récompense
|
||||
{pool.rewards_available >
|
||||
1
|
||||
? "s"
|
||||
: ""}{" "}
|
||||
disponible
|
||||
{pool.rewards_available >
|
||||
1
|
||||
? "s"
|
||||
: ""}
|
||||
</span>
|
||||
) : (
|
||||
<span className="reward-remaining">
|
||||
Encore {remaining} pts
|
||||
pour une récompense
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="reward-progress-bar">
|
||||
<div className="reward-progress-fill" style={{ width: `${progress}%` }} />
|
||||
</div>
|
||||
<div className="reward-pool-info">
|
||||
{pool.rewards_available > 0 ? (
|
||||
<span className="reward-available">{pool.rewards_available} récompense{pool.rewards_available > 1 ? "s" : ""} disponible{pool.rewards_available > 1 ? "s" : ""}</span>
|
||||
) : (
|
||||
<span className="reward-remaining">Encore {remaining} pts pour une récompense</span>
|
||||
{feedback && (
|
||||
<p
|
||||
className={`reward-feedback reward-feedback-${feedback.type}`}
|
||||
>
|
||||
{feedback.text}
|
||||
</p>
|
||||
)}
|
||||
{pool.rewards_available > 0 && (
|
||||
<button
|
||||
className="reward-claim-btn"
|
||||
onClick={() =>
|
||||
openRewardModal(pool)
|
||||
}
|
||||
disabled={isClaiming}
|
||||
>
|
||||
{isClaiming
|
||||
? "..."
|
||||
: "Réclamer ma récompense"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{feedback && (
|
||||
<p className={`reward-feedback reward-feedback-${feedback.type}`}>{feedback.text}</p>
|
||||
)}
|
||||
{pool.rewards_available > 0 && (
|
||||
<button
|
||||
className="reward-claim-btn"
|
||||
onClick={() => handleClaim(pool.key)}
|
||||
disabled={isClaiming}
|
||||
>
|
||||
{isClaiming ? "..." : "Réclamer ma récompense"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* Bouton parrainage */}
|
||||
{appSettings.referral_enabled && (
|
||||
@@ -494,10 +648,19 @@ function ConsultationHistorique() {
|
||||
|
||||
<div className="order-card-footer">
|
||||
<span className="order-card-total">
|
||||
{formatPrice((order.total_prix || 0) - (order.referral_used || 0))}
|
||||
{formatPrice(
|
||||
(order.total_prix || 0) -
|
||||
(order.referral_used || 0),
|
||||
)}
|
||||
{(order.referral_used || 0) > 0 && (
|
||||
<span className="order-card-referral">
|
||||
{" "}— dont {formatPrice(order.referral_used ?? 0)} parrainage
|
||||
{" "}
|
||||
— dont{" "}
|
||||
{formatPrice(
|
||||
order.referral_used ??
|
||||
0,
|
||||
)}{" "}
|
||||
parrainage
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
@@ -512,6 +675,111 @@ function ConsultationHistorique() {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ============================================ */}
|
||||
{/* MODAL — CHOIX DU PRODUIT RÉCOMPENSE */}
|
||||
{/* ============================================ */}
|
||||
{rewardModalPool && pointsRewards?.reward && (
|
||||
<div
|
||||
className="reward-modal-overlay"
|
||||
onClick={closeRewardModal}
|
||||
>
|
||||
<div
|
||||
className="reward-modal"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="reward-modal-header">
|
||||
<div className="reward-modal-header-title">
|
||||
<FontAwesomeIcon
|
||||
icon={faGift}
|
||||
className="reward-modal-icon"
|
||||
/>
|
||||
<h2>Choisissez votre récompense</h2>
|
||||
</div>
|
||||
<button
|
||||
className="reward-modal-close"
|
||||
onClick={closeRewardModal}
|
||||
aria-label="Fermer"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTimes} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="reward-modal-subtitle">
|
||||
{rewardModalPool.name} —{" "}
|
||||
{rewardModalPool.rewards_available} récompense
|
||||
{rewardModalPool.rewards_available > 1
|
||||
? "s"
|
||||
: ""}{" "}
|
||||
disponible
|
||||
{rewardModalPool.rewards_available > 1 ? "s" : ""}
|
||||
</p>
|
||||
|
||||
<div className="reward-modal-products">
|
||||
{(rewardModalPool.eligible_reward_items ?? []).map(
|
||||
(item) => {
|
||||
const isSelected =
|
||||
selectedProductId === item.product_id;
|
||||
return (
|
||||
<button
|
||||
key={item.product_id}
|
||||
type="button"
|
||||
className={`reward-product-card ${isSelected ? "selected" : ""}`}
|
||||
onClick={() =>
|
||||
setSelectedProductId(
|
||||
item.product_id,
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="reward-product-radio">
|
||||
{isSelected && (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="reward-product-info">
|
||||
<span className="reward-product-name">
|
||||
{item.product_name}
|
||||
</span>
|
||||
<span className="reward-product-meta">
|
||||
{item.quantity > 0 &&
|
||||
item.quantity !== 1
|
||||
? `×${item.quantity}`
|
||||
: ""}
|
||||
{item.price > 0
|
||||
? ` · valeur ${item.price.toFixed(2)} €`
|
||||
: ""}
|
||||
</span>
|
||||
</div>
|
||||
<span className="reward-product-free">
|
||||
Offert
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="reward-modal-actions">
|
||||
<button
|
||||
className="reward-modal-btn reward-modal-btn-cancel"
|
||||
onClick={closeRewardModal}
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
className="reward-modal-btn reward-modal-btn-confirm"
|
||||
onClick={confirmRewardChoice}
|
||||
disabled={selectedProductId === null}
|
||||
>
|
||||
<FontAwesomeIcon icon={faGift} />
|
||||
Confirmer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -802,6 +802,148 @@
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
RATING LIVREUR
|
||||
============================================ */
|
||||
|
||||
.rating-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
background: #f59e0b;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
margin-top: 0.75rem;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.rating-btn:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.rating-done-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
margin-top: 0.75rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.rating-done-text {
|
||||
color: #f59e0b;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.rating-star-filled {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.rating-star-empty {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.rating-modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.rating-modal-sheet {
|
||||
background: var(--surface, #1a1a2e);
|
||||
border-radius: 16px 16px 0 0;
|
||||
padding: 1.5rem 1.5rem 2.5rem;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.rating-modal-title {
|
||||
color: var(--text-primary, #fff);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.rating-modal-subtitle {
|
||||
color: var(--text-muted, #9ca3af);
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.rating-stars-row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.rating-star-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.rating-comment-input {
|
||||
width: 100%;
|
||||
background: var(--bg, #0f0f1a);
|
||||
border: 1px solid var(--border, #374151);
|
||||
border-radius: 6px;
|
||||
color: var(--text-primary, #fff);
|
||||
padding: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
resize: vertical;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.rating-comment-input::placeholder {
|
||||
color: var(--text-muted, #9ca3af);
|
||||
}
|
||||
|
||||
.rating-submit-btn {
|
||||
background: #f59e0b;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.rating-submit-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.rating-submit-btn:not(:disabled):hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.rating-cancel-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted, #9ca3af);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
PRINT STYLES
|
||||
============================================ */
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
isUserAuthenticated,
|
||||
getProductById,
|
||||
getMediaUrl,
|
||||
submitLivreurRating,
|
||||
getOrderRatingStatus,
|
||||
} from "../../api/api";
|
||||
import type { CompletedOrder, Product } from "../../api/api_types";
|
||||
import {
|
||||
@@ -26,6 +28,7 @@ import {
|
||||
ShoppingBag,
|
||||
Camera,
|
||||
X,
|
||||
Star,
|
||||
} from "lucide-react";
|
||||
|
||||
interface OrderProduct {
|
||||
@@ -73,6 +76,11 @@ function OrderDetails() {
|
||||
const [error, setError] = useState<string>("");
|
||||
const [showVideo, setShowVideo] = useState(false);
|
||||
const [currentVideoUrl, setCurrentVideoUrl] = useState<string>("");
|
||||
const [ratingModal, setRatingModal] = useState(false);
|
||||
const [selectedStars, setSelectedStars] = useState(0);
|
||||
const [ratingComment, setRatingComment] = useState("");
|
||||
const [ratingSubmitting, setRatingSubmitting] = useState(false);
|
||||
const [ratingDone, setRatingDone] = useState<{ rating: number; comment: string } | null>(null);
|
||||
|
||||
// ✅ VÉRIFICATION INITIALE DE L'AUTHENTIFICATION
|
||||
useEffect(() => {
|
||||
@@ -315,6 +323,11 @@ function OrderDetails() {
|
||||
|
||||
console.log("✅ [ORDER DETAILS] Données mappées:", orderData);
|
||||
setOrder(orderData);
|
||||
|
||||
if (apiData.command_info?.command_status === "approved") {
|
||||
const r = await getOrderRatingStatus(id);
|
||||
if (r.rated) setRatingDone({ rating: r.rating!, comment: r.comment ?? "" });
|
||||
}
|
||||
} else {
|
||||
console.error("❌ [ORDER DETAILS] Erreur:", result.message);
|
||||
setError(result.message || "Erreur lors du chargement");
|
||||
@@ -327,6 +340,17 @@ function OrderDetails() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitRating = async () => {
|
||||
if (selectedStars === 0 || !order) return;
|
||||
setRatingSubmitting(true);
|
||||
const res = await submitLivreurRating(order.id, selectedStars, ratingComment);
|
||||
setRatingSubmitting(false);
|
||||
if (res.success) {
|
||||
setRatingDone({ rating: selectedStars, comment: ratingComment });
|
||||
setRatingModal(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getProductCount = (totalPrix: number): number => {
|
||||
return Math.max(1, Math.round(totalPrix / 25));
|
||||
};
|
||||
@@ -446,6 +470,22 @@ function OrderDetails() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{order.status === "approved" && (
|
||||
ratingDone ? (
|
||||
<div className="rating-done-row">
|
||||
<Star size={14} className="rating-star-filled" />
|
||||
<span className="rating-done-text">
|
||||
Noté {ratingDone.rating}/5{ratingDone.comment ? ` · "${ratingDone.comment}"` : ""}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<button className="rating-btn" onClick={() => setRatingModal(true)}>
|
||||
<Star size={14} />
|
||||
Noter la livraison
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
|
||||
<div className="info-row">
|
||||
<div className="info-icon">
|
||||
<Calendar size={18} />
|
||||
@@ -759,6 +799,47 @@ function OrderDetails() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal notation livreur */}
|
||||
{ratingModal && (
|
||||
<div className="rating-modal-overlay" onClick={() => setRatingModal(false)}>
|
||||
<div className="rating-modal-sheet" onClick={(e) => e.stopPropagation()}>
|
||||
<h2 className="rating-modal-title">Noter la livraison</h2>
|
||||
{order.livreur_assign && (
|
||||
<p className="rating-modal-subtitle">{order.livreur_assign}</p>
|
||||
)}
|
||||
<div className="rating-stars-row">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<button key={star} className="rating-star-btn" onClick={() => setSelectedStars(star)}>
|
||||
<Star
|
||||
size={36}
|
||||
className={star <= selectedStars ? "rating-star-filled" : "rating-star-empty"}
|
||||
fill={star <= selectedStars ? "#f59e0b" : "none"}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<textarea
|
||||
className="rating-comment-input"
|
||||
placeholder="Commentaire (optionnel)"
|
||||
value={ratingComment}
|
||||
onChange={(e) => setRatingComment(e.target.value)}
|
||||
maxLength={500}
|
||||
rows={3}
|
||||
/>
|
||||
<button
|
||||
className="rating-submit-btn"
|
||||
onClick={handleSubmitRating}
|
||||
disabled={selectedStars === 0 || ratingSubmitting}
|
||||
>
|
||||
{ratingSubmitting ? "Envoi..." : "Envoyer ma note"}
|
||||
</button>
|
||||
<button className="rating-cancel-btn" onClick={() => setRatingModal(false)}>
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal vidéo */}
|
||||
{showVideo && currentVideoUrl && (
|
||||
<div className="video-modal" onClick={handleCloseVideo}>
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-top: -70px;
|
||||
}
|
||||
|
||||
.parrainage-subtitle {
|
||||
@@ -51,7 +52,11 @@
|
||||
============================================ */
|
||||
|
||||
.parrainage-balance-card {
|
||||
background: linear-gradient(135deg, rgba(139, 92, 246, 0.15), rgba(139, 92, 246, 0.05));
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(139, 92, 246, 0.15),
|
||||
rgba(139, 92, 246, 0.05)
|
||||
);
|
||||
border: 1px solid rgba(139, 92, 246, 0.3);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
@@ -96,7 +101,11 @@
|
||||
.parrainage-reward-card {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
background: linear-gradient(135deg, rgba(139, 92, 246, 0.12), rgba(99, 102, 241, 0.08));
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(139, 92, 246, 0.12),
|
||||
rgba(99, 102, 241, 0.08)
|
||||
);
|
||||
border: 1px solid rgba(139, 92, 246, 0.35);
|
||||
border-radius: 14px;
|
||||
padding: 1.5rem;
|
||||
@@ -162,7 +171,9 @@
|
||||
border-radius: 12px;
|
||||
padding: 1.25rem 1rem;
|
||||
text-align: center;
|
||||
transition: border-color 0.2s, transform 0.2s;
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
transform 0.2s;
|
||||
}
|
||||
|
||||
.step-card:hover {
|
||||
@@ -276,7 +287,9 @@
|
||||
padding: 0.8rem 1.8rem;
|
||||
border-radius: 10px;
|
||||
text-decoration: none;
|
||||
transition: background 0.2s, transform 0.15s;
|
||||
transition:
|
||||
background 0.2s,
|
||||
transform 0.15s;
|
||||
}
|
||||
|
||||
.telegram-btn:hover {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -102,7 +102,9 @@ const getDeliveryAddress = (order: OrderWithTracking): string => {
|
||||
|
||||
const formatOrderItem = (item: OrderItem) => {
|
||||
return {
|
||||
name: String(item.produit || item.product_name || item.name_product || "Produit"),
|
||||
name: String(
|
||||
item.produit || item.product_name || item.name_product || "Produit",
|
||||
),
|
||||
quantity: Number(item.quantite || item.quantity || 0),
|
||||
price: Number(item.prix || item.price || 0),
|
||||
};
|
||||
@@ -157,7 +159,6 @@ const getStatusIcon = (status: string): IconDefinition => {
|
||||
return iconMap[status?.toLowerCase()] || faQuestionCircle;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* ✅ Calculer les points avec le TOTAL (pas item par item)
|
||||
*/
|
||||
@@ -335,33 +336,35 @@ function SuiviLivraison() {
|
||||
|
||||
if (response.success) {
|
||||
const ordersWithTracking = await Promise.all(
|
||||
(response.commands || []).map(async (order: OrderDetail) => {
|
||||
const normalizedOrder = {
|
||||
...order,
|
||||
total: getTotalAmount(order),
|
||||
};
|
||||
(response.commands || []).map(
|
||||
async (order: OrderDetail) => {
|
||||
const normalizedOrder = {
|
||||
...order,
|
||||
total: getTotalAmount(order),
|
||||
};
|
||||
|
||||
let tracking;
|
||||
let eta;
|
||||
let tracking;
|
||||
let eta;
|
||||
|
||||
try {
|
||||
tracking = await getOrderTracking(order.id);
|
||||
} catch {
|
||||
tracking = undefined;
|
||||
}
|
||||
try {
|
||||
tracking = await getOrderTracking(order.id);
|
||||
} catch {
|
||||
tracking = undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
eta = await getOrderETA(order.id);
|
||||
} catch {
|
||||
eta = undefined;
|
||||
}
|
||||
try {
|
||||
eta = await getOrderETA(order.id);
|
||||
} catch {
|
||||
eta = undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
...normalizedOrder,
|
||||
tracking,
|
||||
eta,
|
||||
};
|
||||
}),
|
||||
return {
|
||||
...normalizedOrder,
|
||||
tracking,
|
||||
eta,
|
||||
};
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
setOrders(ordersWithTracking);
|
||||
@@ -369,7 +372,10 @@ function SuiviLivraison() {
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
console.error("Erreur loadOrders:", err);
|
||||
const msg = err instanceof Error ? err.message : "Erreur lors du chargement";
|
||||
const msg =
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: "Erreur lors du chargement";
|
||||
setError(msg);
|
||||
showToast(msg, "error");
|
||||
} finally {
|
||||
@@ -400,7 +406,10 @@ function SuiviLivraison() {
|
||||
const order = orders.find((o) => o.id === orderId);
|
||||
|
||||
if (order) {
|
||||
const { points, categoryDisplay } = calculateOrderPoints(order, poolNames);
|
||||
const { points, categoryDisplay } = calculateOrderPoints(
|
||||
order,
|
||||
poolNames,
|
||||
);
|
||||
setSelectedOrderPoints(points);
|
||||
setSelectedOrderCategoryDisplay(categoryDisplay);
|
||||
} else {
|
||||
@@ -549,7 +558,12 @@ function SuiviLivraison() {
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error("❌ [CANCEL] Erreur:", error);
|
||||
showToast(error instanceof Error ? error.message : "Erreur lors de l'annulation", "error");
|
||||
showToast(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Erreur lors de l'annulation",
|
||||
"error",
|
||||
);
|
||||
} finally {
|
||||
setCancellingOrder(null);
|
||||
}
|
||||
@@ -617,10 +631,19 @@ function SuiviLivraison() {
|
||||
// Heure d'arrivée estimée : depuis le backend ou calculée côté client
|
||||
const computedArrival = (() => {
|
||||
if (statusLow !== "en_route") return null;
|
||||
if (order.eta?.estimated_arrival) return order.eta.estimated_arrival;
|
||||
if (order.eta?.eta_minutes && order.eta.eta_minutes > 0) {
|
||||
return new Date(Date.now() + order.eta.eta_minutes * 60000)
|
||||
.toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" });
|
||||
if (order.eta?.estimated_arrival)
|
||||
return order.eta.estimated_arrival;
|
||||
if (
|
||||
order.eta?.eta_minutes &&
|
||||
order.eta.eta_minutes > 0
|
||||
) {
|
||||
return new Date(
|
||||
Date.now() +
|
||||
order.eta.eta_minutes * 60000,
|
||||
).toLocaleTimeString("fr-FR", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
}
|
||||
return null;
|
||||
})();
|
||||
@@ -655,7 +678,8 @@ function SuiviLivraison() {
|
||||
<div className="order-header-left">
|
||||
<div className="order-id-row">
|
||||
<span className="order-id">
|
||||
Commande #{order.client_order_number}
|
||||
Commande #
|
||||
{order.client_order_number}
|
||||
</span>
|
||||
<div
|
||||
className="status-badge"
|
||||
@@ -677,7 +701,9 @@ function SuiviLivraison() {
|
||||
</div>
|
||||
{statusLow === "en_route" && (
|
||||
<div className="eta-badge eta-badge--enRoute">
|
||||
<FontAwesomeIcon icon={faClock} />
|
||||
<FontAwesomeIcon
|
||||
icon={faClock}
|
||||
/>
|
||||
{computedArrival
|
||||
? ` Vers ${computedArrival}`
|
||||
: " Aucune heure disponible"}
|
||||
@@ -685,7 +711,9 @@ function SuiviLivraison() {
|
||||
)}
|
||||
{showArrivedEta && (
|
||||
<div className="eta-badge eta-badge--arrived">
|
||||
<FontAwesomeIcon icon={faClock} />
|
||||
<FontAwesomeIcon
|
||||
icon={faClock}
|
||||
/>
|
||||
{" ~5 min"}
|
||||
</div>
|
||||
)}
|
||||
@@ -764,40 +792,80 @@ function SuiviLivraison() {
|
||||
{showArrivedEta && (
|
||||
<div className="eta-card eta-card--arrived">
|
||||
<div className="eta-card-icon">
|
||||
<FontAwesomeIcon icon={faMapMarkerAlt} />
|
||||
<FontAwesomeIcon
|
||||
icon={
|
||||
faMapMarkerAlt
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="eta-card-body">
|
||||
<p className="eta-card-title">Livreur sur place</p>
|
||||
<p className="eta-card-value">~5 min</p>
|
||||
<p className="eta-card-sub">Préparez-vous à réceptionner votre commande</p>
|
||||
<p className="eta-card-title">
|
||||
Livreur sur place
|
||||
</p>
|
||||
<p className="eta-card-value">
|
||||
~5 min
|
||||
</p>
|
||||
<p className="eta-card-sub">
|
||||
Préparez-vous à
|
||||
réceptionner votre
|
||||
commande
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ETA card - en_route */}
|
||||
{(showEta || (statusLow === "en_route" && computedArrival)) && (
|
||||
<div
|
||||
className="eta-card eta-card--enRoute"
|
||||
>
|
||||
{(showEta ||
|
||||
(statusLow === "en_route" &&
|
||||
computedArrival)) && (
|
||||
<div className="eta-card eta-card--enRoute">
|
||||
<div className="eta-card-icon">
|
||||
<FontAwesomeIcon icon={faClock} />
|
||||
<FontAwesomeIcon
|
||||
icon={faClock}
|
||||
/>
|
||||
</div>
|
||||
<div className="eta-card-body">
|
||||
<p className="eta-card-title">Heure d'arrivée estimée</p>
|
||||
<p className="eta-card-title">
|
||||
Heure d'arrivée
|
||||
estimée
|
||||
</p>
|
||||
{computedArrival && (
|
||||
<p className="eta-card-value">{computedArrival}</p>
|
||||
)}
|
||||
{order.eta?.eta_minutes && order.eta.eta_minutes > 0 && (
|
||||
<p className="eta-card-sub">
|
||||
~{order.eta.eta_minutes} min restantes
|
||||
<p className="eta-card-value">
|
||||
{
|
||||
computedArrival
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
{order.eta?.livreur_distance != null && (
|
||||
{order.eta
|
||||
?.eta_minutes &&
|
||||
order.eta
|
||||
.eta_minutes >
|
||||
0 && (
|
||||
<p className="eta-card-sub">
|
||||
~
|
||||
{
|
||||
order
|
||||
.eta
|
||||
.eta_minutes
|
||||
}{" "}
|
||||
min
|
||||
restantes
|
||||
</p>
|
||||
)}
|
||||
{order.eta
|
||||
?.livreur_distance !=
|
||||
null && (
|
||||
<p className="eta-card-sub">
|
||||
Distance :{" "}
|
||||
{typeof order.eta.livreur_distance === "number"
|
||||
? order.eta.livreur_distance.toFixed(1)
|
||||
: order.eta.livreur_distance}{" "}
|
||||
{typeof order
|
||||
.eta
|
||||
.livreur_distance ===
|
||||
"number"
|
||||
? order.eta.livreur_distance.toFixed(
|
||||
1,
|
||||
)
|
||||
: order.eta
|
||||
.livreur_distance}{" "}
|
||||
km
|
||||
</p>
|
||||
)}
|
||||
@@ -924,19 +992,53 @@ function SuiviLivraison() {
|
||||
/>{" "}
|
||||
Montant total
|
||||
</h4>
|
||||
{(order.referral_used ?? 0) > 0 && (
|
||||
<p style={{ margin: "0 0 2px", fontSize: "0.85rem", color: "var(--text-muted)" }}>
|
||||
Brut : {(order.total_prix ?? 0).toFixed(2)} €
|
||||
{(order.referral_used ??
|
||||
0) > 0 && (
|
||||
<p
|
||||
style={{
|
||||
margin: "0 0 2px",
|
||||
fontSize:
|
||||
"0.85rem",
|
||||
color: "var(--text-muted)",
|
||||
}}
|
||||
>
|
||||
Brut :{" "}
|
||||
{(
|
||||
order.total_prix ??
|
||||
0
|
||||
).toFixed(2)}{" "}
|
||||
€
|
||||
</p>
|
||||
)}
|
||||
<p className="total-amount">
|
||||
<strong>
|
||||
{Math.max(0, getTotalAmount(order) - (order.referral_used ?? 0)).toFixed(2)} €
|
||||
{Math.max(
|
||||
0,
|
||||
getTotalAmount(
|
||||
order,
|
||||
) -
|
||||
(order.referral_used ??
|
||||
0),
|
||||
).toFixed(2)}{" "}
|
||||
€
|
||||
</strong>
|
||||
</p>
|
||||
{(order.referral_used ?? 0) > 0 && (
|
||||
<p style={{ margin: "4px 0 0", fontSize: "0.82rem", color: "#10b981", fontWeight: 500 }}>
|
||||
— dont {(order.referral_used!).toFixed(2)} € parrainage déduit
|
||||
{(order.referral_used ??
|
||||
0) > 0 && (
|
||||
<p
|
||||
style={{
|
||||
margin: "4px 0 0",
|
||||
fontSize:
|
||||
"0.82rem",
|
||||
color: "#10b981",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
— dont{" "}
|
||||
{order.referral_used!.toFixed(
|
||||
2,
|
||||
)}{" "}
|
||||
€ parrainage déduit
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -20,45 +20,37 @@
|
||||
}
|
||||
|
||||
/* ===== COMING SOON OVERLAY ===== */
|
||||
.coming-soon-overlay {
|
||||
.category-cs-overlay {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 80px 24px;
|
||||
min-height: 50vh;
|
||||
padding: 24px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.coming-soon-text {
|
||||
font-family: "Reach fill & Outline", sans-serif;
|
||||
font-size: clamp(1.5rem, 8vw, 1rem);
|
||||
.category-cs-text {
|
||||
font-size: clamp(1.6rem, 5vw, 2rem);
|
||||
font-weight: 400;
|
||||
color: #8e8fe8;
|
||||
letter-spacing: 4px;
|
||||
letter-spacing: 2px;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
text-shadow:
|
||||
0 0 10px rgba(142, 143, 232, 0.9),
|
||||
0 0 25px rgba(142, 143, 232, 0.6),
|
||||
0 0 50px rgba(142, 143, 232, 0.3);
|
||||
animation: pulse-scale 2s ease-in-out infinite;
|
||||
text-shadow: 0 0 18px rgba(142, 143, 232, 0.85);
|
||||
animation: pulse-scale 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.coming-soon-desc {
|
||||
color: rgba(142, 143, 232, 0.7);
|
||||
.category-cs-desc {
|
||||
font-size: 1rem;
|
||||
color: rgba(142, 143, 232, 0.7);
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@keyframes pulse-scale {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.35);
|
||||
}
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.12); }
|
||||
}
|
||||
|
||||
.category-filter {
|
||||
|
||||
Reference in New Issue
Block a user