mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-21 22:18:12 +02:00
Add ability to edit scheduled social media posts
Implement functionality to edit scheduled posts, including date, time, and target page. Introduces a new dialog component for editing and updates the API to support PATCH requests for scheduled posts. Replit-Commit-Author: Agent Replit-Commit-Session-Id: ae4037a0-2a6f-4530-9bac-79b543286bda Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/397bca8c-984f-43ff-841a-10897aeb8140/ae4037a0-2a6f-4530-9bac-79b543286bda/ER8Naji
This commit is contained in:
1 parent
0435a442bd
commit
ce8da4dafe
4 files changed
+226
-12
No files matched your search
@@ -34,6 +34,10 @@ externalPort = 4200
|
|||||||
localPort = 40537
|
localPort = 40537
|
||||||
externalPort = 3000
|
externalPort = 3000
|
||||||
|
|
||||||
|
[[ports]]
|
||||||
|
localPort = 40905
|
||||||
|
externalPort = 6800
|
||||||
|
|
||||||
[[ports]]
|
[[ports]]
|
||||||
localPort = 42161
|
localPort = 42161
|
||||||
externalPort = 6000
|
externalPort = 6000
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ChevronLeft, ChevronRight, Calendar as CalendarIcon, Trash2 } from "lucide-react";
|
import { ChevronLeft, ChevronRight, Calendar as CalendarIcon, Trash2, Edit } from "lucide-react";
|
||||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
import type { ScheduledPost } from "@shared/schema";
|
import type { ScheduledPost } from "@shared/schema";
|
||||||
|
import { SiFacebook, SiInstagram } from "react-icons/si";
|
||||||
|
import EditScheduledPostDialog from "./edit-scheduled-post-dialog";
|
||||||
|
|
||||||
export default function CalendarView() {
|
export default function CalendarView() {
|
||||||
const [currentDate, setCurrentDate] = useState(new Date());
|
const [currentDate, setCurrentDate] = useState(new Date());
|
||||||
|
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||||
|
const [selectedPost, setSelectedPost] = useState<any>(null);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { data: scheduledPosts = [] } = useQuery<ScheduledPost[]>({
|
const { data: scheduledPosts = [] } = useQuery<ScheduledPost[]>({
|
||||||
@@ -40,6 +44,12 @@ export default function CalendarView() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEditPost = (post: any, e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setSelectedPost(post);
|
||||||
|
setEditDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
const getPostsForDate = (date: Date) => {
|
const getPostsForDate = (date: Date) => {
|
||||||
return (scheduledPosts || []).filter(post => {
|
return (scheduledPosts || []).filter(post => {
|
||||||
if (!post.scheduledAt) return false;
|
if (!post.scheduledAt) return false;
|
||||||
@@ -181,10 +191,13 @@ export default function CalendarView() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{postsForDay.slice(0, 2).map((post, idx) => {
|
{postsForDay.slice(0, 2).map((post: any, idx) => {
|
||||||
const time = post.scheduledAt ? new Date(post.scheduledAt).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }) : '';
|
const time = post.scheduledAt ? new Date(post.scheduledAt).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }) : '';
|
||||||
const isPending = !post.publishedAt;
|
const isPending = !post.publishedAt;
|
||||||
const isPublished = !!post.publishedAt;
|
const isPublished = !!post.publishedAt;
|
||||||
|
const pageName = post.page?.pageName || 'Page inconnue';
|
||||||
|
const platform = post.page?.platform || 'facebook';
|
||||||
|
const PlatformIcon = platform === 'instagram' ? SiInstagram : SiFacebook;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -199,17 +212,30 @@ export default function CalendarView() {
|
|||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="font-semibold">{time}</div>
|
<div className="font-semibold">{time}</div>
|
||||||
<div className="truncate opacity-90">{post.postType || 'Post'}</div>
|
<div className="flex items-center gap-1 truncate opacity-90">
|
||||||
|
<PlatformIcon className="w-3 h-3 flex-shrink-0" />
|
||||||
|
<span className="truncate">{pageName}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isPending && (
|
{isPending && (
|
||||||
<button
|
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
onClick={(e) => handleDeletePost(post.id, e)}
|
<button
|
||||||
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-red-500/20 rounded"
|
onClick={(e) => handleEditPost(post, e)}
|
||||||
data-testid={`button-delete-post-${post.id}`}
|
className="p-1 hover:bg-blue-500/20 rounded"
|
||||||
title="Supprimer"
|
data-testid={`button-edit-post-${post.id}`}
|
||||||
>
|
title="Modifier"
|
||||||
<Trash2 className="w-3 h-3 text-red-500" />
|
>
|
||||||
</button>
|
<Edit className="w-3 h-3 text-blue-500" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => handleDeletePost(post.id, e)}
|
||||||
|
className="p-1 hover:bg-red-500/20 rounded"
|
||||||
|
data-testid={`button-delete-post-${post.id}`}
|
||||||
|
title="Supprimer"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3 h-3 text-red-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -238,6 +264,14 @@ export default function CalendarView() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{selectedPost && (
|
||||||
|
<EditScheduledPostDialog
|
||||||
|
open={editDialogOpen}
|
||||||
|
onOpenChange={setEditDialogOpen}
|
||||||
|
scheduledPost={selectedPost}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
|
import type { SocialPage } from "@shared/schema";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
|
||||||
|
interface EditScheduledPostDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
scheduledPost: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EditScheduledPostDialog({ open, onOpenChange, scheduledPost }: EditScheduledPostDialogProps) {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [date, setDate] = useState("");
|
||||||
|
const [time, setTime] = useState("");
|
||||||
|
const [pageId, setPageId] = useState("");
|
||||||
|
|
||||||
|
const { data: pages = [] } = useQuery<SocialPage[]>({
|
||||||
|
queryKey: ["/api/pages"],
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (scheduledPost && open) {
|
||||||
|
const scheduledAt = new Date(scheduledPost.scheduledAt);
|
||||||
|
setDate(format(scheduledAt, "yyyy-MM-dd"));
|
||||||
|
setTime(format(scheduledAt, "HH:mm"));
|
||||||
|
setPageId(scheduledPost.pageId);
|
||||||
|
}
|
||||||
|
}, [scheduledPost, open]);
|
||||||
|
|
||||||
|
const updateMutation = useMutation({
|
||||||
|
mutationFn: (data: { scheduledAt: string; pageId: string }) =>
|
||||||
|
apiRequest('PATCH', `/api/scheduled-posts/${scheduledPost.id}`, data),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['/api/scheduled-posts'] });
|
||||||
|
toast({
|
||||||
|
title: "Programmation modifiée",
|
||||||
|
description: "La date et l'heure ont été mises à jour avec succès",
|
||||||
|
});
|
||||||
|
onOpenChange(false);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast({
|
||||||
|
title: "Erreur",
|
||||||
|
description: "Impossible de modifier la programmation",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (!date || !time || !pageId) {
|
||||||
|
toast({
|
||||||
|
title: "Erreur",
|
||||||
|
description: "Veuillez remplir tous les champs",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduledAt = new Date(`${date}T${time}`);
|
||||||
|
updateMutation.mutate({ scheduledAt: scheduledAt.toISOString(), pageId });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-[500px]" data-testid="dialog-edit-scheduled-post">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Modifier la programmation</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-6 py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="date">Date</Label>
|
||||||
|
<input
|
||||||
|
id="date"
|
||||||
|
type="date"
|
||||||
|
value={date}
|
||||||
|
onChange={(e) => setDate(e.target.value)}
|
||||||
|
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
data-testid="input-scheduled-date"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="time">Heure</Label>
|
||||||
|
<input
|
||||||
|
id="time"
|
||||||
|
type="time"
|
||||||
|
value={time}
|
||||||
|
onChange={(e) => setTime(e.target.value)}
|
||||||
|
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
data-testid="input-scheduled-time"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="page">Page</Label>
|
||||||
|
<Select value={pageId} onValueChange={setPageId}>
|
||||||
|
<SelectTrigger data-testid="select-page">
|
||||||
|
<SelectValue placeholder="Sélectionnez une page" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{pages.map((page) => (
|
||||||
|
<SelectItem key={page.id} value={page.id}>
|
||||||
|
{page.pageName} ({page.platform})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
data-testid="button-cancel-edit"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={updateMutation.isPending}
|
||||||
|
data-testid="button-save-edit"
|
||||||
|
>
|
||||||
|
{updateMutation.isPending ? "Enregistrement..." : "Enregistrer"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
+38
-1
@@ -9,7 +9,7 @@ import { z } from "zod";
|
|||||||
import { openRouterService } from "./services/openrouter";
|
import { openRouterService } from "./services/openrouter";
|
||||||
import { cloudinaryService } from "./services/cloudinary";
|
import { cloudinaryService } from "./services/cloudinary";
|
||||||
import { insertPostSchema, insertScheduledPostSchema, insertSocialPageSchema, insertAiGenerationSchema, insertCloudinaryConfigSchema, updateCloudinaryConfigSchema, insertOpenrouterConfigSchema, updateOpenrouterConfigSchema, insertUserSchema, postMedia, type SocialPage } from "@shared/schema";
|
import { insertPostSchema, insertScheduledPostSchema, insertSocialPageSchema, insertAiGenerationSchema, insertCloudinaryConfigSchema, updateCloudinaryConfigSchema, insertOpenrouterConfigSchema, updateOpenrouterConfigSchema, insertUserSchema, postMedia, type SocialPage } from "@shared/schema";
|
||||||
import type { User, InsertUser } from "@shared/schema";
|
import type { User, InsertUser, ScheduledPost } from "@shared/schema";
|
||||||
|
|
||||||
const upload = multer({ storage: multer.memoryStorage() });
|
const upload = multer({ storage: multer.memoryStorage() });
|
||||||
|
|
||||||
@@ -696,6 +696,43 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.patch("/api/scheduled-posts/:id", requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const user = req.user as User;
|
||||||
|
const userId = user.id;
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
// Verify the scheduled post belongs to the user before updating
|
||||||
|
const scheduledPost = await storage.getScheduledPost(id);
|
||||||
|
if (!scheduledPost) {
|
||||||
|
return res.status(404).json({ error: "Scheduled post not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const post = await storage.getPost(scheduledPost.postId);
|
||||||
|
if (!post || post.userId !== userId) {
|
||||||
|
return res.status(403).json({ error: "Unauthorized" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow updating scheduledAt and pageId
|
||||||
|
const { scheduledAt, pageId } = req.body;
|
||||||
|
const updateData: Partial<ScheduledPost> = {};
|
||||||
|
|
||||||
|
if (scheduledAt) {
|
||||||
|
updateData.scheduledAt = new Date(scheduledAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageId) {
|
||||||
|
updateData.pageId = pageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await storage.updateScheduledPost(id, updateData);
|
||||||
|
res.json(updated);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating scheduled post:", error);
|
||||||
|
res.status(500).json({ error: "Failed to update scheduled post" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.post("/api/scheduled-posts", requireAuth, async (req, res) => {
|
app.post("/api/scheduled-posts", requireAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const scheduledPostData = insertScheduledPostSchema.parse(req.body);
|
const scheduledPostData = insertScheduledPostSchema.parse(req.body);
|
||||||
|
|||||||
Reference in new issue
Block a user