Files
ipv6-sims/app/components/home/AgendaSection.tsx
T

906 lines
45 KiB
TypeScript

"use client";
import { useState, useEffect, useMemo } from "react";
export type AgendaItem = {
id: number;
time_ict: string;
time_utc: string;
category: string;
title: string;
description: string;
speaker: string;
speaker_title: string;
location: string;
avatar?: string;
};
type Props = {
data: {
id: string;
eyebrow: string;
title: string;
description: string;
items: AgendaItem[];
};
};
const CATEGORIES = [
"All Sessions",
"Ceremonies",
"Keynotes & Address",
"Technical & Infrastructure",
"Forums & Panels",
"Breaks & Networking",
];
const renderFormattedText = (text: string) => {
if (!text) return null;
const lines = text.split("\n");
return lines.map((line, lineIndex) => {
const parts = line.split(/(\*\*.*?\*\*|\*.*?\*)/g);
const renderedLine = parts.map((part, partIndex) => {
if (part.startsWith("**") && part.endsWith("**")) {
return (
<strong key={partIndex} className="text-primary font-bold">
{part.slice(2, -2)}
</strong>
);
}
if (part.startsWith("*") && part.endsWith("*")) {
return (
<em key={partIndex} className="italic text-on-surface-variant/80">
{part.slice(1, -1)}
</em>
);
}
return part;
});
return (
<span key={lineIndex} className="block min-h-[1.25rem]">
{renderedLine}
</span>
);
});
};
export default function AgendaSection({ data }: Props) {
const base = process.env.NEXT_PUBLIC_BASE_PATH || "";
const [searchQuery, setSearchQuery] = useState("");
const [selectedCategory, setSelectedCategory] = useState("All Sessions");
const [viewMode, setViewMode] = useState<"table" | "card">("card");
const [timezoneMode, setTimezoneMode] = useState<"ICT" | "UTC" | "Local">("ICT");
const [bookmarkedIds, setBookmarkedIds] = useState<number[]>([]);
const [showOnlyBookmarks, setShowOnlyBookmarks] = useState(false);
const [selectedModalItem, setSelectedModalItem] = useState<AgendaItem | null>(null);
// Load bookmarks from localStorage
useEffect(() => {
try {
const saved = localStorage.getItem("ipv6_agenda_bookmarks");
if (saved) {
setBookmarkedIds(JSON.parse(saved));
}
} catch {
// Ignore localStorage errors
}
}, []);
// Keyboard shortcut to close modal
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
setSelectedModalItem(null);
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, []);
// Save bookmarks to localStorage
const toggleBookmark = (id: number) => {
setBookmarkedIds((prev) => {
const next = prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id];
try {
localStorage.setItem("ipv6_agenda_bookmarks", JSON.stringify(next));
} catch {
// Ignore localStorage errors
}
return next;
});
};
// Convert time display based on selected timezone mode
const getTimeDisplay = (item: AgendaItem) => {
if (timezoneMode === "UTC") {
return item.time_utc ? `${item.time_utc} UTC` : item.time_ict;
}
if (timezoneMode === "ICT") {
return item.time_ict ? `${item.time_ict} ICT` : item.time_utc;
}
// Local Time mode
try {
const timeStr = item.time_utc;
if (!timeStr) return item.time_ict;
const parts = timeStr.split(" - ");
if (parts.length < 1) return item.time_ict;
const parseUtcHourMin = (s: string) => {
const m = s.trim().match(/(\d+)[\.:](\d+)\s*(am|pm)/i);
if (!m) return null;
let h = parseInt(m[1], 10);
const min = parseInt(m[2], 10);
const period = m[3].toLowerCase();
if (period === "pm" && h < 12) h += 12;
if (period === "am" && h === 12) h = 0;
const d = new Date();
d.setUTCHours(h, min, 0, 0);
return d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit", hour12: true });
};
const startLocal = parseUtcHourMin(parts[0]);
const endLocal = parts[1] ? parseUtcHourMin(parts[1]) : null;
if (startLocal && endLocal) return `${startLocal} - ${endLocal} (Local)`;
if (startLocal) return `${startLocal} (Local)`;
return item.time_ict;
} catch {
return item.time_ict;
}
};
// Filter items
const filteredItems = useMemo(() => {
return (data.items || []).filter((item) => {
// Category filter
if (selectedCategory !== "All Sessions" && item.category !== selectedCategory) {
const categoryMatch =
selectedCategory === "Breaks & Networking" && (item.category?.includes("Breaks") || item.category?.includes("Nghỉ"));
const ceremonyMatch =
selectedCategory === "Ceremonies" && (item.category?.includes("Ceremony") || item.category?.includes("Lễ"));
const keynoteMatch =
selectedCategory === "Keynotes & Address" && (item.category?.includes("Keynote") || item.category?.includes("Bài phát biểu"));
const techMatch =
selectedCategory === "Technical & Infrastructure" && (item.category?.includes("Technical") || item.category?.includes("Kỹ thuật"));
const forumMatch =
selectedCategory === "Forums & Panels" && (item.category?.includes("Forum") || item.category?.includes("Tọa đàm"));
if (!categoryMatch && !ceremonyMatch && !keynoteMatch && !techMatch && !forumMatch) {
return false;
}
}
// Bookmark filter
if (showOnlyBookmarks && !bookmarkedIds.includes(item.id)) {
return false;
}
// Search query
if (searchQuery.trim()) {
const q = searchQuery.toLowerCase();
const matchTitle = item.title?.toLowerCase().includes(q);
const matchDesc = item.description?.toLowerCase().includes(q);
const matchSpeaker = item.speaker?.toLowerCase().includes(q) || item.speaker_title?.toLowerCase().includes(q);
const matchCategory = item.category?.toLowerCase().includes(q);
if (!matchTitle && !matchDesc && !matchSpeaker && !matchCategory) {
return false;
}
}
return true;
});
}, [data.items, selectedCategory, showOnlyBookmarks, bookmarkedIds, searchQuery]);
const handlePrint = () => {
window.print();
};
const renderAvatar = (avatarUrl?: string, sizeClass: string = "w-9 h-9 lg:w-10 lg:h-10") => {
return (
<div className={`${sizeClass} rounded-full overflow-hidden border border-primary/30 bg-primary/10 flex items-center justify-center backdrop-blur-sm shadow-sm shrink-0`}>
{avatarUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={`${base}${avatarUrl.startsWith("/") ? avatarUrl : `/${avatarUrl}`}`}
alt="Speaker"
className="w-full h-full object-cover"
/>
) : (
<svg className="w-1/2 h-1/2 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
</svg>
)}
</div>
);
};
return (
<section className="py-12 lg:py-20 max-w-hd mx-auto px-[var(--spacing-gutter)]" id={data.id}>
{/* Printable CSS style overlay for clean PDF / Print export */}
<style jsx global>{`
@media print {
body * {
visibility: hidden;
}
#printable-agenda-area,
#printable-agenda-area * {
visibility: visible;
}
#printable-agenda-area {
position: absolute;
left: 0;
top: 0;
width: 100%;
color: #000 !important;
background: #fff !important;
}
.no-print {
display: none !important;
}
.glass-panel {
background: #fff !important;
border: 1px solid #ddd !important;
color: #000 !important;
box-shadow: none !important;
}
.text-primary {
color: #1a56db !important;
}
.text-on-surface,
.text-on-surface-variant {
color: #111 !important;
}
}
`}</style>
{/* Header */}
<div className="flex flex-col lg:flex-row justify-between lg:items-end mb-8 gap-4 no-print">
<div className="max-w-3xl">
<span className="font-[var(--font-label-caps)] text-xs lg:text-sm text-primary block mb-2 tracking-[0.25em] uppercase">
{data.eyebrow}
</span>
<h2 className="font-[var(--font-display-lg)] text-on-surface uppercase tracking-tight text-[clamp(1.75rem,3vw+1rem,3.5rem)]">
{data.title}
</h2>
</div>
<p className="text-on-surface-variant/60 text-base lg:text-lg max-w-md xl:text-right">
{data.description}
</p>
</div>
{/* Main Container */}
<div id="printable-agenda-area" className="w-full">
{/* Printable Header (Visible only when printing) */}
<div className="hidden print:block mb-8 border-b pb-4">
<h1 className="text-2xl font-bold text-gray-900">APAC IPv6, AI & Cloud Summit 2026 - Agenda</h1>
<p className="text-sm text-gray-600">Lotte Center, 54 Liễu Giai, Phường Giảng , Nội, Việt Nam</p>
</div>
{/* Event Venue Banner with Direct Google Maps Button */}
<div className="glass-panel p-4 rounded-2xl mb-6 border border-primary/30 bg-surface/50 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 no-print">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-primary/10 border border-primary/30 flex items-center justify-center text-primary shrink-0">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1 1 15 0Z" />
</svg>
</div>
<div>
<div className="text-[11px] uppercase tracking-wider text-primary font-bold">Event Venue / Địa điểm tổ chức</div>
<div className="text-sm font-semibold text-on-surface">Lotte Center, 54 Liễu Giai, Phường Giảng , Nội</div>
</div>
</div>
<a
href="https://maps.app.goo.gl/DXwWCY4BKAV6BZbc8"
target="_blank"
rel="noopener noreferrer"
className="px-4 py-2 rounded-full gold-gradient-bg text-on-primary font-bold text-xs uppercase tracking-wider hover:shadow-[0_0_15px_rgba(197,160,89,0.4)] transition-all flex items-center gap-1.5 shrink-0"
>
<span>📍 Google Maps Direct</span>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
</svg>
</a>
</div>
{/* Control Toolbar */}
<div className="glass-panel p-3.5 lg:p-5 rounded-3xl mb-6 space-y-3 no-print border border-primary/20 bg-surface/40 backdrop-blur-md">
{/* Row 1: Search & My Schedule & Print & View Modes */}
<div className="flex flex-col md:flex-row gap-3 justify-between items-center">
{/* Search Input */}
<div className="relative w-full md:w-80">
<input
type="text"
placeholder="Search sessions, speaker..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-9 pr-3 py-2 rounded-full bg-background/80 border border-primary/20 text-on-surface text-xs lg:text-sm focus:outline-none focus:border-primary transition-colors placeholder:text-on-surface-variant/50"
/>
<svg
className="w-4 h-4 text-primary absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"
/>
</svg>
</div>
{/* Right Action Controls */}
<div className="flex flex-wrap items-center gap-2.5 w-full md:w-auto justify-end">
{/* My Schedule Filter Button */}
<button
onClick={() => setShowOnlyBookmarks(!showOnlyBookmarks)}
className={`px-3.5 py-1.5 rounded-full text-xs font-semibold uppercase tracking-wider transition-all duration-300 flex items-center gap-1.5 border ${
showOnlyBookmarks
? "bg-primary text-background border-primary shadow-[0_0_15px_rgba(197,160,89,0.4)]"
: "bg-surface/60 text-on-surface border-primary/30 hover:border-primary"
}`}
>
<span> My Schedule ({bookmarkedIds.length})</span>
</button>
{/* Print / Export PDF Button */}
<button
onClick={handlePrint}
className="px-3.5 py-1.5 rounded-full text-xs font-semibold uppercase tracking-wider border border-primary/30 bg-surface/60 text-primary hover:bg-primary/10 transition-colors flex items-center gap-1.5"
title="Print or Save as PDF"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0 1 10.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0 .229 2.523a1.125 1.125 0 0 1-1.12 1.227H7.231c-.6 0-1.1-.47-1.12-1.07L6.34 18m11.318 0h1.091A2.25 2.25 0 0 0 21 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 0 0-1.913-.247M6.34 18H5.25A2.25 2.25 0 0 1 3 15.75V9.456c0-1.081.768-2.015 1.837-2.175a48.041 48.041 0 0 1 1.913-.247m10.5 0a48.536 48.536 0 0 0-10.5 0m10.5 0V3.375c0-.621-.504-1.125-1.125-1.125h-8.25c-.621 0-1.125.504-1.125 1.125v3.656" />
</svg>
<span>Print / PDF</span>
</button>
{/* Timezone Switcher */}
<div className="flex items-center bg-background/80 rounded-full border border-primary/20 p-1 text-xs">
{(["ICT", "UTC", "Local"] as const).map((tz) => (
<button
key={tz}
onClick={() => setTimezoneMode(tz)}
className={`px-2 py-0.5 rounded-full transition-colors ${
timezoneMode === tz ? "bg-primary text-background font-bold" : "text-on-surface-variant/70 hover:text-on-surface"
}`}
>
{tz}
</button>
))}
</div>
{/* View Mode Toggle */}
<div className="flex items-center bg-background/80 rounded-full border border-primary/20 p-1 text-xs">
<button
onClick={() => setViewMode("card")}
className={`px-2.5 py-0.5 rounded-full transition-colors flex items-center gap-1 ${
viewMode === "card" ? "bg-primary text-background font-bold" : "text-on-surface-variant/70 hover:text-on-surface"
}`}
>
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24">
<path d="M4 4h7v16H4V4zm9 0h7v7h-7V4zm0 9h7v7h-7v-7z" />
</svg>
<span>Timeline</span>
</button>
<button
onClick={() => setViewMode("table")}
className={`px-2.5 py-0.5 rounded-full transition-colors flex items-center gap-1 ${
viewMode === "table" ? "bg-primary text-background font-bold" : "text-on-surface-variant/70 hover:text-on-surface"
}`}
>
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24">
<path d="M4 4h16v16H4V4zm2 4v4h12V8H6zm0 6v4h12v-4H6z" />
</svg>
<span>Table</span>
</button>
</div>
</div>
</div>
{/* Row 2: Category Filter Tags */}
<div className="flex items-center gap-1.5 overflow-x-auto pb-1 scrollbar-none border-t border-primary/10 pt-3">
{CATEGORIES.map((category) => (
<button
key={category}
onClick={() => setSelectedCategory(category)}
className={`px-3 py-1 rounded-full text-xs whitespace-nowrap transition-all duration-300 font-medium ${
selectedCategory === category
? "bg-primary/20 text-primary border border-primary/50 font-semibold"
: "bg-surface/40 text-on-surface-variant/80 border border-transparent hover:border-primary/20 hover:text-on-surface"
}`}
>
{category}
</button>
))}
</div>
</div>
{/* Results Counter Bar */}
<div className="flex justify-between items-center mb-6 text-xs text-on-surface-variant/70 px-2 no-print">
<div>
Showing <span className="text-primary font-bold">{filteredItems.length}</span> of {(data.items || []).length} sessions
{showOnlyBookmarks && " (My Schedule)"}
</div>
<div className="flex items-center gap-2">
<span className="relative flex h-2.5 w-2.5">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500"></span>
</span>
<span className="text-emerald-400 font-medium uppercase tracking-wider text-[11px]">Event Date: Aug 4, 2026</span>
</div>
</div>
{/* No Results Fallback */}
{filteredItems.length === 0 && (
<div className="text-center py-12 glass-panel rounded-3xl">
<p className="text-on-surface-variant/60 text-base mb-2">No matching sessions found.</p>
<button
onClick={() => {
setSearchQuery("");
setSelectedCategory("All Sessions");
setShowOnlyBookmarks(false);
}}
className="text-primary hover:underline text-xs font-semibold"
>
Reset all filters
</button>
</div>
)}
{/* CARD VIEW: Wider Max Width + Reduced Gaps for Natural Text Flow */}
{viewMode === "card" && filteredItems.length > 0 && (
<div className="relative w-full max-w-6xl mx-auto pl-8 lg:pl-0">
{/* Central vertical line */}
<div
className="absolute left-4 lg:left-1/2 top-4 bottom-4 w-[2px] bg-gradient-to-b from-primary via-primary/45 to-primary/5 -translate-x-1/2 pointer-events-none"
style={{ content: '""' }}
/>
{/* Timeline Items */}
<div className="space-y-6 lg:space-y-8">
{filteredItems.map((item, index) => {
const isEven = index % 2 === 0;
const isBookmarked = bookmarkedIds.includes(item.id);
const orderNum = String(index + 1).padStart(2, "0");
const timeText = getTimeDisplay(item);
const hasSpeaker = Boolean(item.speaker && item.speaker.trim() !== "");
return (
<div key={item.id || index} className="relative w-full group">
{/* Timeline Dot (glowing on hover) */}
<div className="absolute left-[-24px] lg:left-1/2 top-5 lg:top-6 -translate-x-1/2 w-4 h-4 rounded-full bg-background border-2 border-primary z-20 group-hover:scale-125 group-hover:bg-primary group-hover:shadow-[0_0_10px_#c5a059] transition-all duration-300 status-led" />
{/* Left/Right alternating grid layout */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3 lg:gap-8 w-full">
{isEven ? (
<>
{/* Left: Card (Desktop) / Full content (Mobile) */}
<div className="order-1 lg:order-1">
<div className="glass-panel p-4 lg:p-6 rounded-3xl relative overflow-hidden hover:border-primary/50 transition-all duration-300">
{/* Top Bar inside Card: Category badge + Order + Star */}
<div className="flex justify-between items-center mb-3 gap-2">
<span className="px-2.5 py-0.5 rounded-full text-[11px] font-medium bg-primary/10 text-primary border border-primary/20">
{item.category}
</span>
<div className="flex items-center gap-2.5">
<span className="text-xs font-[var(--font-display-lg)] text-primary/40 group-hover:text-primary transition-colors duration-300">
{orderNum}
</span>
<button
onClick={() => toggleBookmark(item.id)}
className="text-lg no-print hover:scale-110 transition-transform leading-none"
title={isBookmarked ? "Remove from My Schedule" : "Add to My Schedule"}
>
<span className={isBookmarked ? "text-amber-400" : "text-on-surface-variant/30 hover:text-amber-400"}>
{isBookmarked ? "★" : "☆"}
</span>
</button>
</div>
</div>
{/* Mobile-only time badge */}
<div className="lg:hidden mb-2">
<span className="font-[var(--font-display-lg)] text-sm text-primary">
{timeText}
</span>
</div>
{/* Session Title */}
<h3 className="font-[var(--font-display-lg)] text-base lg:text-lg text-on-surface mb-2 leading-snug lg:text-right">
{renderFormattedText(item.title)}
</h3>
{/* Description & Speaker Info */}
<div className="text-xs lg:text-sm text-on-surface-variant/75 leading-relaxed border-t border-primary/10 pt-2.5 mt-2.5 flex flex-col gap-2.5">
{item.description && (
<div className="lg:text-right">
{renderFormattedText(item.description)}
</div>
)}
{/* Speaker info with avatar */}
<div className="flex flex-row lg:flex-row-reverse items-center gap-2.5 border-t border-primary/5 pt-2">
{hasSpeaker ? (
<>
{renderAvatar(item.avatar)}
<div className="lg:text-right">
<div className="font-semibold text-on-surface text-xs lg:text-sm">
{item.speaker}
</div>
{item.speaker_title && (
<div className="text-[11px] text-on-surface-variant/60">
{item.speaker_title}
</div>
)}
</div>
</>
) : (
<div className="text-on-surface-variant/40 font-bold lg:text-right w-full text-base">
</div>
)}
</div>
{/* Location Badge */}
<div className="flex items-center lg:justify-end gap-1 text-[11px] text-on-surface-variant/60">
<svg className="w-3 h-3 text-primary shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
</svg>
<span>{item.location || "Main Stage"}</span>
</div>
</div>
</div>
</div>
{/* Right: Time badge (Desktop-only) */}
<div className="order-2 lg:order-2 hidden lg:flex flex-col items-start justify-center pl-2">
<span className="font-[var(--font-display-lg)] text-lg lg:text-xl text-primary group-hover:text-on-surface transition-colors duration-300">
{timeText}
</span>
{timezoneMode !== "UTC" && item.time_utc && (
<span className="text-xs text-on-surface-variant/50">
{item.time_utc} UTC
</span>
)}
</div>
</>
) : (
<>
{/* Left: Time badge (Desktop-only) */}
<div className="order-2 lg:order-1 hidden lg:flex flex-col items-end justify-center pr-2 text-right">
<span className="font-[var(--font-display-lg)] text-lg lg:text-xl text-primary group-hover:text-on-surface transition-colors duration-300">
{timeText}
</span>
{timezoneMode !== "UTC" && item.time_utc && (
<span className="text-xs text-on-surface-variant/50">
{item.time_utc} UTC
</span>
)}
</div>
{/* Right: Card (Desktop) / Full content (Mobile) */}
<div className="order-1 lg:order-2">
<div className="glass-panel p-4 lg:p-6 rounded-3xl relative overflow-hidden hover:border-primary/50 transition-all duration-300">
{/* Top Bar inside Card: Category badge + Order + Star */}
<div className="flex justify-between items-center mb-3 gap-2">
<span className="px-2.5 py-0.5 rounded-full text-[11px] font-medium bg-primary/10 text-primary border border-primary/20">
{item.category}
</span>
<div className="flex items-center gap-2.5">
<span className="text-xs font-[var(--font-display-lg)] text-primary/40 group-hover:text-primary transition-colors duration-300">
{orderNum}
</span>
<button
onClick={() => toggleBookmark(item.id)}
className="text-lg no-print hover:scale-110 transition-transform leading-none"
title={isBookmarked ? "Remove from My Schedule" : "Add to My Schedule"}
>
<span className={isBookmarked ? "text-amber-400" : "text-on-surface-variant/30 hover:text-amber-400"}>
{isBookmarked ? "★" : "☆"}
</span>
</button>
</div>
</div>
{/* Mobile-only time badge */}
<div className="lg:hidden mb-2">
<span className="font-[var(--font-display-lg)] text-sm text-primary">
{timeText}
</span>
</div>
{/* Session Title */}
<h3 className="font-[var(--font-display-lg)] text-base lg:text-lg text-on-surface mb-2 leading-snug">
{renderFormattedText(item.title)}
</h3>
{/* Description & Speaker Info */}
<div className="text-xs lg:text-sm text-on-surface-variant/75 leading-relaxed border-t border-primary/10 pt-2.5 mt-2.5 flex flex-col gap-2.5">
{item.description && (
<div>{renderFormattedText(item.description)}</div>
)}
{/* Speaker info with avatar */}
<div className="flex flex-row items-center gap-2.5 border-t border-primary/5 pt-2">
{hasSpeaker ? (
<>
{renderAvatar(item.avatar)}
<div>
<div className="font-semibold text-on-surface text-xs lg:text-sm">
{item.speaker}
</div>
{item.speaker_title && (
<div className="text-[11px] text-on-surface-variant/60">
{item.speaker_title}
</div>
)}
</div>
</>
) : (
<div className="text-on-surface-variant/40 font-bold text-base">
</div>
)}
</div>
{/* Location Badge */}
<div className="flex items-center gap-1 text-[11px] text-on-surface-variant/60">
<svg className="w-3 h-3 text-primary shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
</svg>
<span>{item.location || "Main Stage"}</span>
</div>
</div>
</div>
</div>
</>
)}
</div>
</div>
);
})}
</div>
</div>
)}
{/* TABLE VIEW: Responsive Truncated Rows + Click Row to Open Detail Modal */}
{viewMode === "table" && filteredItems.length > 0 && (
<div className="glass-panel rounded-3xl overflow-hidden border border-primary/20 shadow-2xl">
<div className="overflow-x-auto">
<table className="w-full text-left text-xs lg:text-sm text-on-surface border-collapse">
<thead className="bg-primary/10 uppercase tracking-wider text-[11px] font-bold text-primary border-b border-primary/20">
<tr>
<th className="py-3 px-3 lg:px-4 w-36 shrink-0">Time</th>
<th className="py-3 px-3 w-32 shrink-0">Category</th>
<th className="py-3 px-4 min-w-[240px]">Session Title & Description</th>
<th className="py-3 px-3 lg:px-4 w-48 shrink-0">Speaker / Presenter</th>
<th className="py-3 px-3 w-28 shrink-0">Location</th>
<th className="py-3 px-2 w-16 text-center no-print shrink-0">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-primary/10">
{filteredItems.map((item, idx) => {
const isBookmarked = bookmarkedIds.includes(item.id);
const isEven = idx % 2 === 0;
const hasSpeaker = Boolean(item.speaker && item.speaker.trim() !== "");
return (
<tr
key={item.id || idx}
onClick={() => setSelectedModalItem(item)}
className={`hover:bg-primary/10 transition-colors group cursor-pointer ${
isEven ? "bg-surface/20" : "bg-transparent"
}`}
title="Click to view full session details"
>
{/* Time */}
<td className="py-3 px-3 lg:px-4 align-top">
<div className="font-bold text-primary whitespace-nowrap text-xs lg:text-sm">
{getTimeDisplay(item)}
</div>
{timezoneMode !== "UTC" && item.time_utc && (
<div className="text-[11px] text-on-surface-variant/50 mt-0.5 whitespace-nowrap">
{item.time_utc} UTC
</div>
)}
</td>
{/* Category */}
<td className="py-3 px-3 align-top">
<span className="inline-block px-2.5 py-0.5 rounded-full text-[11px] font-medium bg-primary/10 text-primary border border-primary/20 whitespace-nowrap">
{item.category}
</span>
</td>
{/* Title & Description (Truncated for clean responsive table layout) */}
<td className="py-3 px-4 align-top max-w-md">
<h4 className="font-semibold text-on-surface text-xs lg:text-sm mb-1 leading-snug line-clamp-2 group-hover:text-primary transition-colors">
{item.title}
</h4>
{item.description && (
<p className="text-[11px] lg:text-xs text-on-surface-variant/75 leading-relaxed line-clamp-1">
{item.description}
</p>
)}
</td>
{/* Speaker */}
<td className="py-3 px-3 lg:px-4 align-top">
{hasSpeaker ? (
<div className="flex items-center gap-2">
{renderAvatar(item.avatar, "w-7 h-7 lg:w-8 lg:h-8")}
<div className="min-w-0">
<div className="font-semibold text-on-surface text-xs truncate">
{item.speaker}
</div>
{item.speaker_title && (
<div className="text-[10px] text-on-surface-variant/60 truncate">
{item.speaker_title}
</div>
)}
</div>
</div>
) : (
<span className="text-on-surface-variant/40 font-bold text-base"></span>
)}
</td>
{/* Location */}
<td className="py-3 px-3 align-top">
<div className="flex items-center gap-1 text-xs text-on-surface-variant/80 whitespace-nowrap">
<svg className="w-3.5 h-3.5 text-primary shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span>{item.location || "Main Stage"}</span>
</div>
</td>
{/* Actions: Save Star + Expand indicator */}
<td className="py-3 px-2 align-top text-center no-print" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-center gap-1">
<button
onClick={() => toggleBookmark(item.id)}
className="p-1 rounded-full hover:bg-primary/20 transition-all text-base"
title={isBookmarked ? "Remove from My Schedule" : "Add to My Schedule"}
>
<span className={isBookmarked ? "text-amber-400" : "text-on-surface-variant/30 hover:text-amber-400"}>
{isBookmarked ? "★" : "☆"}
</span>
</button>
<button
onClick={() => setSelectedModalItem(item)}
className="p-1 rounded-full text-on-surface-variant/40 hover:text-primary transition-colors"
title="Expand details"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25v-4.5m0 4.5h-4.5m4.5 0L15 15m-11.25 5.25h4.5m-4.5 0v-4.5m0 4.5L9 15" />
</svg>
</button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
)}
</div>
{/* SESSION DETAIL MODAL (Opens on Row Click in Table View or Detail Click) */}
{selectedModalItem && (
<div
className="fixed inset-0 z-50 bg-black/75 backdrop-blur-md flex items-center justify-center p-4 no-print animate-in fade-in duration-200"
onClick={() => setSelectedModalItem(null)}
>
<div
className="glass-panel p-6 lg:p-8 rounded-3xl max-w-2xl w-full relative max-h-[90vh] overflow-y-auto border border-primary/30 shadow-2xl bg-surface/95 text-on-surface"
onClick={(e) => e.stopPropagation()}
>
{/* Modal Header Bar */}
<div className="flex items-center justify-between gap-4 mb-4 pb-4 border-b border-primary/20">
<span className="px-3 py-1 rounded-full text-xs font-semibold bg-primary/20 text-primary border border-primary/40">
{selectedModalItem.category}
</span>
<div className="flex items-center gap-3">
<button
onClick={() => toggleBookmark(selectedModalItem.id)}
className="px-3 py-1 rounded-full text-xs font-semibold uppercase tracking-wider border border-primary/40 bg-surface/60 text-primary hover:bg-primary/20 transition-colors flex items-center gap-1.5"
>
<span className={bookmarkedIds.includes(selectedModalItem.id) ? "text-amber-400" : "text-on-surface-variant/40"}>
{bookmarkedIds.includes(selectedModalItem.id) ? "★ Saved" : "☆ Save to Schedule"}
</span>
</button>
<button
onClick={() => setSelectedModalItem(null)}
className="p-1.5 rounded-full hover:bg-primary/20 text-on-surface-variant/70 hover:text-on-surface transition-colors"
title="Close Modal (Esc)"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
{/* Session Time & Location Badges */}
<div className="flex flex-wrap items-center gap-3 text-xs text-on-surface-variant/80 mb-4">
<div className="flex items-center gap-1.5 bg-primary/10 px-3 py-1 rounded-full text-primary font-bold">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
<span>{getTimeDisplay(selectedModalItem)}</span>
</div>
{selectedModalItem.time_utc && (
<span className="text-on-surface-variant/60 text-[11px]">
({selectedModalItem.time_utc} UTC)
</span>
)}
<div className="flex items-center gap-1 text-on-surface-variant/80 bg-surface/80 px-3 py-1 rounded-full border border-primary/20">
<svg className="w-3.5 h-3.5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
</svg>
<span>{selectedModalItem.location || "Main Stage"}</span>
</div>
</div>
{/* Full Title (Untruncated) */}
<h2 className="font-[var(--font-display-lg)] text-xl lg:text-2xl text-on-surface font-bold mb-4 leading-snug">
{renderFormattedText(selectedModalItem.title)}
</h2>
{/* Full Description (Untruncated) */}
{selectedModalItem.description && (
<div className="text-sm text-on-surface-variant/85 leading-relaxed bg-surface/40 p-4 rounded-2xl border border-primary/10 mb-6">
<div className="text-xs uppercase font-bold text-primary mb-1 tracking-wider">Description</div>
{renderFormattedText(selectedModalItem.description)}
</div>
)}
{/* Speaker Information */}
<div className="border-t border-primary/20 pt-4">
<div className="text-xs uppercase font-bold text-primary mb-2 tracking-wider">Speaker / Presenter</div>
{selectedModalItem.speaker && selectedModalItem.speaker.trim() !== "" ? (
<div className="flex items-center gap-4 bg-primary/5 p-3.5 rounded-2xl border border-primary/15">
{renderAvatar(selectedModalItem.avatar, "w-12 h-12")}
<div>
<div className="font-bold text-on-surface text-base">
{selectedModalItem.speaker}
</div>
{selectedModalItem.speaker_title && (
<div className="text-xs text-on-surface-variant/70 mt-0.5">
{selectedModalItem.speaker_title}
</div>
)}
</div>
</div>
) : (
<div className="text-on-surface-variant/40 font-bold text-lg"></div>
)}
</div>
{/* Modal Footer */}
<div className="mt-6 pt-4 border-t border-primary/10 flex justify-end">
<button
onClick={() => setSelectedModalItem(null)}
className="px-6 py-2.5 rounded-full gold-gradient-bg text-on-primary font-bold text-xs uppercase tracking-wider hover:shadow-lg transition-all"
>
Close
</button>
</div>
</div>
</div>
)}
</section>
);
}