mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-24 15:38:10 +02:00
Improve calendar view by adding a responsive list layout
Introduce a new component for a responsive calendar list view and update the existing calendar view to conditionally render based on screen size. 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
ce8da4dafe
commit
d164f512cc
4 files changed
+235
-32
No files matched your search
@@ -34,10 +34,6 @@ 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
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import { format, isSameDay, parseISO } from "date-fns";
|
||||||
|
import { fr } from "date-fns/locale";
|
||||||
|
import { Edit, Trash2, ChevronDown, ChevronUp } from "lucide-react";
|
||||||
|
import { SiFacebook, SiInstagram } from "react-icons/si";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
interface CalendarListViewProps {
|
||||||
|
scheduledPosts: any[];
|
||||||
|
onEditPost: (post: any) => void;
|
||||||
|
onDeletePost: (postId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CalendarListView({ scheduledPosts, onEditPost, onDeletePost }: CalendarListViewProps) {
|
||||||
|
const [expandedDates, setExpandedDates] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
// Group posts by date
|
||||||
|
const groupedPosts = scheduledPosts.reduce((acc: { [key: string]: any[] }, post) => {
|
||||||
|
if (!post.scheduledAt) return acc;
|
||||||
|
const date = format(parseISO(post.scheduledAt), "yyyy-MM-dd");
|
||||||
|
if (!acc[date]) {
|
||||||
|
acc[date] = [];
|
||||||
|
}
|
||||||
|
acc[date].push(post);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
// Sort dates
|
||||||
|
const sortedDates = Object.keys(groupedPosts).sort();
|
||||||
|
|
||||||
|
const toggleDate = (date: string) => {
|
||||||
|
const newExpanded = new Set(expandedDates);
|
||||||
|
if (newExpanded.has(date)) {
|
||||||
|
newExpanded.delete(date);
|
||||||
|
} else {
|
||||||
|
newExpanded.add(date);
|
||||||
|
}
|
||||||
|
setExpandedDates(newExpanded);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isToday = (dateStr: string) => {
|
||||||
|
return isSameDay(parseISO(dateStr), new Date());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3 px-4 pb-4">
|
||||||
|
{sortedDates.length === 0 ? (
|
||||||
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
|
Aucune publication programmée
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
sortedDates.map((dateStr) => {
|
||||||
|
const posts = groupedPosts[dateStr];
|
||||||
|
const isExpanded = expandedDates.has(dateStr);
|
||||||
|
const dateObj = parseISO(dateStr);
|
||||||
|
const isDateToday = isToday(dateStr);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={dateStr}
|
||||||
|
className={`rounded-xl border ${isDateToday ? 'border-primary/50 bg-primary/5' : 'border-border/50 bg-card'} overflow-hidden`}
|
||||||
|
data-testid={`calendar-list-date-${dateStr}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => toggleDate(dateStr)}
|
||||||
|
className="w-full px-4 py-3 flex items-center justify-between hover:bg-muted/30 transition-colors"
|
||||||
|
data-testid={`button-toggle-date-${dateStr}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="text-left">
|
||||||
|
<div className="font-semibold text-foreground">
|
||||||
|
{format(dateObj, "EEEE d MMMM yyyy", { locale: fr })}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{posts.length} publication{posts.length > 1 ? 's' : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isDateToday && (
|
||||||
|
<span className="text-xs bg-primary text-primary-foreground px-2 py-1 rounded-full font-semibold">
|
||||||
|
Aujourd'hui
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronUp className="w-5 h-5 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="w-5 h-5 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="border-t border-border/50">
|
||||||
|
{posts.map((post: any) => {
|
||||||
|
const time = post.scheduledAt
|
||||||
|
? new Date(post.scheduledAt).toLocaleTimeString('fr-FR', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
})
|
||||||
|
: '';
|
||||||
|
const isPending = !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 (
|
||||||
|
<div
|
||||||
|
key={post.id}
|
||||||
|
className={`px-4 py-3 border-b border-border/30 last:border-b-0 ${
|
||||||
|
isPending ? 'bg-blue-500/5' : 'bg-green-500/5'
|
||||||
|
}`}
|
||||||
|
data-testid={`calendar-list-post-${post.id}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<span className="font-semibold text-lg text-foreground">{time}</span>
|
||||||
|
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${
|
||||||
|
isPending
|
||||||
|
? 'bg-blue-500/20 text-blue-600'
|
||||||
|
: 'bg-green-500/20 text-green-600'
|
||||||
|
}`}>
|
||||||
|
{isPending ? 'Programmé' : 'Publié'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<PlatformIcon className="w-4 h-4 flex-shrink-0" />
|
||||||
|
<span className="truncate">{pageName}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isPending && (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onEditPost(post)}
|
||||||
|
className="h-11 w-11 p-0"
|
||||||
|
data-testid={`button-edit-post-${post.id}`}
|
||||||
|
title="Modifier"
|
||||||
|
>
|
||||||
|
<Edit className="w-5 h-5 text-blue-500" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onDeletePost(post.id)}
|
||||||
|
className="h-11 w-11 p-0"
|
||||||
|
data-testid={`button-delete-post-${post.id}`}
|
||||||
|
title="Supprimer"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-5 h-5 text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,12 +7,15 @@ 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 { SiFacebook, SiInstagram } from "react-icons/si";
|
||||||
import EditScheduledPostDialog from "./edit-scheduled-post-dialog";
|
import EditScheduledPostDialog from "./edit-scheduled-post-dialog";
|
||||||
|
import CalendarListView from "./calendar-list-view";
|
||||||
|
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||||
|
|
||||||
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 [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||||
const [selectedPost, setSelectedPost] = useState<any>(null);
|
const [selectedPost, setSelectedPost] = useState<any>(null);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const isDesktop = useMediaQuery("(min-width: 768px)");
|
||||||
|
|
||||||
const { data: scheduledPosts = [] } = useQuery<ScheduledPost[]>({
|
const { data: scheduledPosts = [] } = useQuery<ScheduledPost[]>({
|
||||||
queryKey: ["/api/scheduled-posts"],
|
queryKey: ["/api/scheduled-posts"],
|
||||||
@@ -37,15 +40,15 @@ export default function CalendarView() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleDeletePost = (postId: string, e: React.MouseEvent) => {
|
const handleDeletePost = (postId: string, e?: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
if (e) e.stopPropagation();
|
||||||
if (confirm("Êtes-vous sûr de vouloir supprimer cette publication programmée ?")) {
|
if (confirm("Êtes-vous sûr de vouloir supprimer cette publication programmée ?")) {
|
||||||
deletePostMutation.mutate(postId);
|
deletePostMutation.mutate(postId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditPost = (post: any, e: React.MouseEvent) => {
|
const handleEditPost = (post: any, e?: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
if (e) e.stopPropagation();
|
||||||
setSelectedPost(post);
|
setSelectedPost(post);
|
||||||
setEditDialogOpen(true);
|
setEditDialogOpen(true);
|
||||||
};
|
};
|
||||||
@@ -147,16 +150,17 @@ export default function CalendarView() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-8">
|
{isDesktop ? (
|
||||||
<div className="grid grid-cols-7 gap-2 mb-2">
|
<div className="p-8">
|
||||||
{daysOfWeek.map((day) => (
|
<div className="grid grid-cols-7 gap-2 mb-2">
|
||||||
<div key={day} className="bg-muted/30 py-3 rounded-lg text-center">
|
{daysOfWeek.map((day) => (
|
||||||
<span className="text-sm font-semibold text-muted-foreground">{day}</span>
|
<div key={day} className="bg-muted/30 py-3 rounded-lg text-center">
|
||||||
</div>
|
<span className="text-sm font-semibold text-muted-foreground">{day}</span>
|
||||||
))}
|
</div>
|
||||||
</div>
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-7 gap-2">
|
<div className="grid grid-cols-7 gap-2">
|
||||||
{days.map((day, index) => (
|
{days.map((day, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
@@ -253,25 +257,30 @@ export default function CalendarView() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8 flex items-center justify-center gap-8 flex-wrap">
|
<div className="mt-8 flex items-center justify-center gap-8 flex-wrap">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-4 h-4 rounded-md bg-blue-500/20 border-2 border-blue-500"></div>
|
<div className="w-4 h-4 rounded-md bg-blue-500/20 border-2 border-blue-500"></div>
|
||||||
<span className="text-sm text-muted-foreground font-medium">Programmé</span>
|
<span className="text-sm text-muted-foreground font-medium">Programmé</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-4 h-4 rounded-md bg-green-500/20 border-2 border-green-500"></div>
|
<div className="w-4 h-4 rounded-md bg-green-500/20 border-2 border-green-500"></div>
|
||||||
<span className="text-sm text-muted-foreground font-medium">Publié</span>
|
<span className="text-sm text-muted-foreground font-medium">Publié</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
|
<CalendarListView
|
||||||
{selectedPost && (
|
scheduledPosts={scheduledPosts}
|
||||||
<EditScheduledPostDialog
|
onEditPost={handleEditPost}
|
||||||
open={editDialogOpen}
|
onDeletePost={handleDeletePost}
|
||||||
onOpenChange={setEditDialogOpen}
|
|
||||||
scheduledPost={selectedPost}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<EditScheduledPostDialog
|
||||||
|
open={editDialogOpen}
|
||||||
|
onOpenChange={setEditDialogOpen}
|
||||||
|
scheduledPost={selectedPost || {}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
export function useMediaQuery(query: string): boolean {
|
||||||
|
const [matches, setMatches] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const media = window.matchMedia(query);
|
||||||
|
|
||||||
|
// Set initial value
|
||||||
|
setMatches(media.matches);
|
||||||
|
|
||||||
|
// Define callback for changes
|
||||||
|
const listener = (e: MediaQueryListEvent) => {
|
||||||
|
setMatches(e.matches);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Modern browsers
|
||||||
|
if (media.addEventListener) {
|
||||||
|
media.addEventListener('change', listener);
|
||||||
|
return () => media.removeEventListener('change', listener);
|
||||||
|
}
|
||||||
|
// Legacy browsers
|
||||||
|
else {
|
||||||
|
media.addListener(listener);
|
||||||
|
return () => media.removeListener(listener);
|
||||||
|
}
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user