mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-11 17:26:45 +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 { useQuery } from "@tanstack/react-query";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||
import html2canvas from "html2canvas";
|
||||
import Sidebar from "@/components/sidebar";
|
||||
import TopBar from "@/components/topbar";
|
||||
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 { Badge } from "@/components/ui/badge";
|
||||
import { Wand2, Save, ArrowRight, Image as ImageIcon, X } from "lucide-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
interface Ribbon {
|
||||
enabled: boolean;
|
||||
@@ -84,10 +86,65 @@ export default function ImageEditor() {
|
||||
queryKey: ["/api/media"],
|
||||
});
|
||||
|
||||
const previewRef = useRef<HTMLDivElement>(null);
|
||||
const { toast } = useToast();
|
||||
|
||||
// State for generated ribbon overlay
|
||||
const [ribbonPublicId, setRibbonPublicId] = useState<string | null>(null);
|
||||
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
|
||||
useEffect(() => {
|
||||
if (!ribbon.enabled || !ribbon.text) {
|
||||
@@ -591,7 +648,7 @@ export default function ImageEditor() {
|
||||
{selectedMedia ? (
|
||||
<div className="space-y-4">
|
||||
<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
|
||||
src={previewUrl || selectedMedia.originalUrl}
|
||||
alt="Aperçu"
|
||||
@@ -645,9 +702,15 @@ export default function ImageEditor() {
|
||||
</div>
|
||||
|
||||
<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" />
|
||||
Enregistrer
|
||||
{saveImageMutation.isPending ? "Enregistrement..." : "Enregistrer"}
|
||||
</Button>
|
||||
<Button className="flex-1" data-testid="button-use-in-post">
|
||||
<ArrowRight className="w-4 h-4 mr-2" />
|
||||
|
||||
Generated
+50
@@ -61,6 +61,7 @@
|
||||
"express": "^4.21.2",
|
||||
"express-session": "^1.18.1",
|
||||
"framer-motion": "^11.13.1",
|
||||
"html2canvas": "^1.4.1",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.453.0",
|
||||
"memorystore": "^1.6.7",
|
||||
@@ -4376,6 +4377,15 @@
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"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": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
@@ -4859,6 +4869,15 @@
|
||||
"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": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
@@ -5943,6 +5962,19 @@
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
@@ -8424,6 +8456,15 @@
|
||||
"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": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||
@@ -8680,6 +8721,15 @@
|
||||
"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": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"express": "^4.21.2",
|
||||
"express-session": "^1.18.1",
|
||||
"framer-motion": "^11.13.1",
|
||||
"html2canvas": "^1.4.1",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.453.0",
|
||||
"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);
|
||||
|
||||
return httpServer;
|
||||
|
||||
Reference in new issue
Block a user