import { useState, useEffect, useRef } from "react"; import { Page } from "@/components/layout/app-shell"; import { PageHeader } from "@/components/layout/page-header"; import { useQuery, useMutation } from "@tanstack/react-query"; import { apiRequest, queryClient } from "@/lib/queryClient"; import { toPng } from 'html-to-image'; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Slider } from "@/components/ui/slider"; import { Badge } from "@/components/ui/badge"; import { Wand2, Save, Image as ImageIcon, X } from "lucide-react"; import { Link } from "wouter"; import { EmptyState } from "@/components/empty-state"; import { useToast } from "@/hooks/use-toast"; interface Ribbon { enabled: boolean; text: "Promo" | "Nouveauté" | "Arrivage" | "Stock limité"; color: "red" | "yellow"; position: "north_west" | "north_east"; } interface PriceBadge { enabled: boolean; price: string; color: "red" | "yellow"; position: "north_east" | "south_east" | "north_west" | "south_west"; size: number; // Font size in pixels } interface Logo { enabled: boolean; opacity: number; position: "north_east" | "south_east" | "north_west" | "south_west" | "center"; size: "small" | "medium" | "large"; } export default function ImageEditor() { const [selectedMedia, setSelectedMedia] = useState(null); const [ribbon, setRibbon] = useState({ enabled: false, text: "Promo", color: "red", position: "north_west" }); const [priceBadge, setPriceBadge] = useState({ enabled: false, price: "", color: "red", position: "north_east", size: 20 // Default font size }); const [logo, setLogo] = useState({ enabled: false, opacity: 60, position: "south_east", size: "medium" }); const [previewUrl, setPreviewUrl] = useState(""); const { data: mediaList } = useQuery({ queryKey: ["/api/media"], }); const { data: cloudinaryConfig } = useQuery({ queryKey: ['/api/cloudinary/config'], }); const previewRef = useRef(null); const { toast } = useToast(); // Save edited image mutation - Server-side processing with Sharp const saveImageMutation = useMutation({ mutationFn: async () => { if (!selectedMedia) { throw new Error("Aucune image sélectionnée"); } // Send image URL and overlay parameters to backend const payload = { imageUrl: selectedMedia.originalUrl, ribbon: ribbon.enabled ? ribbon : undefined, priceBadge: priceBadge.enabled && priceBadge.price ? priceBadge : undefined, logo: logo.enabled ? logo : undefined, }; const response = await apiRequest('POST', '/api/media/apply-overlays', payload); return response; }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/api/media"] }); toast({ title: "Image enregistrée !", description: "L'image éditée a été ajoutée à votre médiathèque.", }); }, onError: (error: Error) => { toast({ title: "Erreur", description: error.message || "Impossible d'enregistrer l'image.", variant: "destructive" }); } }); // Set preview URL from selected media useEffect(() => { if (!selectedMedia) { setPreviewUrl(""); return; } setPreviewUrl(selectedMedia.originalUrl); }, [selectedMedia, ribbon, priceBadge, logo]); return (
{/* Left Column - Controls */}
{/* Image Selection */} 1. Choisissez une image {mediaList && (mediaList as any[]).filter((m: any) => m.type === 'image').length > 0 ? (
{(mediaList as any[]) .filter((m: any) => m.type === 'image') .map((media: any) => (
setSelectedMedia(media)} data-testid={`select-image-${media.id}`} >
))}
) : ( Ouvrir la médiathèque } /> )}
{/* Ribbon Controls */} 2. Ruban promotionnel
setRibbon({ ...ribbon, enabled: checked as boolean })} data-testid="checkbox-ribbon" />
{ribbon.enabled && ( <>
setRibbon({ ...ribbon, text: value as any })} >
setRibbon({ ...ribbon, color: value as any })} >
)}
{/* Price Badge */} 3. Prix
setPriceBadge({ ...priceBadge, enabled: checked as boolean })} data-testid="checkbox-price" />
{priceBadge.enabled && ( <>
setPriceBadge({ ...priceBadge, price: e.target.value })} data-testid="input-price" />
setPriceBadge({ ...priceBadge, color: value as any })} >
{priceBadge.size}px
setPriceBadge({ ...priceBadge, size: value[0] })} min={12} max={70} step={1} data-testid="slider-price-size" />
)}
{/* Logo */} 4. Logo
setLogo({ ...logo, enabled: checked as boolean })} data-testid="checkbox-logo-enabled" />
{logo.enabled && ( <>
{logo.opacity}%
setLogo({ ...logo, opacity: value[0] })} min={0} max={100} step={5} data-testid="slider-logo-opacity" />
)}
{/* Right Column - Preview */}
Aperçu {selectedMedia ? (
Aperçu {/* CSS Triangle Ribbon Overlay - Auto-centered */} {ribbon.enabled && ribbon.text && (
{ribbon.text === "Stock limité" ? ( <> Stock
limité ) : ribbon.text}
)} {/* Price Badge Overlay (CSS) */} {priceBadge.enabled && priceBadge.price && (
{priceBadge.price} €
)} {/* Logo Overlay (CSS) */} {logo.enabled && cloudinaryConfig && (cloudinaryConfig as any).logoUrl && ( Logo )}
) : (
Choisissez une image pour voir l'aperçu
)}
); }