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",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
||||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@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",
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.1.0.tgz",
|
||||||
"integrity": "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==",
|
"integrity": "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-common-types": "7.1.0"
|
"@fortawesome/fontawesome-common-types": "7.1.0"
|
||||||
},
|
},
|
||||||
@@ -1463,7 +1461,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
||||||
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
@@ -1473,7 +1470,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
|
||||||
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
@@ -1530,7 +1526,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.0.tgz",
|
"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==",
|
"integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.48.0",
|
"@typescript-eslint/scope-manager": "8.48.0",
|
||||||
"@typescript-eslint/types": "8.48.0",
|
"@typescript-eslint/types": "8.48.0",
|
||||||
@@ -1769,7 +1764,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -1869,7 +1863,6 @@
|
|||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.25",
|
"baseline-browser-mapping": "^2.8.25",
|
||||||
"caniuse-lite": "^1.0.30001754",
|
"caniuse-lite": "^1.0.30001754",
|
||||||
@@ -2108,7 +2101,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
|
||||||
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -2677,7 +2669,6 @@
|
|||||||
"version": "1.13.2",
|
"version": "1.13.2",
|
||||||
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-1.13.2.tgz",
|
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-1.13.2.tgz",
|
||||||
"integrity": "sha512-CPjtWygL+f7naL+sGHoC2JQR0DG7u+9ik6WdkjjVmz2uy0kBC2l+aKfdi3ZzUR7VKSQJ6Mc/CeCN+6iVNah+ww==",
|
"integrity": "sha512-CPjtWygL+f7naL+sGHoC2JQR0DG7u+9ik6WdkjjVmz2uy0kBC2l+aKfdi3ZzUR7VKSQJ6Mc/CeCN+6iVNah+ww==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mapbox/geojson-rewind": "^0.5.0",
|
"@mapbox/geojson-rewind": "^0.5.0",
|
||||||
"@mapbox/geojson-types": "^1.0.2",
|
"@mapbox/geojson-types": "^1.0.2",
|
||||||
@@ -2887,7 +2878,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -2960,7 +2950,6 @@
|
|||||||
"version": "19.2.0",
|
"version": "19.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -2969,7 +2958,6 @@
|
|||||||
"version": "19.2.0",
|
"version": "19.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
||||||
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@@ -3228,7 +3216,6 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -3319,7 +3306,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.4.tgz",
|
||||||
"integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==",
|
"integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -3460,7 +3446,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz",
|
||||||
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
|
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
// ============================================
|
|
||||||
// api/api.ts - VERSION CORRIGÉE
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
const API_URL = "/api/v1";
|
const API_URL = "/api/v1";
|
||||||
const BACKEND_URL = "";
|
const BACKEND_URL = "";
|
||||||
export function getMediaUrl(url: string): string {
|
export function getMediaUrl(url: string): string {
|
||||||
@@ -26,17 +22,11 @@ import type {
|
|||||||
CancelCommandResponse,
|
CancelCommandResponse,
|
||||||
PenaltiesResponse,
|
PenaltiesResponse,
|
||||||
} from "./api_types";
|
} from "./api_types";
|
||||||
// ============================================
|
|
||||||
// 🔐 TYPES - AUTHRESPONSE COMPLETE
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ✅ TYPE CORRECT - Inclut access_token!
|
|
||||||
*/
|
|
||||||
export interface AuthResponse {
|
export interface AuthResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
message?: string;
|
message?: string;
|
||||||
access_token?: string; // ✅ CRITICAL!
|
access_token?: string;
|
||||||
token_type?: string;
|
token_type?: string;
|
||||||
expires_in?: number;
|
expires_in?: number;
|
||||||
requires_2fa?: boolean;
|
requires_2fa?: boolean;
|
||||||
@@ -53,17 +43,8 @@ export interface AuthResponse {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// 🔐 GESTION CENTRALISÉE DU JWT
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ✅ Extraire username du JWT
|
|
||||||
* Source de vérité UNIQUE pour le username
|
|
||||||
*/
|
|
||||||
export const extractUsernameFromToken = (): string | null => {
|
export const extractUsernameFromToken = (): string | null => {
|
||||||
try {
|
try {
|
||||||
// ✅ CRITICAL: sessionStorage (pas localStorage!)
|
|
||||||
const token = sessionStorage.getItem("token");
|
const token = sessionStorage.getItem("token");
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
@@ -2152,3 +2133,84 @@ export const unlinkTelegram = async (): Promise<void> => {
|
|||||||
/* silencieux */
|
/* silencieux */
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 🏆 POINTS — RÉCOMPENSES
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export type RewardCategoryConfig = {
|
||||||
|
category: string;
|
||||||
|
all_products: boolean;
|
||||||
|
product_ids: number[];
|
||||||
|
amount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PointsPoolInfo = {
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
points: number;
|
||||||
|
rewards_earned: number;
|
||||||
|
rewards_claimed: number;
|
||||||
|
rewards_available: number;
|
||||||
|
eligible_configs: RewardCategoryConfig[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PointsRewardConfig = {
|
||||||
|
threshold: number;
|
||||||
|
type: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMyPointsRewards = async (): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
enabled: boolean;
|
||||||
|
pools: PointsPoolInfo[];
|
||||||
|
reward: PointsRewardConfig | null;
|
||||||
|
}> => {
|
||||||
|
const token = getAuthToken();
|
||||||
|
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 };
|
||||||
|
const data = await safeJson(response);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
enabled: data.enabled ?? false,
|
||||||
|
pools: data.pools ?? [],
|
||||||
|
reward: data.reward ?? null,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { success: false, enabled: false, pools: [], reward: null };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const claimMyReward = async (poolKey: string): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
description?: string;
|
||||||
|
remaining_rewards?: number;
|
||||||
|
error?: string;
|
||||||
|
}> => {
|
||||||
|
const token = getAuthToken();
|
||||||
|
if (!token) return { success: false, error: "Non authentifié" };
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/points/claim`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ pool_key: poolKey }),
|
||||||
|
});
|
||||||
|
const data = await safeJson(response);
|
||||||
|
if (!response.ok) return { success: false, error: data.error || "Erreur" };
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
description: data.description,
|
||||||
|
remaining_rewards: data.remaining_rewards,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { success: false, error: "Erreur de connexion" };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,13 +4,6 @@
|
|||||||
// Interfaces complètes pour le frontend
|
// Interfaces complètes pour le frontend
|
||||||
// À importer dans les composants
|
// À importer dans les composants
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// 🔐 AUTHENTIFICATION - TYPES
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Réponse générique de l'API
|
|
||||||
*/
|
|
||||||
export interface ApiResponse {
|
export interface ApiResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
message?: string;
|
message?: string;
|
||||||
@@ -19,8 +12,7 @@ export interface ApiResponse {
|
|||||||
token_type?: string;
|
token_type?: string;
|
||||||
expires_in?: number;
|
expires_in?: number;
|
||||||
user?: UserResponse;
|
user?: UserResponse;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
[key: string]: unknown;
|
||||||
[key: string]: any; // Pour les champs additionnels
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -779,6 +771,6 @@ export interface ConfirmReceptionResponse {
|
|||||||
category?: string;
|
category?: string;
|
||||||
points_earned?: number;
|
points_earned?: number;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,6 +181,15 @@
|
|||||||
margin-top: clamp(1rem, 3vw, 1.5rem);
|
margin-top: clamp(1rem, 3vw, 1.5rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.summary-referral-deduction {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: clamp(0.85rem, 2.5vw, 0.95rem);
|
||||||
|
color: #16a34a;
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
.total-price {
|
.total-price {
|
||||||
color: #6d28d9;
|
color: #6d28d9;
|
||||||
font-size: clamp(1.2rem, 5vw, 1.5rem);
|
font-size: clamp(1.2rem, 5vw, 1.5rem);
|
||||||
|
|||||||
@@ -417,8 +417,19 @@ function Checkout() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="summary-total">
|
<div className="summary-total">
|
||||||
<span>Total:</span>
|
<span>Total:</span>
|
||||||
<span className="total-price">{total.toFixed(2)} €</span>
|
<span className="total-price">
|
||||||
|
{(useReferral && referralBalance > 0
|
||||||
|
? Math.max(0, total - referralBalance)
|
||||||
|
: total
|
||||||
|
).toFixed(2)} €
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{useReferral && referralBalance > 0 && (
|
||||||
|
<div className="summary-referral-deduction">
|
||||||
|
<span>Dont crédit parrainage :</span>
|
||||||
|
<span>-{Math.min(referralBalance, total).toFixed(2)} €</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Formulaire */}
|
{/* Formulaire */}
|
||||||
@@ -787,7 +798,9 @@ function Checkout() {
|
|||||||
<div className="confirmation-total-label">
|
<div className="confirmation-total-label">
|
||||||
<i className="fas fa-euro-sign"></i> TOTAL
|
<i className="fas fa-euro-sign"></i> TOTAL
|
||||||
</div>
|
</div>
|
||||||
<div className="confirmation-total-amount">{confirmationData.total.toFixed(2)} €</div>
|
<div className="confirmation-total-amount">
|
||||||
|
{(confirmationData.total - (confirmationData.referral_used ?? 0)).toFixed(2)} €
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -151,6 +151,154 @@
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
RÉCOMPENSES PAR PALIER
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.rewards-section {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rewards-section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: #f59e0b;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.8px;
|
||||||
|
margin: 0 0 0.4rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rewards-section-icon {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rewards-section-desc {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-style: italic;
|
||||||
|
margin: 0 0 0.875rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rewards-pools {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-pool-card {
|
||||||
|
background: rgba(245, 158, 11, 0.07);
|
||||||
|
border: 1px solid rgba(245, 158, 11, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-pool-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-pool-name {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-pool-pts {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-eligible-cats {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.35rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-eligible-cat {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #10b981;
|
||||||
|
background: rgba(16, 185, 129, 0.12);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-progress-bar {
|
||||||
|
height: 6px;
|
||||||
|
background: rgba(245, 158, 11, 0.15);
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #f59e0b, #fbbf24);
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-pool-info {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-available {
|
||||||
|
color: #10b981;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-remaining {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-feedback {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-style: italic;
|
||||||
|
margin: 0.25rem 0 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-feedback-success {
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-feedback-error {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-claim-btn {
|
||||||
|
width: 100%;
|
||||||
|
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.55rem 1rem;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-claim-btn:hover:not(:disabled) {
|
||||||
|
opacity: 0.88;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-claim-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
SECTION TITLE
|
SECTION TITLE
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ import {
|
|||||||
isUserAuthenticated,
|
isUserAuthenticated,
|
||||||
getPublicSettings,
|
getPublicSettings,
|
||||||
getReferralBalance,
|
getReferralBalance,
|
||||||
|
getMyPointsRewards,
|
||||||
|
claimMyReward,
|
||||||
} from "../../api/api";
|
} from "../../api/api";
|
||||||
import type { PublicSettings } from "../../api/api";
|
import type { PublicSettings, PointsPoolInfo, PointsRewardConfig } from "../../api/api";
|
||||||
import type {
|
import type {
|
||||||
CompletedOrder,
|
CompletedOrder,
|
||||||
ClientStats,
|
ClientStats,
|
||||||
@@ -59,6 +61,13 @@ function ConsultationHistorique() {
|
|||||||
two_fa_enabled: false,
|
two_fa_enabled: false,
|
||||||
});
|
});
|
||||||
const [referralBalance, setReferralBalance] = useState<number>(0);
|
const [referralBalance, setReferralBalance] = useState<number>(0);
|
||||||
|
const [pointsRewards, setPointsRewards] = useState<{
|
||||||
|
enabled: boolean;
|
||||||
|
pools: PointsPoolInfo[];
|
||||||
|
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 [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState<string>("");
|
const [error, setError] = useState<string>("");
|
||||||
|
|
||||||
@@ -86,6 +95,9 @@ function ConsultationHistorique() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
getMyPointsRewards().then((r) => {
|
||||||
|
if (r.success && r.enabled) setPointsRewards(r);
|
||||||
|
});
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const fetchHistory = async () => {
|
const fetchHistory = async () => {
|
||||||
@@ -157,6 +169,19 @@ function ConsultationHistorique() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleClaim = async (poolKey: string) => {
|
||||||
|
setClaimingPool(poolKey);
|
||||||
|
setClaimFeedback(null);
|
||||||
|
const res = await claimMyReward(poolKey);
|
||||||
|
setClaimingPool(null);
|
||||||
|
if (res.success) {
|
||||||
|
setClaimFeedback({ pool: poolKey, type: "success", text: res.description || "Récompense réclamée !" });
|
||||||
|
getMyPointsRewards().then((r) => { if (r.success) setPointsRewards(r); });
|
||||||
|
} else {
|
||||||
|
setClaimFeedback({ pool: poolKey, type: "error", text: res.error || "Erreur" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const poolNames = clientStats?.pool_names?.length
|
const poolNames = clientStats?.pool_names?.length
|
||||||
? clientStats.pool_names
|
? clientStats.pool_names
|
||||||
: appSettings.pool_names;
|
: appSettings.pool_names;
|
||||||
@@ -268,6 +293,65 @@ function ConsultationHistorique() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
||||||
|
<p className="rewards-section-desc">{pointsRewards.reward.description}</p>
|
||||||
|
<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.map((cfg) => (
|
||||||
|
<span key={cfg.category} className="reward-eligible-cat">
|
||||||
|
{cfg.category}{cfg.amount > 0 ? ` — ${cfg.amount}€` : ""}
|
||||||
|
</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>
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Bouton parrainage */}
|
{/* Bouton parrainage */}
|
||||||
{appSettings.referral_enabled && (
|
{appSettings.referral_enabled && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user