mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-21 14:08:10 +02:00
3 files changed
+129
-38
No files matched your search
+4
-1
@@ -1,8 +1,11 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="fr">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
|
||||||
|
<title>Social Flow - Automatisation des réseaux sociaux</title>
|
||||||
|
<meta name="description" content="Automatisez vos publications Facebook et Instagram avec Social Flow. Génération de contenu par IA, planification et publication automatique." />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Architects+Daughter&family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=Fira+Code:wght@300..700&family=Geist+Mono:wght@100..900&family=Geist:wght@100..900&family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=IBM+Plex+Sans:ital,wght@0,100..700;1,100..700&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=Lora:ital,wght@0,400..700;1,400..700&family=Merriweather:ital,opsz,wght@0,18..144,300..900;1,18..144,300..900&family=Montserrat:ital,wght@0,100..900;1,100..900&family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Outfit:wght@100..900&family=Oxanium:wght@200..800&family=Playfair+Display:ital,wght@0,400..900;1,400..900&family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,100..900;1,100..900&family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&family=Space+Grotesk:wght@300..700&family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Architects+Daughter&family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=Fira+Code:wght@300..700&family=Geist+Mono:wght@100..900&family=Geist:wght@100..900&family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=IBM+Plex+Sans:ital,wght@0,100..700;1,100..700&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=Lora:ital,wght@0,400..700;1,400..700&family=Merriweather:ital,opsz,wght@0,18..144,300..900;1,18..144,300..900&family=Montserrat:ital,wght@0,100..900;1,100..900&family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Outfit:wght@100..900&family=Oxanium:wght@200..800&family=Playfair+Display:ital,wght@0,400..900;1,400..900&family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,100..900;1,100..900&family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&family=Space+Grotesk:wght@300..700&family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
|
||||||
|
|||||||
+104
-37
@@ -1,7 +1,8 @@
|
|||||||
import { useState } from "react";
|
import { useState, useCallback } from "react";
|
||||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { Send, Sparkles, Image as ImageIcon, Calendar } from "lucide-react";
|
import { Send, Sparkles, Image as ImageIcon, Calendar, Upload } from "lucide-react";
|
||||||
|
import { useDropzone } from "react-dropzone";
|
||||||
import Sidebar from "@/components/sidebar";
|
import Sidebar from "@/components/sidebar";
|
||||||
import TopBar from "@/components/topbar";
|
import TopBar from "@/components/topbar";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -36,6 +37,51 @@ export default function NewPost() {
|
|||||||
queryKey: ['/api/media'],
|
queryKey: ['/api/media'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const uploadMutation = useMutation({
|
||||||
|
mutationFn: async (file: File) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
const response = await fetch("/api/media/upload", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error("Upload failed");
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/media"] });
|
||||||
|
setSelectedMedia(data.id);
|
||||||
|
toast({
|
||||||
|
title: "Succès",
|
||||||
|
description: "Photo téléchargée avec succès",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast({
|
||||||
|
title: "Erreur",
|
||||||
|
description: "Impossible de télécharger la photo",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||||
|
if (acceptedFiles.length > 0) {
|
||||||
|
uploadMutation.mutate(acceptedFiles[0]);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
|
||||||
|
onDrop,
|
||||||
|
accept: {
|
||||||
|
"image/*": [".png", ".jpg", ".jpeg", ".gif"],
|
||||||
|
"video/*": [".mp4", ".mov"],
|
||||||
|
},
|
||||||
|
maxSize: 52428800,
|
||||||
|
noClick: true,
|
||||||
|
noKeyboard: true,
|
||||||
|
});
|
||||||
|
|
||||||
const generateTextMutation = useMutation({
|
const generateTextMutation = useMutation({
|
||||||
mutationFn: async (productInfoText: string) => {
|
mutationFn: async (productInfoText: string) => {
|
||||||
const productInfo = {
|
const productInfo = {
|
||||||
@@ -254,44 +300,65 @@ export default function NewPost() {
|
|||||||
|
|
||||||
<Card className="rounded-2xl border-border/50 shadow-lg">
|
<Card className="rounded-2xl border-border/50 shadow-lg">
|
||||||
<CardHeader className="p-6">
|
<CardHeader className="p-6">
|
||||||
<CardTitle>Média</CardTitle>
|
<div className="flex items-center justify-between">
|
||||||
<CardDescription>Sélectionnez une image ou vidéo</CardDescription>
|
<div>
|
||||||
|
<CardTitle>Média</CardTitle>
|
||||||
|
<CardDescription>Sélectionnez ou uploadez une image/vidéo</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={open}
|
||||||
|
disabled={uploadMutation.isPending}
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
data-testid="button-upload-new-media"
|
||||||
|
>
|
||||||
|
<Upload className="w-4 h-4 mr-2" />
|
||||||
|
{uploadMutation.isPending ? 'Upload...' : 'Uploader'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{mediaList.length === 0 ? (
|
<div {...getRootProps()} className={`${isDragActive ? 'bg-primary/5 border-primary' : ''}`}>
|
||||||
<div className="text-center py-8">
|
<input {...getInputProps()} />
|
||||||
<ImageIcon className="w-12 h-12 mx-auto text-muted-foreground mb-2" />
|
{mediaList.length === 0 ? (
|
||||||
<p className="text-muted-foreground">Aucun média disponible</p>
|
<div className="text-center py-8 border-2 border-dashed rounded-lg">
|
||||||
<Button
|
<ImageIcon className="w-12 h-12 mx-auto text-muted-foreground mb-2" />
|
||||||
variant="link"
|
<p className="text-muted-foreground mb-2">
|
||||||
onClick={() => navigate('/media')}
|
{isDragActive ? "Déposez votre photo ici" : "Aucun média disponible"}
|
||||||
data-testid="link-upload-media"
|
</p>
|
||||||
>
|
<Button
|
||||||
Télécharger des médias
|
onClick={open}
|
||||||
</Button>
|
disabled={uploadMutation.isPending}
|
||||||
</div>
|
size="sm"
|
||||||
) : (
|
data-testid="button-upload-first-media"
|
||||||
<div className="grid grid-cols-3 gap-2">
|
|
||||||
{mediaList.map((media) => (
|
|
||||||
<button
|
|
||||||
key={media.id}
|
|
||||||
onClick={() => setSelectedMedia(media.id)}
|
|
||||||
className={`relative aspect-square rounded-lg overflow-hidden border-2 transition-all ${
|
|
||||||
selectedMedia === media.id
|
|
||||||
? 'border-primary ring-2 ring-primary'
|
|
||||||
: 'border-transparent hover:border-muted-foreground'
|
|
||||||
}`}
|
|
||||||
data-testid={`button-select-media-${media.id}`}
|
|
||||||
>
|
>
|
||||||
<img
|
<Upload className="w-4 h-4 mr-2" />
|
||||||
src={media.originalUrl}
|
Uploader une photo
|
||||||
alt={media.fileName}
|
</Button>
|
||||||
className="w-full h-full object-cover"
|
</div>
|
||||||
/>
|
) : (
|
||||||
</button>
|
<div className="grid grid-cols-3 gap-2">
|
||||||
))}
|
{mediaList.map((media) => (
|
||||||
</div>
|
<button
|
||||||
)}
|
key={media.id}
|
||||||
|
onClick={() => setSelectedMedia(media.id)}
|
||||||
|
className={`relative aspect-square rounded-lg overflow-hidden border-2 transition-all ${
|
||||||
|
selectedMedia === media.id
|
||||||
|
? 'border-primary ring-2 ring-primary'
|
||||||
|
: 'border-transparent hover:border-muted-foreground'
|
||||||
|
}`}
|
||||||
|
data-testid={`button-select-media-${media.id}`}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={media.originalUrl}
|
||||||
|
alt={media.fileName}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#6366f1;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#a855f7;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Background circle -->
|
||||||
|
<circle cx="50" cy="50" r="48" fill="url(#grad1)"/>
|
||||||
|
|
||||||
|
<!-- Social network icon - simplified nodes and connections -->
|
||||||
|
<circle cx="30" cy="35" r="8" fill="white" opacity="0.9"/>
|
||||||
|
<circle cx="70" cy="35" r="8" fill="white" opacity="0.9"/>
|
||||||
|
<circle cx="50" cy="65" r="8" fill="white" opacity="0.9"/>
|
||||||
|
|
||||||
|
<!-- Connecting lines -->
|
||||||
|
<line x1="30" y1="35" x2="50" y2="65" stroke="white" stroke-width="3" opacity="0.7"/>
|
||||||
|
<line x1="70" y1="35" x2="50" y2="65" stroke="white" stroke-width="3" opacity="0.7"/>
|
||||||
|
<line x1="30" y1="35" x2="70" y2="35" stroke="white" stroke-width="3" opacity="0.7"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 952 B |
Reference in new issue
Block a user