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:
parent
203f808a68
commit
1a3e503355
2 changed files with 165 additions and 1 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Reference in a new issue