148 lines
4.3 KiB
TypeScript
148 lines
4.3 KiB
TypeScript
"use client";
|
|
|
|
import { motion } from "framer-motion";
|
|
import {
|
|
Clock,
|
|
ExternalLink,
|
|
LucideMapPin,
|
|
MoreHorizontal,
|
|
Pencil,
|
|
Trash2,
|
|
} from "lucide-react";
|
|
import { RRuleDisplay } from "@/components/rrule-display";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import { formatEventRangeLabel } from "@/lib/event-date-format";
|
|
import { getEventValidationIssues } from "@/lib/event-form";
|
|
import type { CalendarEvent } from "@/lib/types";
|
|
|
|
interface EventCardProps {
|
|
event: CalendarEvent;
|
|
onEdit: (event: CalendarEvent) => void;
|
|
onDelete: (eventId: string) => void;
|
|
}
|
|
|
|
export const EVENT_CARD_SURFACE_CLASSES =
|
|
"group cursor-pointer rounded-[10px] bg-card p-4 shadow-[0_0_0_1px_rgba(0,0,0,0.08),0_2px_2px_rgba(0,0,0,0.04),0_0_0_1px_#fafafa] transition-[background-color,transform,box-shadow] duration-150 hover:-translate-y-0.5 hover:bg-accent/20";
|
|
|
|
export const EventCard = ({ event, onEdit, onDelete }: EventCardProps) => {
|
|
const validationIssues = getEventValidationIssues(event);
|
|
|
|
const handleEdit = () => {
|
|
onEdit({
|
|
id: event.id,
|
|
title: event.title,
|
|
description: event.description || "",
|
|
location: event.location || "",
|
|
url: event.url || "",
|
|
start: event.start,
|
|
end: event.end || "",
|
|
allDay: event.allDay || false,
|
|
recurrenceRule: event.recurrenceRule,
|
|
});
|
|
};
|
|
|
|
return (
|
|
<motion.div
|
|
layout
|
|
initial={{ opacity: 0, y: 8 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -8, transition: { duration: 0.15 } }}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
<div className={EVENT_CARD_SURFACE_CLASSES}>
|
|
<div className="flex items-start gap-3">
|
|
<div className="min-w-0 flex-1 space-y-1.5">
|
|
<h3 className="truncate text-sm font-medium leading-snug">
|
|
{event.title}
|
|
</h3>
|
|
|
|
{event.description && (
|
|
<p className="line-clamp-2 text-xs leading-relaxed text-muted-foreground">
|
|
{event.description}
|
|
</p>
|
|
)}
|
|
|
|
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground">
|
|
<span className="inline-flex items-center gap-1">
|
|
<Clock className="h-3 w-3 shrink-0" />
|
|
{formatEventRangeLabel(event)}
|
|
</span>
|
|
|
|
{event.location && (
|
|
<span className="inline-flex items-center gap-1 truncate">
|
|
<LucideMapPin className="h-3 w-3 shrink-0" />
|
|
<span className="truncate">{event.location}</span>
|
|
</span>
|
|
)}
|
|
|
|
{event.url && (
|
|
<Button
|
|
variant="link"
|
|
size="sm"
|
|
className="h-auto gap-1 p-0 text-xs"
|
|
asChild
|
|
>
|
|
<a
|
|
href={event.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
onClick={(currentEvent) => currentEvent.stopPropagation()}
|
|
>
|
|
<ExternalLink className="h-3 w-3" />
|
|
<span className="max-w-[120px] truncate">Link</span>
|
|
</a>
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{event.recurrenceRule && (
|
|
<RRuleDisplay rrule={event.recurrenceRule} start={event.start} />
|
|
)}
|
|
|
|
{validationIssues.length > 0 && (
|
|
<div className="rounded-[8px] bg-[#fff4f2] px-3 py-2 text-xs text-[#b42318] shadow-[inset_0_0_0_1px_rgba(180,35,24,0.14)] dark:bg-[#2a1715] dark:text-[#ff8a80]">
|
|
Warning: {validationIssues[0]}.
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-7 w-7 shrink-0 text-muted-foreground/70 hover:text-foreground"
|
|
aria-label="Event actions"
|
|
>
|
|
<MoreHorizontal className="h-3.5 w-3.5" />
|
|
<span className="sr-only">Event actions</span>
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-36">
|
|
<DropdownMenuItem onClick={handleEdit}>
|
|
<Pencil className="h-3.5 w-3.5" />
|
|
Edit
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem
|
|
onClick={() => onDelete(event.id)}
|
|
className="text-destructive focus:text-destructive"
|
|
>
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
Delete
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
};
|