chore: add new features

This commit is contained in:
2026-03-05 19:09:16 +01:00
parent 532584edb2
commit 5ca480ea0b
23 changed files with 695 additions and 284 deletions
+18 -3
View File
@@ -6,6 +6,13 @@
// ✅ sessionStorage (pas localStorage)
const API_URL = "https://uber-stup.club/api/v1";
const BACKEND_URL = "https://uber-stup.club";
export function getMediaUrl(url: string): string {
if (!url) return "";
if (url.startsWith("http")) return url;
return `${BACKEND_URL}${url}`;
}
import type {
ConfirmReceptionResponse,
CheckoutCartResponse,
@@ -291,14 +298,21 @@ export const changePassword = async (
if (!response.ok) {
return {
success: false,
message: data.error || data.message || "Erreur lors du changement de mot de passe",
message:
data.error ||
data.message ||
"Erreur lors du changement de mot de passe",
};
}
return { success: true, message: data.message || "Mot de passe mis à jour" };
return {
success: true,
message: data.message || "Mot de passe mis à jour",
};
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : "Erreur de connexion",
message:
error instanceof Error ? error.message : "Erreur de connexion",
};
}
};
@@ -763,6 +777,7 @@ export interface Product {
name: string;
description?: string;
category: string;
unit?: string;
stock: number;
prices?: Array<{ quantity: number; price: number }>;
media?: MediaItem[]; // ✅ CHANGÉ: string[] → MediaItem[]
+1
View File
@@ -365,6 +365,7 @@ export interface Product {
description?: string;
category: string;
stock: number;
unit?: string;
prices?: ProductPrice[];
media?: Array<{
// ✅ CHANGÉ
+2 -1
View File
@@ -20,6 +20,7 @@ function ProductCard({
id,
name,
price,
unit = "g",
image,
stock,
category = "autre",
@@ -180,7 +181,7 @@ function ProductCard({
key={priceOption.quantity}
value={priceOption.quantity}
>
{priceOption.quantity}g -{" "}
{priceOption.quantity}{unit} -{" "}
{priceOption.price.toFixed(2)}
</option>
))}
@@ -19,6 +19,7 @@ import {
Play,
} from "lucide-react";
import type { Product, ProductPrice } from "../api/api_types";
import { getMediaUrl } from "../api/api";
import "./ProductModal.css";
interface ProductDetailsModalProps {
@@ -114,14 +115,14 @@ const ProductDetailsModal: React.FC<ProductDetailsModalProps> = ({
<div className="media-viewer">
{currentMedia?.type === "image" ? (
<img
src={`${currentMedia.url}`}
src={getMediaUrl(currentMedia.url)}
alt={product.name}
className="media-display"
/>
) : currentMedia?.type === "video" ? (
<div className="video-container">
<video
src={`${currentMedia.url}`}
src={getMediaUrl(currentMedia.url)}
controls
className="media-display"
>
@@ -179,7 +180,7 @@ const ProductDetailsModal: React.FC<ProductDetailsModalProps> = ({
>
{media.type === "image" ? (
<img
src={`{product.media[0].url}${media.url}`}
src={getMediaUrl(media.url)}
alt={`Media ${index + 1}`}
/>
) : (
+21 -17
View File
@@ -1,7 +1,7 @@
import { useState, useEffect } from "react";
import ProductCard from "../../components/ProductCard";
import Navbar from "../../components/Navbar";
import { getAllProducts, getProductsByCategory } from "../../api/api";
import { getAllProducts, getProductsByCategory, getMediaUrl } from "../../api/api";
import type { Product } from "../../api/api";
import "./UserAccueil.css";
@@ -85,7 +85,7 @@ function UserAccueil() {
(mediaItem) => mediaItem && mediaItem.type === "video",
);
return videoMedia?.url ? `${videoMedia.url}` : undefined;
return videoMedia?.url ? getMediaUrl(videoMedia.url) : undefined;
};
const getProductImage = (product: Product): string => {
// Pas de media ? Image placeholder
@@ -99,7 +99,7 @@ function UserAccueil() {
// Vérifier si c'est un objet avec type "image" et url
if (mediaItem && mediaItem.type === "image" && mediaItem.url) {
return `${mediaItem.url}`;
return getMediaUrl(mediaItem.url);
}
}
@@ -107,10 +107,21 @@ function UserAccueil() {
return "https://via.placeholder.com/400x400/1a1a1a/ffffff?text=No+Image";
};
const grosSemiStyle =
selectedCategory === "gros&semi"
? {
backgroundImage: `url('/logo-gros-semi.png')`,
backgroundRepeat: "no-repeat",
backgroundPosition: "center center",
backgroundSize: "contain",
backgroundAttachment: "local",
}
: {};
return (
<>
<Navbar />
<div className="user-page-container">
<div className="user-page-container" style={grosSemiStyle}>
<div className="category-filter">
{categories.map((category) => (
<button
@@ -136,18 +147,6 @@ function UserAccueil() {
</div>
)}
{error && (
<div className="error-container">
<p className="error-message">{error}</p>
</div>
)}
{!loading && !error && products.length === 0 && (
<div className="empty-container">
<p>Aucun produit disponible dans cette catégorie.</p>
</div>
)}
{!loading && !error && products.length > 0 && (
<div className="products-grid">
{products.map((product) => (
@@ -159,7 +158,7 @@ function UserAccueil() {
id={product.id}
name={product.name}
price={getProductPrice(product)}
unit="g"
unit={product.unit || "g"}
image={getProductImage(product)}
stock={product.stock}
category={product.category}
@@ -171,6 +170,11 @@ function UserAccueil() {
))}
</div>
)}
{selectedCategory === "gros&semi" && (
<div className="coming-soon-overlay">
<span className="coming-soon-text">Prochainement</span>
</div>
)}
</div>
</>
);
+3 -3
View File
@@ -11,7 +11,7 @@ import { useCart } from "../../context/CartContext";
import Navbar from "../../components/Navbar";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { isUserAuthenticated, getProductById } from "../../api/api";
import { isUserAuthenticated, getProductById, getMediaUrl } from "../../api/api";
import type { Product } from "../../api/api";
import { Trash2, ShoppingBag, AlertTriangle } from "lucide-react";
import "./Cart.css";
@@ -111,7 +111,7 @@ function Cart() {
for (let i = 0; i < product.media.length; i++) {
const mediaItem = product.media[i];
if (mediaItem && mediaItem.type === "image" && mediaItem.url) {
return `${mediaItem.url}`;
return getMediaUrl(mediaItem.url);
}
}
@@ -136,7 +136,7 @@ function Cart() {
(mediaItem) => mediaItem && mediaItem.type === "video",
);
return videoMedia?.url ? `${videoMedia.url}` : undefined;
return videoMedia?.url ? getMediaUrl(videoMedia.url) : undefined;
};
// ============================================
@@ -14,6 +14,7 @@ import {
getCommandItemsWithDetails,
isUserAuthenticated,
getProductById,
getMediaUrl,
} from "../../api/api";
import type { CompletedOrder, Product } from "../../api/api_types";
import {
@@ -117,7 +118,7 @@ function OrderDetails() {
for (let i = 0; i < product.media.length; i++) {
const mediaItem = product.media[i];
if (mediaItem && mediaItem.type === "image" && mediaItem.url) {
return `${mediaItem.url}`;
return getMediaUrl(mediaItem.url);
}
}
@@ -142,7 +143,7 @@ function OrderDetails() {
(mediaItem) => mediaItem && mediaItem.type === "video",
);
return videoMedia?.url ? `${videoMedia.url}` : undefined;
return videoMedia?.url ? getMediaUrl(videoMedia.url) : undefined;
};
// HANDLERS VIDÉO
@@ -148,7 +148,7 @@ function ProductDetail() {
// ✅ Toast de succès
setToast({
show: true,
message: `${product.name} (${selectedGrams}g) ajouté au panier !`,
message: `${product.name} (${selectedGrams}${product.unit || "g"}) ajouté au panier !`,
type: "success",
});
};
@@ -214,7 +214,7 @@ function ProductDetail() {
{selectedPrice > 0 && (
<p className="product-detail-price">
{selectedPrice.toFixed(2)} {" "}
{selectedGrams && `pour ${selectedGrams}g`}
{selectedGrams && `pour ${selectedGrams}${product.unit || "g"}`}
</p>
)}
@@ -252,7 +252,7 @@ function ProductDetail() {
key={p.quantity}
value={p.quantity}
>
{p.quantity}g -{" "}
{p.quantity}{product.unit || "g"} -{" "}
{p.price.toFixed(2)}
</option>
))}
+261 -211
View File
@@ -1,319 +1,369 @@
.user-page-container {
width: 100%;
min-height: 100vh;
padding: clamp(1rem, 3vw, 1.5rem);
padding-top: calc(60px + clamp(1rem, 3vw, 1.5rem));
box-sizing: border-box;
overflow-x: hidden;
margin-top: 0;
background-color: #1a1a1a;
@font-face {
font-family: "Reach fill & Outline";
src:
url("/fonts/reach-fill-outline.woff2") format("woff2"),
url("/fonts/reach-fill-outline.woff") format("woff");
font-weight: normal;
font-style: normal;
}
.user-page-container {
width: 100%;
min-height: 100vh;
padding: clamp(1rem, 3vw, 1.5rem);
padding-top: calc(60px + clamp(1rem, 3vw, 1.5rem));
box-sizing: border-box;
overflow-x: hidden;
margin-top: 0;
background-color: #1a1a1a;
position: relative;
}
/* ===== COMING SOON OVERLAY (Gros&Semi) ===== */
.coming-soon-overlay {
position: fixed;
inset: 0;
display: flex;
justify-content: center;
align-items: flex-end;
padding-bottom: 12vh;
pointer-events: none;
z-index: 10;
}
.coming-soon-text {
font-family: "Reach fill & Outline", sans-serif;
font-size: clamp(1.5rem, 8vw, 0rem);
font-weight: 400;
color: #8e8fe8;
letter-spacing: 4px;
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;
}
@keyframes pulse-scale {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.35);
}
}
.category-filter {
display: flex;
gap: clamp(0.5rem, 2vw, 0.8rem);
overflow-x: auto;
padding-bottom: clamp(1rem, 3vw, 1.5rem);
margin-bottom: clamp(1rem, 3vw, 1.5rem);
scrollbar-width: none;
-ms-overflow-style: none;
display: flex;
gap: clamp(0.5rem, 2vw, 0.8rem);
overflow-x: auto;
padding-bottom: clamp(1rem, 3vw, 1.5rem);
margin-bottom: clamp(1rem, 3vw, 1.5rem);
scrollbar-width: none;
-ms-overflow-style: none;
}
.category-filter::-webkit-scrollbar {
display: none;
display: none;
}
.category-button {
background-color: transparent;
color: white;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 25px;
padding: clamp(0.6rem, 2vw, 0.8rem) clamp(1.2rem, 3vw, 1.5rem);
font-size: clamp(0.9rem, 3vw, 1rem);
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
-webkit-tap-highlight-color: transparent;
flex-shrink: 0;
background-color: transparent;
color: white;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 25px;
padding: clamp(0.6rem, 2vw, 0.8rem) clamp(1.2rem, 3vw, 1.5rem);
font-size: clamp(0.9rem, 3vw, 1rem);
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
-webkit-tap-highlight-color: transparent;
flex-shrink: 0;
}
.category-button:active {
transform: scale(0.95);
transform: scale(0.95);
}
.category-button.active {
background-color: white;
color: black;
border-color: white;
background-color: white;
color: black;
border-color: white;
}
/* Effets néon par catégorie - BOUTONS */
.category-button.active[data-category="tous"] {
background-color: #9333ea;
color: white;
border-color: #9333ea;
background-color: #9333ea;
color: white;
border-color: #9333ea;
}
.category-button.active[data-category="weed&hash"] {
background-color: #10b981;
color: white;
border-color: #10b981;
background-color: #10b981;
color: white;
border-color: #10b981;
}
.category-button.active[data-category="zipette&co"] {
background-color: #F5F5F0;
color: black;
border-color: #F5F5F0;
background-color: #f5f5f0;
color: black;
border-color: #f5f5f0;
}
.category-button.active[data-category="gros&semi"] {
background-color: #3dc2f7;
color: white;
border-color: #3dc2f7;
background-color: #3dc2f7;
color: white;
border-color: #3dc2f7;
}
/* ===== CATEGORY HEADER ===== */
.category-header {
margin-bottom: clamp(2rem, 5vw, 3rem);
animation: headerFadeIn 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
margin-bottom: clamp(2rem, 5vw, 3rem);
animation: headerFadeIn 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
@keyframes headerFadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.category-title {
font-size: clamp(1.8rem, 5vw, 2.8rem);
font-weight: 700;
color: white;
margin: 0 0 0.8rem 0;
letter-spacing: -0.5px;
background: linear-gradient(135deg, #ffffff 0%, rgba(255, 255, 255, 0.8) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-size: clamp(1.8rem, 5vw, 2.8rem);
font-weight: 700;
color: white;
margin: 0 0 0.8rem 0;
letter-spacing: -0.5px;
background: linear-gradient(
135deg,
#ffffff 0%,
rgba(255, 255, 255, 0.8) 100%
);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.category-subtitle {
font-size: clamp(0.95rem, 3vw, 1.1rem);
color: rgba(255, 255, 255, 0.65);
margin: 0;
font-weight: 400;
letter-spacing: 0.3px;
line-height: 1.5;
font-size: clamp(0.95rem, 3vw, 1.1rem);
color: rgba(255, 255, 255, 0.65);
margin: 0;
font-weight: 400;
letter-spacing: 0.3px;
line-height: 1.5;
}
/* Carrousel de produits - un produit à la fois */
.products-grid {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
gap: clamp(1rem, 3vw, 1.5rem);
width: 100%;
max-width: 1200px;
padding: 0 clamp(1rem, 3vw, 1.5rem);
padding-bottom: 1rem;
scrollbar-width: none;
-ms-overflow-style: none;
-webkit-overflow-scrolling: touch;
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
gap: clamp(1rem, 3vw, 1.5rem);
width: 100%;
max-width: 1200px;
padding: 0 clamp(1rem, 3vw, 1.5rem);
padding-bottom: 1rem;
scrollbar-width: none;
-ms-overflow-style: none;
-webkit-overflow-scrolling: touch;
}
.products-grid::-webkit-scrollbar {
display: none;
display: none;
}
.products-grid > * {
flex: 0 0 calc(100% - clamp(2rem, 6vw, 3rem));
scroll-snap-align: center;
scroll-snap-stop: always;
flex: 0 0 calc(100% - clamp(2rem, 6vw, 3rem));
scroll-snap-align: center;
scroll-snap-stop: always;
}
/* Effets néon par catégorie - CONTAINERS DE PRODUITS */
/* Catégorie: tous - VIOLET - Effet néon amélioré */
.products-grid > div[data-category="tous"] .product-card {
border: 2px solid #9333ea;
box-shadow:
0 0 20px rgba(147, 51, 234, 0.6),
0 0 40px rgba(147, 51, 234, 0.4),
0 0 60px rgba(147, 51, 234, 0.2),
0 0 80px rgba(147, 51, 234, 0.1);
border: 2px solid #9333ea;
box-shadow:
0 0 20px rgba(147, 51, 234, 0.6),
0 0 40px rgba(147, 51, 234, 0.4),
0 0 60px rgba(147, 51, 234, 0.2),
0 0 80px rgba(147, 51, 234, 0.1);
}
/* Catégorie: weed&hash - VERT - Effet néon amélioré */
.products-grid > div[data-category="weed&hash"] .product-card {
border: 2px solid #10b981;
box-shadow:
0 0 20px rgba(16, 185, 129, 0.6),
0 0 40px rgba(16, 185, 129, 0.4),
0 0 60px rgba(16, 185, 129, 0.2),
0 0 80px rgba(16, 185, 129, 0.1);
border: 2px solid #10b981;
box-shadow:
0 0 20px rgba(16, 185, 129, 0.6),
0 0 40px rgba(16, 185, 129, 0.4),
0 0 60px rgba(16, 185, 129, 0.2),
0 0 80px rgba(16, 185, 129, 0.1);
}
/* Catégorie: zipette&co - BLANC CASSÉ - Effet néon amélioré */
.products-grid > div[data-category="zipette&co"] .product-card {
border: 2px solid #F5F5F0;
box-shadow:
0 0 20px rgba(245, 245, 240, 0.6),
0 0 40px rgba(245, 245, 240, 0.4),
0 0 60px rgba(245, 245, 240, 0.2),
0 0 80px rgba(245, 245, 240, 0.1);
border: 2px solid #f5f5f0;
box-shadow:
0 0 20px rgba(245, 245, 240, 0.6),
0 0 40px rgba(245, 245, 240, 0.4),
0 0 60px rgba(245, 245, 240, 0.2),
0 0 80px rgba(245, 245, 240, 0.1);
}
/* Catégorie: gros&semi - BLEU CIEL - Effet néon amélioré */
.products-grid > div[data-category="gros&semi"] .product-card {
border: 2px solid #3dc2f7;
box-shadow:
0 0 20px rgba(61, 194, 247, 0.6),
0 0 40px rgba(61, 194, 247, 0.4),
0 0 60px rgba(61, 194, 247, 0.2),
0 0 80px rgba(61, 194, 247, 0.1);
border: 2px solid #3dc2f7;
box-shadow:
0 0 20px rgba(61, 194, 247, 0.6),
0 0 40px rgba(61, 194, 247, 0.4),
0 0 60px rgba(61, 194, 247, 0.2),
0 0 80px rgba(61, 194, 247, 0.1);
}
/* Petits téléphones */
@media (max-width: 360px) {
.products-grid {
padding: 0 0.8rem;
padding-bottom: 1rem;
}
.products-grid > * {
flex: 0 0 calc(100% - 1.6rem);
}
.user-page-container {
padding: 0.8rem;
padding-top: calc(60px + 0.8rem);
}
.products-grid {
padding: 0 0.8rem;
padding-bottom: 1rem;
}
.category-title {
font-size: 1.6rem;
}
.products-grid > * {
flex: 0 0 calc(100% - 1.6rem);
}
.category-subtitle {
font-size: 0.9rem;
}
.user-page-container {
padding: 0.8rem;
padding-top: calc(60px + 0.8rem);
}
.category-title {
font-size: 1.6rem;
}
.category-subtitle {
font-size: 0.9rem;
}
}
/* Tablettes portrait */
@media (min-width: 600px) {
.products-grid {
padding: 0 2rem;
padding-bottom: 1rem;
}
.products-grid > * {
flex: 0 0 calc(100% - 4rem);
}
.user-page-container {
padding: 2rem;
padding-top: calc(60px + 2rem);
}
.products-grid {
padding: 0 2rem;
padding-bottom: 1rem;
}
.products-grid > * {
flex: 0 0 calc(100% - 4rem);
}
.user-page-container {
padding: 2rem;
padding-top: calc(60px + 2rem);
}
}
/* Tablettes paysage et desktop */
@media (min-width: 900px) {
.products-grid {
padding: 0 2rem;
padding-bottom: 1rem;
}
.products-grid > * {
flex: 0 0 calc(50% - 2rem);
}
.products-grid {
padding: 0 2rem;
padding-bottom: 1rem;
}
.products-grid > * {
flex: 0 0 calc(50% - 2rem);
}
}
/* Désactiver hover sur tactile */
@media (hover: none) {
.category-button:hover {
background-color: transparent;
color: white;
}
.category-button.active:hover {
background-color: white;
color: black;
}
.category-button.active[data-category="tous"]:hover {
background-color: #9333ea;
color: white;
}
.category-button.active[data-category="weed&hash"]:hover {
background-color: #10b981;
color: white;
}
.category-button.active[data-category="zipette&co"]:hover {
background-color: #F5F5F0;
color: white;
}
.category-button.active[data-category="gros&semi"]:hover {
background-color: #3dc2f7;
color: white;
}
.category-button.active[data-category="festif"]:hover {
background-color: #9333ea;
color: white;
}
.category-button:hover {
background-color: transparent;
color: white;
}
.category-button.active:hover {
background-color: white;
color: black;
}
.category-button.active[data-category="tous"]:hover {
background-color: #9333ea;
color: white;
}
.category-button.active[data-category="weed&hash"]:hover {
background-color: #10b981;
color: white;
}
.category-button.active[data-category="zipette&co"]:hover {
background-color: #f5f5f0;
color: white;
}
.category-button.active[data-category="gros&semi"]:hover {
background-color: #3dc2f7;
color: white;
}
.category-button.active[data-category="festif"]:hover {
background-color: #9333ea;
color: white;
}
}
.loading-container,
.error-container,
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
text-align: center;
min-height: 300px;
color: white;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
text-align: center;
min-height: 300px;
color: white;
}
.loading-container p,
.empty-container p {
font-size: 1.1rem;
color: rgba(255, 255, 255, 0.8);
font-size: 1.1rem;
color: rgba(255, 255, 255, 0.8);
}
.error-message {
color: #ff4444;
margin-bottom: 1rem;
font-size: 1.1rem;
font-weight: 500;
color: #ff4444;
margin-bottom: 1rem;
font-size: 1.1rem;
font-weight: 500;
}
.error-container button {
padding: 0.75rem 1.5rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: all 0.3s ease;
padding: 0.75rem 1.5rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: all 0.3s ease;
}
.error-container button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.error-container button:active {
transform: translateY(0);
}
transform: translateY(0);
}