Add Events card view to planning page with TopBar toggle

- planning.tsx: when ?tab=events, render a new card-based events view instead of the calendar; cards show date badge (month/day), title, description, time, and tablo name; includes search input and filter button; wired to existing useEventsByTablo hook and navigate-to-create flow
- TopBar.tsx: add "Événements" nav link that toggles to /planning?tab=events, with active state highlighting

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arthur Belleville 2026-02-21 20:01:54 +01:00
parent 203f808a68
commit 1a3e503355
No known key found for this signature in database
2 changed files with 165 additions and 1 deletions

View file

@ -354,6 +354,17 @@ export function TopBar() {
/>
</div>
<div className="flex items-center gap-3">
<Link
to="/planning?tab=events"
className={`hidden md:flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
location.pathname === "/planning" && new URLSearchParams(location.search).get("tab") === "events"
? "bg-purple-50 dark:bg-purple-950/30 text-purple-600 dark:text-purple-400"
: "text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-gray-100"
}`}
>
<CalendarIcon className="w-4 h-4" />
Événements
</Link>
<NotificationDropdown />
<ProfileDropdown />
</div>

View file

@ -11,7 +11,7 @@ import {
SelectValue,
} from "@xtablo/ui/components/select";
import { TypographyH3, TypographyH4 } from "@xtablo/ui/components/typography";
import { Download, FolderInputIcon, PlusIcon, RefreshCcw } from "lucide-react";
import { ClockIcon, Download, EllipsisVerticalIcon, FolderInputIcon, MapPinIcon, PlusIcon, RefreshCcw, SlidersHorizontalIcon } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Outlet, useNavigate, useParams, useSearchParams } from "react-router-dom";
@ -39,6 +39,8 @@ export const PlanningPage = () => {
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
const [isWebcalModalOpen, setIsWebcalModalOpen] = useState(false);
const isReadOnly = useIsReadOnlyUser();
const currentTab = searchParams.get("tab") ?? "calendar";
const [eventsSearchQuery, setEventsSearchQuery] = useState("");
// Fetch tablos
const { data: tablos, isLoading: tablosLoading } = useTablosList();
@ -789,6 +791,157 @@ export const PlanningPage = () => {
</div>
);
// ── Events card view ──────────────────────────────────────────────────────
const renderEventsView = () => {
const q = eventsSearchQuery.toLowerCase();
const filtered = tabloEvents.filter(
(e) =>
!q ||
e.title?.toLowerCase().includes(q) ||
e.description?.toLowerCase().includes(q)
);
const months = ["JAN", "FÉV", "MAR", "AVR", "MAI", "JUN", "JUL", "AOÛ", "SEP", "OCT", "NOV", "DÉC"];
return (
<div className="py-6 px-2">
{/* Header */}
<div className="flex items-center justify-between mb-6 flex-wrap gap-4">
<h1 className="text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100">
{t("planning:events", "Événements")}
</h1>
<button
type="button"
onClick={() => {
if (isReadOnly) return;
navigate(`/planning/create?date=${currentDate.toISOString()}${selectedTabloId !== "all" ? `&tablo_id=${selectedTabloId}` : ""}`);
}}
disabled={isReadOnly}
className="flex items-center gap-2 px-5 py-3 bg-primary text-white rounded-xl hover:bg-primary/90 transition-colors font-medium shadow-sm disabled:opacity-50"
>
<PlusIcon className="w-5 h-5" />
<span>{t("planning:createEvent")}</span>
</button>
</div>
{/* Search + filter */}
<div className="flex items-center justify-between gap-3 mb-8 flex-wrap">
<div className="relative flex-1 min-w-[250px] max-w-xs">
<svg xmlns="http://www.w3.org/2000/svg" className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" />
</svg>
<input
type="text"
placeholder="Rechercher..."
value={eventsSearchQuery}
onChange={(e) => setEventsSearchQuery(e.target.value)}
className="w-full pl-12 pr-4 py-3 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent text-gray-700 dark:text-gray-200"
/>
</div>
<button
type="button"
className="flex items-center gap-2 px-5 py-3 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<SlidersHorizontalIcon className="w-5 h-5 text-gray-600 dark:text-gray-400" />
<span className="text-gray-700 dark:text-gray-300 font-medium">Filtrer</span>
</button>
</div>
{/* Event cards grid */}
{tabloEventsLoading ? (
<div className="flex items-center justify-center py-24">
<img src="/icon.jpg" alt="Loading..." className="animate-spin rounded-full h-8 w-8 object-cover" />
</div>
) : filtered.length === 0 ? (
<div className="flex flex-col items-center justify-center py-24 text-center">
<p className="text-gray-500 dark:text-gray-400 text-lg font-medium">Aucun événement trouvé</p>
<p className="text-gray-400 dark:text-gray-500 text-sm mt-1">
{eventsSearchQuery ? "Essayez un autre terme" : "Créez votre premier événement"}
</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
{filtered.map((event) => {
const date = event.start_date ? new Date(event.start_date + "T00:00:00") : null;
const monthLabel = date ? months[date.getMonth()] : "";
const dayLabel = date ? String(date.getDate()).padStart(2, "0") : "";
const timeLabel = event.start_time
? `${event.start_time.slice(0, 5)}${event.end_time ? ` ${event.end_time.slice(0, 5)}` : ""}`
: null;
return (
<div
key={event.event_id}
className="bg-white dark:bg-gray-800 rounded-2xl p-6 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-all hover:border-gray-300 dark:hover:border-gray-600 relative group cursor-pointer"
onClick={() => navigate(`/planning/${event.event_id}?tablo_id=${event.tablo_id}`)}
>
<button
type="button"
onClick={(e) => { e.stopPropagation(); }}
className="absolute top-6 right-6 p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors opacity-0 group-hover:opacity-100"
>
<EllipsisVerticalIcon className="w-5 h-5" />
</button>
<div className="flex items-start gap-4 mb-4">
{/* Date badge */}
<div className="flex-shrink-0">
<div className="w-16 h-16 rounded-xl border border-primary flex flex-col items-center justify-center overflow-hidden">
<span className="text-xs font-semibold text-primary uppercase tracking-wide bg-[#F4F3FF] dark:bg-primary/20 w-full text-center py-1.5">
{monthLabel}
</span>
<span className="text-2xl font-bold text-primary leading-none py-1">
{dayLabel}
</span>
</div>
</div>
{/* Title + description */}
<div className="flex-1 min-w-0 pt-1 pr-8">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-1 leading-tight">
{event.title}
</h3>
{event.description && (
<p className="text-sm text-gray-500 dark:text-gray-400 line-clamp-2">
{event.description}
</p>
)}
</div>
</div>
{/* Meta */}
<div className="space-y-2.5 mt-3">
{timeLabel && (
<div className="flex items-center gap-2.5 text-[#344054] dark:text-gray-300">
<ClockIcon className="w-4 h-4 flex-shrink-0" />
<span className="text-sm">{timeLabel}</span>
</div>
)}
{event.tablo_name && (
<div className="flex items-center gap-2.5 text-[#344054] dark:text-gray-300">
<MapPinIcon className="w-4 h-4 flex-shrink-0" />
<span className="text-sm truncate">{event.tablo_name}</span>
</div>
)}
</div>
</div>
);
})}
</div>
)}
</div>
);
};
if (currentTab === "events") {
return (
<div className="min-h-screen bg-background px-4">
{renderEventsView()}
<Outlet />
</div>
);
}
return (
<div className="min-h-screen bg-background">
<div className="flex">