mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-11 17:26:45 +02:00
Enable editing of scheduled posts with text and media previews
Update the edit scheduled post dialog to allow modification of post content and associated media, incorporating fetched post data and a new text area component. 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
db67f4d039
commit
d7dbb0bcce
1 file changed
+180
-38
@@ -2,12 +2,14 @@ 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 { Textarea } from "@/components/ui/textarea";
|
||||
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 type { SocialPage, Media, Post } from "@shared/schema";
|
||||
import { format } from "date-fns";
|
||||
import { Image as ImageIcon, CheckCircle } from "lucide-react";
|
||||
|
||||
interface EditScheduledPostDialogProps {
|
||||
open: boolean;
|
||||
@@ -20,11 +22,23 @@ export default function EditScheduledPostDialog({ open, onOpenChange, scheduledP
|
||||
const [date, setDate] = useState("");
|
||||
const [time, setTime] = useState("");
|
||||
const [pageId, setPageId] = useState("");
|
||||
const [postText, setPostText] = useState("");
|
||||
const [selectedMediaIds, setSelectedMediaIds] = useState<string[]>([]);
|
||||
|
||||
const { data: pages = [] } = useQuery<SocialPage[]>({
|
||||
queryKey: ["/api/pages"],
|
||||
});
|
||||
|
||||
const { data: postData } = useQuery<{ post: Post; media: Media[] }>({
|
||||
queryKey: ["/api/posts", scheduledPost?.postId],
|
||||
enabled: !!scheduledPost?.postId && open,
|
||||
});
|
||||
|
||||
const { data: allMedia = [] } = useQuery<Media[]>({
|
||||
queryKey: ["/api/media"],
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (scheduledPost && open) {
|
||||
const scheduledAt = new Date(scheduledPost.scheduledAt);
|
||||
@@ -34,27 +48,38 @@ export default function EditScheduledPostDialog({ open, onOpenChange, scheduledP
|
||||
}
|
||||
}, [scheduledPost, open]);
|
||||
|
||||
const updateMutation = useMutation({
|
||||
useEffect(() => {
|
||||
if (postData && open) {
|
||||
setPostText(postData.post.content);
|
||||
setSelectedMediaIds(postData.media.map(m => m.id));
|
||||
}
|
||||
}, [postData, open]);
|
||||
|
||||
const updateScheduledMutation = 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 = () => {
|
||||
const updatePostTextMutation = useMutation({
|
||||
mutationFn: (data: { content: string }) =>
|
||||
apiRequest('PATCH', `/api/posts/${scheduledPost?.postId}`, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/posts', scheduledPost?.postId] });
|
||||
},
|
||||
});
|
||||
|
||||
const updatePostMediaMutation = useMutation({
|
||||
mutationFn: (data: { mediaIds: string[] }) =>
|
||||
apiRequest('PATCH', `/api/posts/${scheduledPost?.postId}/media`, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/posts', scheduledPost?.postId] });
|
||||
},
|
||||
});
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!date || !time || !pageId) {
|
||||
toast({
|
||||
title: "Erreur",
|
||||
@@ -64,40 +89,157 @@ export default function EditScheduledPostDialog({ open, onOpenChange, scheduledP
|
||||
return;
|
||||
}
|
||||
|
||||
const scheduledAt = new Date(`${date}T${time}`);
|
||||
updateMutation.mutate({ scheduledAt: scheduledAt.toISOString(), pageId });
|
||||
try {
|
||||
const scheduledAt = new Date(`${date}T${time}`);
|
||||
|
||||
await updateScheduledMutation.mutateAsync({ scheduledAt: scheduledAt.toISOString(), pageId });
|
||||
|
||||
if (postData && postText !== postData.post.content) {
|
||||
await updatePostTextMutation.mutateAsync({ content: postText });
|
||||
}
|
||||
|
||||
const currentMediaIds = postData?.media.map(m => m.id) || [];
|
||||
if (JSON.stringify(selectedMediaIds) !== JSON.stringify(currentMediaIds)) {
|
||||
await updatePostMediaMutation.mutateAsync({ mediaIds: selectedMediaIds });
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Modifications enregistrées",
|
||||
description: "Le post a été mis à jour avec succès",
|
||||
});
|
||||
onOpenChange(false);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: "Impossible de modifier le post",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMediaSelection = (mediaId: string) => {
|
||||
setSelectedMediaIds(prev => {
|
||||
if (prev.includes(mediaId)) {
|
||||
return prev.filter(id => id !== mediaId);
|
||||
} else {
|
||||
if (prev.length >= 10) {
|
||||
toast({
|
||||
title: "Limite atteinte",
|
||||
description: "Maximum 10 photos par publication",
|
||||
variant: "destructive",
|
||||
});
|
||||
return prev;
|
||||
}
|
||||
return [...prev, mediaId];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const isPending = updateScheduledMutation.isPending || updatePostTextMutation.isPending || updatePostMediaMutation.isPending;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[500px]" data-testid="dialog-edit-scheduled-post">
|
||||
<DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto" data-testid="dialog-edit-scheduled-post">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Modifier la programmation</DialogTitle>
|
||||
<DialogTitle>Modifier le post</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"
|
||||
<Label htmlFor="post-text">Texte du post</Label>
|
||||
<Textarea
|
||||
id="post-text"
|
||||
value={postText}
|
||||
onChange={(e) => setPostText(e.target.value)}
|
||||
placeholder="Écrivez votre texte..."
|
||||
className="min-h-[100px]"
|
||||
data-testid="textarea-post-text"
|
||||
/>
|
||||
</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"
|
||||
/>
|
||||
<Label>Photos sélectionnées ({selectedMediaIds.length}/10)</Label>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{selectedMediaIds.map((mediaId, index) => {
|
||||
const media = allMedia.find(m => m.id === mediaId);
|
||||
if (!media) return null;
|
||||
return (
|
||||
<div key={mediaId} className="relative group">
|
||||
<img
|
||||
src={media.instagramFeedUrl || media.originalUrl}
|
||||
alt="Selected"
|
||||
className="w-full h-24 object-cover rounded-lg border-2 border-primary"
|
||||
/>
|
||||
<div className="absolute top-1 left-1 bg-primary text-primary-foreground rounded-full w-6 h-6 flex items-center justify-center text-xs font-bold">
|
||||
{index + 1}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => toggleMediaSelection(mediaId)}
|
||||
className="absolute top-1 right-1 bg-destructive text-destructive-foreground rounded-full w-6 h-6 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
data-testid={`button-remove-media-${index}`}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Toutes les photos</Label>
|
||||
<div className="grid grid-cols-4 gap-2 max-h-[200px] overflow-y-auto p-2 border rounded-lg">
|
||||
{allMedia.map((media) => {
|
||||
const isSelected = selectedMediaIds.includes(media.id);
|
||||
return (
|
||||
<button
|
||||
key={media.id}
|
||||
onClick={() => toggleMediaSelection(media.id)}
|
||||
className="relative group"
|
||||
data-testid={`button-select-media-${media.id}`}
|
||||
>
|
||||
<img
|
||||
src={media.instagramFeedUrl || media.originalUrl}
|
||||
alt={media.fileName}
|
||||
className={`w-full h-20 object-cover rounded-lg border-2 transition-all ${
|
||||
isSelected ? 'border-primary' : 'border-border hover:border-primary/50'
|
||||
}`}
|
||||
/>
|
||||
{isSelected && (
|
||||
<div className="absolute inset-0 bg-primary/20 rounded-lg flex items-center justify-center">
|
||||
<CheckCircle className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-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>
|
||||
|
||||
<div className="space-y-2">
|
||||
@@ -127,10 +269,10 @@ export default function EditScheduledPostDialog({ open, onOpenChange, scheduledP
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={updateMutation.isPending}
|
||||
disabled={isPending}
|
||||
data-testid="button-save-edit"
|
||||
>
|
||||
{updateMutation.isPending ? "Enregistrement..." : "Enregistrer"}
|
||||
{isPending ? "Enregistrement..." : "Enregistrer"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
Reference in new issue
Block a user