122 lines
3.6 KiB
TypeScript
122 lines
3.6 KiB
TypeScript
import { IconButtonMiniLink } from "@/components/IconButtonMini";
|
|
import { IoTicketOutline } from "react-icons/io5";
|
|
|
|
export type ConcertData = {
|
|
_id: string;
|
|
title?: string;
|
|
subtitle?: string;
|
|
date: string;
|
|
time: string;
|
|
locationName?: string;
|
|
city?: string;
|
|
country?: string;
|
|
artists?: { _id: string; name?: string; slug?: string }[];
|
|
ticketUrl?: string;
|
|
};
|
|
|
|
function formatConcertDate(dateString: string) {
|
|
return new Intl.DateTimeFormat("en-GB", {
|
|
day: "numeric",
|
|
month: "short",
|
|
year: "numeric",
|
|
}).format(new Date(dateString + "T00:00:00"));
|
|
}
|
|
|
|
function getCountryCode(code?: string): string | undefined {
|
|
if (!code) return undefined;
|
|
return code.toUpperCase();
|
|
}
|
|
|
|
/** City + country code, e.g. "Utrecht (NL)" */
|
|
function formatCityCountry(city?: string, country?: string): string | undefined {
|
|
const code = getCountryCode(country);
|
|
if (city && code) return `${city} (${code})`;
|
|
if (city) return city;
|
|
if (code) return `(${code})`;
|
|
return undefined;
|
|
}
|
|
|
|
export function getDisplayTitle(concert: ConcertData): string {
|
|
if (concert.title) return concert.title;
|
|
if (concert.artists?.length) {
|
|
return concert.artists
|
|
.map((a) => a.name)
|
|
.filter(Boolean)
|
|
.join(", ");
|
|
}
|
|
return "Concert";
|
|
}
|
|
|
|
export function ConcertRow({
|
|
concert,
|
|
past,
|
|
}: {
|
|
concert: ConcertData;
|
|
past?: boolean;
|
|
}) {
|
|
const displayTitle = getDisplayTitle(concert);
|
|
const cityCountry = formatCityCountry(concert.city, concert.country);
|
|
|
|
return (
|
|
<tr>
|
|
<td className="py-4 pr-4 align-middle whitespace-nowrap text-lightsec dark:text-darksec">
|
|
<div className="text-lighttext dark:text-darktext">{formatConcertDate(concert.date)}</div>
|
|
<div className="text-sm">{concert.time}</div>
|
|
</td>
|
|
<td className="py-4 pr-4 align-middle text-lighttext dark:text-darktext">
|
|
<div className="line-clamp-1 font-silkasb">{displayTitle}</div>
|
|
{concert.subtitle && (
|
|
<div className="line-clamp-1 text-lightsec dark:text-darksec">{concert.subtitle}</div>
|
|
)}
|
|
{cityCountry && (
|
|
<div className="line-clamp-1 text-lightsec md:hidden dark:text-darksec">{cityCountry}</div>
|
|
)}
|
|
</td>
|
|
<td className="hidden py-4 pr-4 align-middle md:table-cell">
|
|
{concert.locationName && (
|
|
<div className="line-clamp-1 text-lighttext dark:text-darktext">{concert.locationName}</div>
|
|
)}
|
|
{cityCountry && <div className="line-clamp-1 text-lightsec dark:text-darksec">{cityCountry}</div>}
|
|
</td>
|
|
<td className="py-4 align-middle">
|
|
{!past && concert.ticketUrl && (
|
|
<IconButtonMiniLink
|
|
href={concert.ticketUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
aria-label="Buy tickets"
|
|
className="inline-flex items-center justify-center !p-1.5"
|
|
>
|
|
<IoTicketOutline className="size-4" />
|
|
</IconButtonMiniLink>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
);
|
|
}
|
|
|
|
export function ConcertTable({
|
|
concerts,
|
|
past,
|
|
}: {
|
|
concerts: ConcertData[];
|
|
past?: boolean;
|
|
}) {
|
|
if (concerts.length === 0) return null;
|
|
|
|
return (
|
|
<table className="w-full table-fixed text-left text-sm">
|
|
<colgroup>
|
|
<col className="w-28 md:w-32" />
|
|
<col />
|
|
<col className="hidden md:table-column md:w-56" />
|
|
<col className="w-10" />
|
|
</colgroup>
|
|
<tbody className="divide-y-1 divide-lighttext/10 dark:divide-darktext/10">
|
|
{concerts.map((concert) => (
|
|
<ConcertRow key={concert._id} concert={concert} past={past} />
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
);
|
|
}
|