mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-12 01:36:54 +02:00
Refonte UI : Reel, programmation de Reel et éditeur d'images
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ADkrVcVFTT14hbB148HYS
This commit is contained in:
3 files changed
+695
-661
No files matched your search
@@ -13,7 +13,9 @@ 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, ArrowRight, Image as ImageIcon, X } from "lucide-react";
|
||||
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 {
|
||||
@@ -119,26 +121,111 @@ export default function ImageEditor() {
|
||||
|
||||
return (
|
||||
<Page width="wide">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-foreground flex items-center gap-3">
|
||||
<Wand2 className="w-8 h-8" />
|
||||
Éditeur d'images
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Ajoutez des rubans promotionnels, badges prix et filtres à vos images
|
||||
</p>
|
||||
</div>
|
||||
<style>{`
|
||||
/* Triangle ribbon - auto-centered text */
|
||||
.ribbon-container {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ribbon-container.north_west {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
.ribbon-container.north_east {
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
.ribbon-triangle {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.ribbon-container.north_west .ribbon-triangle {
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-width: 140px 140px 0 0;
|
||||
}
|
||||
|
||||
.ribbon-container.north_west .ribbon-triangle.red {
|
||||
border-top-color: #FF0000;
|
||||
border-right-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
border-left-color: transparent;
|
||||
}
|
||||
|
||||
.ribbon-container.north_west .ribbon-triangle.yellow {
|
||||
border-top-color: #FFC107;
|
||||
border-right-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
border-left-color: transparent;
|
||||
}
|
||||
|
||||
.ribbon-container.north_east .ribbon-triangle {
|
||||
top: 0;
|
||||
right: 0;
|
||||
border-width: 0 140px 140px 0;
|
||||
}
|
||||
|
||||
.ribbon-container.north_east .ribbon-triangle.red {
|
||||
border-top-color: transparent;
|
||||
border-right-color: #FF0000;
|
||||
border-bottom-color: transparent;
|
||||
border-left-color: transparent;
|
||||
}
|
||||
|
||||
.ribbon-container.north_east .ribbon-triangle.yellow {
|
||||
border-top-color: transparent;
|
||||
border-right-color: #FFC107;
|
||||
border-bottom-color: transparent;
|
||||
border-left-color: transparent;
|
||||
}
|
||||
|
||||
.ribbon-text {
|
||||
position: absolute;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
z-index: 11;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ribbon-container.north_west .ribbon-text {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) rotate(-45deg) translateY(-20px);
|
||||
}
|
||||
|
||||
.ribbon-container.north_east .ribbon-text {
|
||||
top: 50%;
|
||||
right: 50%;
|
||||
transform: translate(50%, -50%) rotate(45deg) translateY(-20px);
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<PageHeader
|
||||
icon={Wand2}
|
||||
title="Éditeur d'images"
|
||||
description="Ajoutez un ruban, un prix ou votre logo sur une image, puis enregistrez-la dans la médiathèque."
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-[minmax(0,5fr)_minmax(0,6fr)]">
|
||||
{/* Left Column - Controls */}
|
||||
<div className="space-y-6">
|
||||
{/* Image Selection */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<ImageIcon className="w-5 h-5" />
|
||||
Sélectionner une image
|
||||
</CardTitle>
|
||||
<CardTitle>1. Choisissez une image</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{mediaList && (mediaList as any[]).filter((m: any) => m.type === 'image').length > 0 ? (
|
||||
@@ -149,7 +236,7 @@ export default function ImageEditor() {
|
||||
<div
|
||||
key={media.id}
|
||||
className={`relative cursor-pointer rounded-lg overflow-hidden border-2 transition-all ${
|
||||
selectedMedia?.id === media.id ? 'border-primary ring-2 ring-primary' : 'border-border hover:border-primary/50'
|
||||
selectedMedia?.id === media.id ? 'border-primary ring-2 ring-primary/30' : 'border-transparent hover:border-primary/50'
|
||||
}`}
|
||||
onClick={() => setSelectedMedia(media)}
|
||||
data-testid={`select-image-${media.id}`}
|
||||
@@ -163,9 +250,17 @@ export default function ImageEditor() {
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground text-center py-8">
|
||||
Aucune image disponible. Uploadez des images dans la médiathèque.
|
||||
</p>
|
||||
<EmptyState
|
||||
compact
|
||||
icon={ImageIcon}
|
||||
title="Aucune image disponible"
|
||||
description="Importez d'abord des images dans la médiathèque."
|
||||
action={
|
||||
<Button asChild size="sm">
|
||||
<Link href="/media">Ouvrir la médiathèque</Link>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -173,7 +268,7 @@ export default function ImageEditor() {
|
||||
{/* Ribbon Controls */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Ruban promotionnel</CardTitle>
|
||||
<CardTitle>2. Ruban promotionnel</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
@@ -254,7 +349,7 @@ export default function ImageEditor() {
|
||||
{/* Price Badge */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Badge prix</CardTitle>
|
||||
<CardTitle>3. Prix</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
@@ -339,7 +434,7 @@ export default function ImageEditor() {
|
||||
{/* Logo */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Logo</CardTitle>
|
||||
<CardTitle>4. Logo</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
@@ -415,7 +510,7 @@ export default function ImageEditor() {
|
||||
<CardContent>
|
||||
{selectedMedia ? (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg overflow-hidden bg-muted aspect-square flex items-center justify-center">
|
||||
<div className="relative rounded-lg overflow-hidden bg-muted aspect-square flex items-center justify-center">
|
||||
<div ref={previewRef} className="relative">
|
||||
<img
|
||||
src={previewUrl || selectedMedia.originalUrl}
|
||||
@@ -495,7 +590,7 @@ export default function ImageEditor() {
|
||||
|
||||
<button
|
||||
onClick={() => setSelectedMedia(null)}
|
||||
className="absolute top-2 right-2 p-1.5 bg-black/50 hover:bg-black/70 rounded-full text-white transition-colors z-20"
|
||||
className="absolute top-2 right-2 p-1.5 bg-background/80 backdrop-blur hover:bg-background rounded-full text-foreground shadow transition-colors z-20"
|
||||
data-testid="button-clear-selection"
|
||||
title="Désélectionner l'image"
|
||||
>
|
||||
@@ -505,18 +600,19 @@ export default function ImageEditor() {
|
||||
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="outline"
|
||||
variant="brand"
|
||||
data-testid="button-save"
|
||||
onClick={() => saveImageMutation.mutate()}
|
||||
disabled={saveImageMutation.isPending}
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
{saveImageMutation.isPending ? "Enregistrement..." : "Enregistrer"}
|
||||
{saveImageMutation.isPending ? "Enregistrement…" : "Enregistrer dans la médiathèque"}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="aspect-square flex items-center justify-center bg-muted rounded-lg">
|
||||
<p className="text-muted-foreground">Sélectionnez une image</p>
|
||||
<div className="aspect-square flex flex-col items-center justify-center gap-2 rounded-lg border border-dashed bg-muted/40 text-sm text-muted-foreground">
|
||||
<ImageIcon className="h-8 w-8" />
|
||||
Choisissez une image pour voir l'aperçu
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
+514
-594
File diff suppressed because it is too large.
Load diff
@@ -3,7 +3,7 @@ import { Page } from "@/components/layout/app-shell";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { useLocation } from "wouter";
|
||||
import { CalendarClock, Upload, Loader2, Check, Video } from "lucide-react";
|
||||
import { CalendarClock, Upload, Loader2, Check, Link2 } from "lucide-react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
@@ -14,7 +14,8 @@ import { StoryToggle } from "@/components/reels/story-toggle";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { apiRequest, queryClient, handleUnauthorized, getErrorMessage } from "@/lib/queryClient";
|
||||
import type { SocialPage, Media } from "@shared/schema";
|
||||
import { SiFacebook, SiTiktok } from "react-icons/si";
|
||||
import { PlatformIcon } from "@/components/platform-icon";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
import { MediaThumbnail } from "@/components/media-thumbnail";
|
||||
import { DateTimePicker } from "@/components/datetime-picker";
|
||||
|
||||
@@ -110,44 +111,56 @@ export default function ScheduleReel() {
|
||||
setSelectedPages((prev) => (checked ? [...prev, id] : prev.filter((p) => p !== id)));
|
||||
};
|
||||
|
||||
const renderTargetCheckbox = (page: SocialPage) => (
|
||||
<div key={page.id} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={`schedule-reel-page-${page.id}`}
|
||||
checked={selectedPages.includes(page.id)}
|
||||
onCheckedChange={(checked) => togglePage(page.id, checked === true)}
|
||||
/>
|
||||
<label htmlFor={`schedule-reel-page-${page.id}`} className="text-sm font-medium leading-none flex-1">
|
||||
{page.pageName}
|
||||
const renderTargetCheckbox = (page: SocialPage) => {
|
||||
const checked = selectedPages.includes(page.id);
|
||||
return (
|
||||
<label
|
||||
key={page.id}
|
||||
htmlFor={`schedule-reel-page-${page.id}`}
|
||||
className={`flex cursor-pointer items-center gap-3 rounded-lg border p-3 transition-colors ${checked ? "border-primary bg-primary/5" : "hover:bg-accent"}`}
|
||||
>
|
||||
<PlatformIcon platform={page.platform} size="sm" />
|
||||
<span className="min-w-0 flex-1 truncate text-sm font-medium">{page.pageName}</span>
|
||||
<Checkbox
|
||||
id={`schedule-reel-page-${page.id}`}
|
||||
checked={checked}
|
||||
onCheckedChange={(value) => togglePage(page.id, value === true)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const sectionTitle = (n: number, title: string, done: boolean) => (
|
||||
<CardTitle className="flex items-center gap-3">
|
||||
<span
|
||||
className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-xs font-semibold ${done ? "bg-primary text-primary-foreground" : "bg-primary/10 text-primary"}`}
|
||||
>
|
||||
{done ? <Check className="h-4 w-4" /> : n}
|
||||
</span>
|
||||
{title}
|
||||
</CardTitle>
|
||||
);
|
||||
|
||||
return (
|
||||
<Page width="narrow" className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-foreground flex items-center gap-2">
|
||||
<CalendarClock className="w-8 h-8 text-primary" />
|
||||
Programmer un Reel
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Publiez une vidéo déjà montée, telle quelle, à la date et l'heure choisies
|
||||
</p>
|
||||
</div>
|
||||
<Page width="narrow">
|
||||
<PageHeader
|
||||
icon={CalendarClock}
|
||||
title="Programmer un Reel"
|
||||
description="Publiez une vidéo déjà montée, telle quelle, à la date et l'heure choisies."
|
||||
/>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* 1. Vidéo */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Video className="w-5 h-5" /> Vidéo
|
||||
</CardTitle>
|
||||
<CardDescription>Format vertical 9:16 recommandé</CardDescription>
|
||||
{sectionTitle(1, "Vidéo", !!selectedVideo)}
|
||||
<CardDescription>Format vertical 9:16 recommandé.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={`border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-colors ${
|
||||
isDragActive ? "border-primary bg-primary/5" : "border-border hover:border-primary/50"
|
||||
isDragActive ? "border-primary bg-primary/5" : "border-border bg-muted/30 hover:border-primary/50"
|
||||
}`}
|
||||
data-testid="dropzone-schedule-reel"
|
||||
>
|
||||
@@ -188,7 +201,7 @@ export default function ScheduleReel() {
|
||||
/>
|
||||
{selected && (
|
||||
<div className="absolute inset-0 bg-primary/30 flex items-center justify-center">
|
||||
<Check className="w-6 h-6 text-white" />
|
||||
<Check className="w-6 h-6 text-primary-foreground" />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
@@ -209,7 +222,8 @@ export default function ScheduleReel() {
|
||||
{/* 2. Description */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Description</CardTitle>
|
||||
{sectionTitle(2, "Description", !!description.trim())}
|
||||
<CardDescription>Texte publié avec le Reel (facultatif).</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Textarea
|
||||
@@ -225,27 +239,29 @@ export default function ScheduleReel() {
|
||||
{/* 3. Destinations et date */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Publication</CardTitle>
|
||||
{sectionTitle(3, "Destinations et date", canSchedule)}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{facebookPages.length === 0 && tiktokAccounts.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">Aucune page Facebook ou compte TikTok connecté.</p>
|
||||
<EmptyState
|
||||
compact
|
||||
icon={Link2}
|
||||
title="Aucun compte connecté"
|
||||
description="Connectez une page Facebook ou un compte TikTok pour publier."
|
||||
action={<Button size="sm" onClick={() => navigate("/pages")}>Connecter un compte</Button>}
|
||||
/>
|
||||
)}
|
||||
{facebookPages.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-2">
|
||||
<SiFacebook className="w-4 h-4 text-[#1877F2]" /> Pages Facebook
|
||||
</Label>
|
||||
{facebookPages.map(renderTargetCheckbox)}
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Pages Facebook</p>
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">{facebookPages.map(renderTargetCheckbox)}</div>
|
||||
<StoryToggle checked={alsoStory} onCheckedChange={setAlsoStory} />
|
||||
</div>
|
||||
)}
|
||||
{tiktokAccounts.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-2">
|
||||
<SiTiktok className="w-4 h-4" /> Comptes TikTok
|
||||
</Label>
|
||||
{tiktokAccounts.map(renderTargetCheckbox)}
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Comptes TikTok</p>
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">{tiktokAccounts.map(renderTargetCheckbox)}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -264,6 +280,7 @@ export default function ScheduleReel() {
|
||||
<Button
|
||||
className="w-full"
|
||||
size="lg"
|
||||
variant="brand"
|
||||
disabled={!canSchedule || scheduleMutation.isPending}
|
||||
onClick={() => scheduleMutation.mutate()}
|
||||
data-testid="button-schedule-reel"
|
||||
@@ -277,6 +294,7 @@ export default function ScheduleReel() {
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user