mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 21:41:17 +00:00
199 lines
6.7 KiB
TypeScript
199 lines
6.7 KiB
TypeScript
/**
|
|
* CourseListItem - Kurs-Karten-Komponente
|
|
*
|
|
* Diese Komponente zeigt einen einzelnen Kurs als Karte an.
|
|
* Sie wird in der Kursliste verwendet und zeigt alle wichtigen Kursinformationen
|
|
* inklusive Teilnehmerzahl, Status und Bearbeitungsoption.
|
|
*/
|
|
|
|
import AbstractTooltip from "@/src/components/layout/AbstractTooltip";
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/src/components/ui/avatar";
|
|
import { Pen, Trash2, Link } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
|
|
/**
|
|
* Props für die CourseListItem Komponente
|
|
*/
|
|
interface CourseListItemProps {
|
|
courseId?: string; // ID des Kurses für den Buchungslink
|
|
courseName: string; // Name des Kurses
|
|
room: string; // Raum, in dem der Kurs stattfindet
|
|
timeFrom: string; // Uhrzeit am Start des Kurses
|
|
timeTo: string; // Uhrzeit am Ende des Kurses
|
|
currentParticipants: number; // Aktuelle Anzahl der Teilnehmer
|
|
maxParticipants: number; // Maximale Anzahl der Teilnehmer
|
|
price: number; // Preis in Euro
|
|
level: string; // Level
|
|
trainers: string[];
|
|
trainersMap?: Record<string, { label: string; pbSrc?: string }>;
|
|
onEdit?: () => void; // Callback-Funktion beim Klick auf Bearbeiten-Button
|
|
onDelete?: () => void; // Callback-Funktion beim Klick auf Löschen-Button
|
|
status?: string; // Status des Kurses ("draft" oder "published")
|
|
isPast?: boolean; // Ob der Kurs in der Vergangenheit liegt
|
|
showDeleteIcon?: boolean; // Ob das Löschen-Icon angezeigt werden soll
|
|
}
|
|
|
|
/**
|
|
* Kurs-Karten-Komponente
|
|
* Zeigt Kursinformationen in einem visuell ansprechenden Karten-Layout
|
|
*/
|
|
const CourseListItem: React.FC<CourseListItemProps> = ({
|
|
courseId,
|
|
courseName,
|
|
room,
|
|
timeFrom,
|
|
timeTo,
|
|
currentParticipants,
|
|
maxParticipants,
|
|
price,
|
|
level,
|
|
trainers,
|
|
trainersMap = {},
|
|
onEdit,
|
|
onDelete,
|
|
status,
|
|
isPast,
|
|
showDeleteIcon,
|
|
}) => {
|
|
// Prüfen, ob der Kurs voll belegt ist (für visuelle Hervorhebung)
|
|
const isFull = currentParticipants >= maxParticipants;
|
|
|
|
const levelConfig: Record<string, { label: string; bg: string; text: string }> = {
|
|
any: { label: "Jedes Niveau", bg: "bg-gray-200", text: "text-gray-700" },
|
|
beginner: { label: "Anfänger", bg: "bg-blue-200", text: "text-blue-700" },
|
|
advanced: { label: "Fortgeschrittene", bg: "bg-orange-200", text: "text-orange-700" },
|
|
pro: { label: "Profi", bg: "bg-red-200", text: "text-red-700" },
|
|
};
|
|
|
|
const currentLevel = levelConfig[level] || levelConfig.any;
|
|
|
|
// Funktion zum Kopieren des Buchungslinks
|
|
const handleCopyLink = () => {
|
|
if (!courseId) return;
|
|
|
|
const bookingUrl = `${window.location.origin}/courses/book/${courseId}`;
|
|
navigator.clipboard.writeText(bookingUrl);
|
|
toast.success("Link wurde in die Zwischenablage kopiert");
|
|
};
|
|
|
|
const getInitials = (name: string) => {
|
|
if (!name) return "";
|
|
|
|
const parts = name.trim().split(" ");
|
|
if (parts.length === 1) return parts[0][0].toUpperCase();
|
|
|
|
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={`bg-white border border-gray-200 rounded-xl p-5 shadow-sm hover:shadow-md transition flex flex-col gap-4 relative ${
|
|
isPast ? "opacity-60 bg-gray-50" : ""
|
|
}`}
|
|
>
|
|
<div className="flex items-center w-full">
|
|
{trainers.map((trainerId, index) => {
|
|
const trainerInfo = trainersMap[trainerId];
|
|
const displayName = trainerInfo?.label ?? trainerId;
|
|
const avatarSrc = trainerInfo?.pbSrc;
|
|
return (
|
|
<AbstractTooltip key={`${trainerId}-${index}`} tooltipText={displayName}>
|
|
<div
|
|
className={`relative ${index !== 0 ? "-ml-3" : ""}`}
|
|
style={{ zIndex: trainers.length - index }}
|
|
>
|
|
<Avatar className="rounded-full border-2 border-white">
|
|
{avatarSrc && <AvatarImage src={avatarSrc} alt={displayName} />}
|
|
<AvatarFallback>{getInitials(displayName)}</AvatarFallback>
|
|
</Avatar>
|
|
</div>
|
|
</AbstractTooltip>
|
|
);
|
|
})}
|
|
{/* Time */}
|
|
<div className="w-full flex justify-end">
|
|
<p className="text-gray-500 text-lg">
|
|
{timeFrom} - {timeTo}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Course Name */}
|
|
<h2 className="text-xl font-semibold text-gray-900">{courseName}</h2>
|
|
|
|
<div className="flex items-center justify-between w-full">
|
|
{/* Participant Number */}
|
|
<span className={isFull ? "text-red-600 font-semibold" : "text-gray-800"}>
|
|
{currentParticipants}/{maxParticipants} Teilnehmer
|
|
</span>
|
|
|
|
{/* Price */}
|
|
<h2 className="text-lg font-semibold">
|
|
{price.toLocaleString("de-DE", {
|
|
style: "currency",
|
|
currency: "EUR",
|
|
})}
|
|
</h2>
|
|
</div>
|
|
|
|
{/* Participant Bar */}
|
|
<div className="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
|
|
<div
|
|
className={`h-full ${isFull ? "bg-red-500" : "bg-blue"}`}
|
|
style={{
|
|
width: `${maxParticipants > 0 ? (currentParticipants / maxParticipants) * 100 : 0}%`,
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between w-full">
|
|
<span
|
|
className={`w-fit text-xs font-semibold px-2 py-1 rounded-lg ${currentLevel.bg} ${currentLevel.text}`}
|
|
>
|
|
{currentLevel.label}
|
|
</span>
|
|
{/* Room */}
|
|
<p className="text-gray-500">{room}</p>
|
|
</div>
|
|
|
|
<hr />
|
|
|
|
<div className="flex items-center justify-between w-full">
|
|
{/* Status */}
|
|
{status && (
|
|
<span
|
|
className={`w-fit top-3 left-3 text-xs font-semibold px-2 py-1 rounded ${
|
|
status === "draft" ? "bg-yellow-100 text-yellow-700" : "bg-green-100 text-green-700"
|
|
}`}
|
|
>
|
|
{status === "draft" ? "Entwurf" : "Veröffentlicht"}
|
|
</span>
|
|
)}
|
|
{/* Actions */}
|
|
<div className="flex gap-2">
|
|
{courseId && status === "published" && (
|
|
<button
|
|
onClick={handleCopyLink}
|
|
className="p-2 rounded-md hover:bg-blue-100 transition"
|
|
title="Buchungslink kopieren"
|
|
>
|
|
<Link size={20} className="text-blue-600" />
|
|
</button>
|
|
)}
|
|
{showDeleteIcon && onDelete && (
|
|
<button onClick={onDelete} className="p-2 rounded-md hover:bg-red-100 transition">
|
|
<Trash2 size={20} className="text-red-600" />
|
|
</button>
|
|
)}
|
|
{onEdit && (
|
|
<button onClick={onEdit} className="p-2 rounded-md hover:bg-gray-100 transition">
|
|
<Pen size={20} className="text-gray-700" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default CourseListItem;
|