mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-23 15:08:13 +02:00
Add ability to save edited images with overlays to the media library
Implement a new endpoint for uploading edited images, converting them to a JPEG blob using html2canvas, and storing them in Cloudinary. Replit-Commit-Author: Agent Replit-Commit-Session-Id: ae4037a0-2a6f-4530-9bac-79b543286bda Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/397bca8c-984f-43ff-841a-10897aeb8140/ae4037a0-2a6f-4530-9bac-79b543286bda/VEZwUqa
This commit is contained in:
1 parent
986d122528
commit
b764cdfc48
4 files changed
+167
-6
No files matched your search
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||||
import { apiRequest } from "@/lib/queryClient";
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
|
import html2canvas from "html2canvas";
|
||||||
import Sidebar from "@/components/sidebar";
|
import Sidebar from "@/components/sidebar";
|
||||||
import TopBar from "@/components/topbar";
|
import TopBar from "@/components/topbar";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
@@ -13,6 +14,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
|||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Wand2, Save, ArrowRight, Image as ImageIcon, X } from "lucide-react";
|
import { Wand2, Save, ArrowRight, Image as ImageIcon, X } from "lucide-react";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
interface Ribbon {
|
interface Ribbon {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -84,10 +86,65 @@ export default function ImageEditor() {
|
|||||||
queryKey: ["/api/media"],
|
queryKey: ["/api/media"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const previewRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
// State for generated ribbon overlay
|
// State for generated ribbon overlay
|
||||||
const [ribbonPublicId, setRibbonPublicId] = useState<string | null>(null);
|
const [ribbonPublicId, setRibbonPublicId] = useState<string | null>(null);
|
||||||
const [isGeneratingRibbon, setIsGeneratingRibbon] = useState(false);
|
const [isGeneratingRibbon, setIsGeneratingRibbon] = useState(false);
|
||||||
|
|
||||||
|
// Save edited image mutation
|
||||||
|
const saveImageMutation = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
if (!previewRef.current || !selectedMedia) {
|
||||||
|
throw new Error("Aucune image sélectionnée");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture the preview container as canvas (including CSS overlays)
|
||||||
|
const canvas = await html2canvas(previewRef.current, {
|
||||||
|
backgroundColor: null,
|
||||||
|
scale: 2, // Higher quality
|
||||||
|
useCORS: true,
|
||||||
|
allowTaint: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert canvas to blob
|
||||||
|
const blob = await new Promise<Blob>((resolve) => {
|
||||||
|
canvas.toBlob((blob) => resolve(blob!), 'image/jpeg', 0.95);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Upload to server
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', blob, `edited_${Date.now()}.jpg`);
|
||||||
|
|
||||||
|
const response = await fetch('/api/media/upload-edited', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Erreur lors de l'upload");
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
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"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Generate ribbon when settings change
|
// Generate ribbon when settings change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ribbon.enabled || !ribbon.text) {
|
if (!ribbon.enabled || !ribbon.text) {
|
||||||
@@ -591,7 +648,7 @@ export default function ImageEditor() {
|
|||||||
{selectedMedia ? (
|
{selectedMedia ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="rounded-lg overflow-hidden bg-muted aspect-square flex items-center justify-center">
|
<div className="rounded-lg overflow-hidden bg-muted aspect-square flex items-center justify-center">
|
||||||
<div className="relative w-full h-full">
|
<div ref={previewRef} className="relative w-full h-full">
|
||||||
<img
|
<img
|
||||||
src={previewUrl || selectedMedia.originalUrl}
|
src={previewUrl || selectedMedia.originalUrl}
|
||||||
alt="Aperçu"
|
alt="Aperçu"
|
||||||
@@ -645,9 +702,15 @@ export default function ImageEditor() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Button className="flex-1" variant="outline" data-testid="button-save">
|
<Button
|
||||||
|
className="flex-1"
|
||||||
|
variant="outline"
|
||||||
|
data-testid="button-save"
|
||||||
|
onClick={() => saveImageMutation.mutate()}
|
||||||
|
disabled={saveImageMutation.isPending}
|
||||||
|
>
|
||||||
<Save className="w-4 h-4 mr-2" />
|
<Save className="w-4 h-4 mr-2" />
|
||||||
Enregistrer
|
{saveImageMutation.isPending ? "Enregistrement..." : "Enregistrer"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="flex-1" data-testid="button-use-in-post">
|
<Button className="flex-1" data-testid="button-use-in-post">
|
||||||
<ArrowRight className="w-4 h-4 mr-2" />
|
<ArrowRight className="w-4 h-4 mr-2" />
|
||||||
|
|||||||
Generated
+50
@@ -61,6 +61,7 @@
|
|||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.1",
|
||||||
"framer-motion": "^11.13.1",
|
"framer-motion": "^11.13.1",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.453.0",
|
"lucide-react": "^0.453.0",
|
||||||
"memorystore": "^1.6.7",
|
"memorystore": "^1.6.7",
|
||||||
@@ -4376,6 +4377,15 @@
|
|||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-arraybuffer": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/base64-js": {
|
"node_modules/base64-js": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
@@ -4859,6 +4869,15 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-line-break": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"utrie": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cssesc": {
|
"node_modules/cssesc": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||||
@@ -5943,6 +5962,19 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html2canvas": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"css-line-break": "^2.1.0",
|
||||||
|
"text-segmentation": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/http-errors": {
|
"node_modules/http-errors": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||||
@@ -8424,6 +8456,15 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/text-segmentation": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"utrie": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/thenify": {
|
"node_modules/thenify": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||||
@@ -8680,6 +8721,15 @@
|
|||||||
"node": ">= 0.4.0"
|
"node": ">= 0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/utrie": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-arraybuffer": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vary": {
|
"node_modules/vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.1",
|
||||||
"framer-motion": "^11.13.1",
|
"framer-motion": "^11.13.1",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.453.0",
|
"lucide-react": "^0.453.0",
|
||||||
"memorystore": "^1.6.7",
|
"memorystore": "^1.6.7",
|
||||||
|
|||||||
@@ -1171,6 +1171,53 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Upload edited image with overlays
|
||||||
|
app.post("/api/media/upload-edited", requireAuth, upload.single("file"), async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!req.file) {
|
||||||
|
return res.status(400).json({ error: "No file uploaded" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = req.user as User;
|
||||||
|
const userId = user.id;
|
||||||
|
|
||||||
|
// Check if Cloudinary is configured
|
||||||
|
const cloudinaryConfig = await storage.getCloudinaryConfig(userId);
|
||||||
|
|
||||||
|
if (!cloudinaryConfig) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "Cloudinary not configured. Please configure Cloudinary in Settings first."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload edited image to Cloudinary
|
||||||
|
const uploadResult = await cloudinaryService.uploadMedia(
|
||||||
|
req.file.buffer,
|
||||||
|
`edited_${Date.now()}.jpg`,
|
||||||
|
userId,
|
||||||
|
req.file.mimetype
|
||||||
|
);
|
||||||
|
|
||||||
|
const mediaItem = await storage.createMedia({
|
||||||
|
userId,
|
||||||
|
type: "image",
|
||||||
|
cloudinaryPublicId: uploadResult.publicId,
|
||||||
|
originalUrl: uploadResult.originalUrl,
|
||||||
|
facebookFeedUrl: uploadResult.facebookFeedUrl,
|
||||||
|
instagramFeedUrl: uploadResult.instagramFeedUrl,
|
||||||
|
instagramStoryUrl: uploadResult.instagramStoryUrl,
|
||||||
|
fileName: `edited_${Date.now()}.jpg`,
|
||||||
|
fileSize: req.file.size,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json(mediaItem);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error uploading edited image:", error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : "Failed to upload edited image";
|
||||||
|
res.status(500).json({ error: errorMessage });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const httpServer = createServer(app);
|
const httpServer = createServer(app);
|
||||||
|
|
||||||
return httpServer;
|
return httpServer;
|
||||||
|
|||||||
Reference in new issue
Block a user