feat(event-card): redesign with motion layout animations, improved date formatting, and contextual metadata
- Wrap card in motion.div with layout/enter/exit animations - Replace Card/CardHeader/CardContent with flat glass-card div - Add hover-reveal action menu with opacity transition - Improve date formatting with locale options for month/day/time - Show end time inline next to start - Add ExternalLink for event URLs - Add icons to dropdown menu items with DropdownMenuSeparator
This commit is contained in:
@@ -1,11 +1,21 @@
|
|||||||
import { Clock, LucideMapPin, MoreHorizontal } from "lucide-react";
|
"use client";
|
||||||
|
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import {
|
||||||
|
Clock,
|
||||||
|
ExternalLink,
|
||||||
|
LucideMapPin,
|
||||||
|
MoreHorizontal,
|
||||||
|
Pencil,
|
||||||
|
Trash2,
|
||||||
|
} from "lucide-react";
|
||||||
import { RRuleDisplay } from "@/components/rrule-display";
|
import { RRuleDisplay } from "@/components/rrule-display";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import type { CalendarEvent } from "@/lib/types";
|
import type { CalendarEvent } from "@/lib/types";
|
||||||
@@ -19,8 +29,17 @@ interface EventCardProps {
|
|||||||
export const EventCard = ({ event, onEdit, onDelete }: EventCardProps) => {
|
export const EventCard = ({ event, onEdit, onDelete }: EventCardProps) => {
|
||||||
const formatDateTime = (dateStr: string, allDay: boolean | undefined) => {
|
const formatDateTime = (dateStr: string, allDay: boolean | undefined) => {
|
||||||
return allDay
|
return allDay
|
||||||
? new Date(dateStr).toLocaleDateString()
|
? new Date(dateStr).toLocaleDateString(undefined, {
|
||||||
: new Date(dateStr).toLocaleString();
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
})
|
||||||
|
: new Date(dateStr).toLocaleString(undefined, {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = () => {
|
const handleEdit = () => {
|
||||||
@@ -36,60 +55,92 @@ export const EventCard = ({ event, onEdit, onDelete }: EventCardProps) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const endDate =
|
||||||
|
event.end && !event.allDay ? formatDateTime(event.end, event.allDay) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full">
|
<motion.div
|
||||||
<CardHeader className="pb-3">
|
layout
|
||||||
<div className="flex items-start justify-between">
|
initial={{ opacity: 0, y: 8 }}
|
||||||
<div className="space-y-1 flex-1">
|
animate={{ opacity: 1, y: 0 }}
|
||||||
<h3 className="font-semibold leading-none tracking-tight">
|
exit={{ opacity: 0, y: -8, transition: { duration: 0.15 } }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<div className="glass-card p-4 group cursor-pointer hover:bg-accent/50 transition-colors duration-150">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="flex-1 min-w-0 space-y-1.5">
|
||||||
|
<h3 className="font-medium text-sm leading-snug truncate">
|
||||||
{event.title}
|
{event.title}
|
||||||
</h3>
|
</h3>
|
||||||
{event.recurrenceRule && (
|
|
||||||
<div className="mt-1">
|
|
||||||
<RRuleDisplay rrule={event.recurrenceRule} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{event.description && (
|
{event.description && (
|
||||||
<p className="text-sm text-muted-foreground mt-2 break-words">
|
<p className="text-xs text-muted-foreground line-clamp-2 leading-relaxed">
|
||||||
{event.description}
|
{event.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
|
||||||
|
<span className="inline-flex items-center gap-1">
|
||||||
|
<Clock className="h-3 w-3 shrink-0" />
|
||||||
|
{formatDateTime(event.start, event.allDay)}
|
||||||
|
{endDate && <span className="text-muted-foreground/50">-</span>}
|
||||||
|
{endDate}
|
||||||
|
</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 && (
|
||||||
|
<a
|
||||||
|
href={event.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-1 text-primary/70 hover:text-primary transition-colors"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-3 w-3" />
|
||||||
|
<span className="truncate max-w-[120px]">Link</span>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{event.recurrenceRule && (
|
||||||
|
<RRuleDisplay rrule={event.recurrenceRule} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
<Button
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
variant="ghost"
|
||||||
<span className="sr-only">Open menu</span>
|
size="icon"
|
||||||
|
className="h-7 w-7 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity duration-150"
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="h-3.5 w-3.5" />
|
||||||
|
<span className="sr-only">Event actions</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end" className="w-36">
|
||||||
<DropdownMenuItem onClick={handleEdit}>Edit</DropdownMenuItem>
|
<DropdownMenuItem onClick={handleEdit}>
|
||||||
|
<Pencil className="h-3.5 w-3.5 mr-2" />
|
||||||
|
Edit
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => onDelete(event.id)}
|
onClick={() => onDelete(event.id)}
|
||||||
className="text-destructive"
|
className="text-destructive focus:text-destructive"
|
||||||
>
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5 mr-2" />
|
||||||
Delete
|
Delete
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</div>
|
||||||
|
</motion.div>
|
||||||
<CardContent className="pt-0">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center text-sm text-muted-foreground">
|
|
||||||
<Clock className="mr-2 h-4 w-4" />
|
|
||||||
{formatDateTime(event.start, event.allDay)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{event.location && (
|
|
||||||
<div className="flex items-center text-sm text-muted-foreground">
|
|
||||||
<LucideMapPin className="mr-2 h-4 w-4" />
|
|
||||||
{event.location}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user