INSIGHT-MVP/packages/frontend/src/components/EventCountdownTiles.tsx
Thomas Reitz a85634a906 feat: add trade event (Messe-Timer) feature with admin CRUD and dashboard tiles
Backend: TradeEvent Prisma model, NestJS CRUD module with date validation
and tenant isolation. Frontend: Admin Events page with create/edit/delete
modals, dashboard countdown tiles showing upcoming/ongoing/ended events
with progress bars, and useEventCountdown hook for live timer updates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 13:33:19 +01:00

186 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useActiveTradeEvents } from '../crm/hooks';
import { useEventCountdown, type EventCountdown } from '../hooks/useEventCountdown';
import type { TradeEvent } from '../crm/types';
import styles from './EventCountdownTiles.module.css';
// ============================================================
// Helpers
// ============================================================
function formatDateRange(start: string, end: string): string {
const s = new Date(start).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
});
const e = new Date(end).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
});
return `${s} ${e}`;
}
/** Hide events that ended more than 24h ago */
function isStillRelevant(event: TradeEvent): boolean {
const endDay = new Date(event.endDate);
endDay.setHours(23, 59, 59, 999);
const cutoff = new Date(endDay.getTime() + 24 * 60 * 60 * 1000);
return new Date() < cutoff;
}
// ============================================================
// Single Tile
// ============================================================
function CountdownTile({ event }: { event: TradeEvent }) {
const countdown: EventCountdown = useEventCountdown(event);
const borderColor =
countdown.status === 'upcoming'
? '#3b82f6'
: countdown.status === 'ongoing'
? '#22c55e'
: '#9ca3af';
return (
<div className={styles.tile} style={{ borderLeftColor: borderColor }}>
<div className={styles.tileHeader}>
<h3 className={styles.tileName}>{event.name}</h3>
<span
className={`${styles.statusChip} ${styles[`chip_${countdown.status}`]}`}
>
{countdown.status === 'upcoming'
? 'Bevorstehend'
: countdown.status === 'ongoing'
? 'Laeuft'
: 'Beendet'}
</span>
</div>
{/* Countdown / Progress */}
<div className={styles.countdownRow}>
{countdown.status === 'upcoming' && (
<span className={styles.countdownLabel}>{countdown.label}</span>
)}
{countdown.status === 'ongoing' && (
<>
<span className={styles.countdownLabel}>
{countdown.label} laeuft!
</span>
<div className={styles.progressBar}>
<div
className={styles.progressFill}
style={{ width: `${countdown.progressPercent}%` }}
/>
</div>
</>
)}
{countdown.status === 'ended' && (
<span className={styles.countdownLabelMuted}>Beendet</span>
)}
</div>
{/* Meta Info */}
<div className={styles.tileMeta}>
<div className={styles.metaItem}>
<svg
width="12"
height="12"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
>
<rect x="2" y="3" width="12" height="11" rx="1" />
<path d="M5 1v4M11 1v4M2 7h12" />
</svg>
<span>{formatDateRange(event.startDate, event.endDate)}</span>
</div>
{event.location && (
<div className={styles.metaItem}>
<svg
width="12"
height="12"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M8 1C5.2 1 3 3.2 3 6c0 4 5 9 5 9s5-5 5-9c0-2.8-2.2-5-5-5z" />
<circle cx="8" cy="6" r="1.5" />
</svg>
<span>
{event.location}
{event.boothInfo && ` · ${event.boothInfo}`}
</span>
</div>
)}
{!event.location && event.boothInfo && (
<div className={styles.metaItem}>
<svg
width="12"
height="12"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
>
<rect x="1" y="4" width="14" height="10" rx="1" />
<path d="M1 7h14" />
</svg>
<span>{event.boothInfo}</span>
</div>
)}
</div>
{event.websiteUrl && (
<a
href={event.websiteUrl}
target="_blank"
rel="noopener noreferrer"
className={styles.websiteLink}
>
<svg
width="12"
height="12"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M6 3H3a1 1 0 00-1 1v9a1 1 0 001 1h9a1 1 0 001-1v-3M10 2h4v4M7 9l7-7" />
</svg>
Website
</a>
)}
</div>
);
}
// ============================================================
// EventCountdownTiles — Grid
// ============================================================
export function EventCountdownTiles() {
const { data, isLoading } = useActiveTradeEvents();
const events = (data?.data ?? []).filter(isStillRelevant);
if (isLoading || events.length === 0) return null;
return (
<div className={styles.grid}>
{events.map((ev) => (
<CountdownTile key={ev.id} event={ev} />
))}
</div>
);
}