194 lines
5.8 KiB
TypeScript
194 lines
5.8 KiB
TypeScript
import { memo, useState } from 'react';
|
|
import type { Wish } from '@family-wishlist/shared';
|
|
import {
|
|
Archive,
|
|
CheckCircle2,
|
|
Copy,
|
|
ExternalLink,
|
|
MoreHorizontal,
|
|
Pencil,
|
|
RotateCcw,
|
|
Trash2,
|
|
} from 'lucide-react';
|
|
import { WishBadges } from '../WishBadges/WishBadges';
|
|
import { Button } from '../ui/Button';
|
|
import { cn } from '@/lib/cn';
|
|
import { formatPrice } from '@/lib/format';
|
|
import { useI18n } from '@/i18n/i18n';
|
|
|
|
export type WishCardView = 'owner' | 'guest';
|
|
|
|
export interface WishCardAction {
|
|
key: string;
|
|
icon: React.ComponentType<{ className?: string }>;
|
|
label: string;
|
|
onClick: () => void;
|
|
danger?: boolean;
|
|
}
|
|
|
|
interface WishCardProps {
|
|
wish: Wish & { isNewForOwner?: boolean };
|
|
view: WishCardView;
|
|
onEdit?: () => void;
|
|
onArchive?: () => void;
|
|
onComplete?: () => void;
|
|
onRestore?: () => void;
|
|
onDuplicate?: () => void;
|
|
onDelete?: () => void;
|
|
footer?: React.ReactNode;
|
|
}
|
|
|
|
function WishCardInner({
|
|
wish,
|
|
view,
|
|
onEdit,
|
|
onArchive,
|
|
onComplete,
|
|
onRestore,
|
|
onDuplicate,
|
|
onDelete,
|
|
footer,
|
|
}: WishCardProps) {
|
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
const { locale, t } = useI18n();
|
|
const completed = wish.status === 'COMPLETED';
|
|
const priceLabel = formatPrice(wish.price, wish.currency, locale);
|
|
const imageSrc = wish.imageUrl ?? '/default-gift.svg';
|
|
|
|
const actions: WishCardAction[] = [];
|
|
if (view === 'owner') {
|
|
if (wish.status === 'ACTIVE') {
|
|
if (onEdit) actions.push({ key: 'edit', icon: Pencil, label: t('wish.action.edit'), onClick: onEdit });
|
|
if (onComplete)
|
|
actions.push({
|
|
key: 'complete',
|
|
icon: CheckCircle2,
|
|
label: t('wish.action.complete'),
|
|
onClick: onComplete,
|
|
});
|
|
if (onArchive)
|
|
actions.push({ key: 'archive', icon: Archive, label: t('wish.action.archive'), onClick: onArchive });
|
|
if (onDelete)
|
|
actions.push({
|
|
key: 'delete',
|
|
icon: Trash2,
|
|
label: t('wish.action.delete'),
|
|
onClick: onDelete,
|
|
danger: true,
|
|
});
|
|
} else if (wish.status === 'ARCHIVED') {
|
|
if (onRestore)
|
|
actions.push({ key: 'restore', icon: RotateCcw, label: t('wish.action.restore'), onClick: onRestore });
|
|
if (onDelete)
|
|
actions.push({
|
|
key: 'delete',
|
|
icon: Trash2,
|
|
label: t('wish.action.delete'),
|
|
onClick: onDelete,
|
|
danger: true,
|
|
});
|
|
} else if (wish.status === 'COMPLETED') {
|
|
if (onDuplicate)
|
|
actions.push({
|
|
key: 'duplicate',
|
|
icon: Copy,
|
|
label: t('wish.action.duplicate'),
|
|
onClick: onDuplicate,
|
|
});
|
|
if (onDelete)
|
|
actions.push({
|
|
key: 'delete',
|
|
icon: Trash2,
|
|
label: t('wish.action.delete'),
|
|
onClick: onDelete,
|
|
danger: true,
|
|
});
|
|
} else if (wish.status === 'DELETED') {
|
|
if (onRestore)
|
|
actions.push({ key: 'restore', icon: RotateCcw, label: t('wish.action.restore'), onClick: onRestore });
|
|
}
|
|
}
|
|
|
|
return (
|
|
<article className={cn('wish-card', completed && 'wish-card--completed')}>
|
|
<div className="wish-card__image-wrap">
|
|
<img
|
|
className="wish-card__image"
|
|
src={imageSrc}
|
|
alt={wish.title}
|
|
loading="lazy"
|
|
onError={(e) => {
|
|
(e.currentTarget as HTMLImageElement).src = '/default-gift.svg';
|
|
}}
|
|
/>
|
|
<div className="absolute left-3 top-3">
|
|
<WishBadges wish={wish} view={view} />
|
|
</div>
|
|
{actions.length > 0 && (
|
|
<div className="absolute right-3 top-3">
|
|
<div className="relative">
|
|
<Button
|
|
variant="secondary"
|
|
size="icon"
|
|
aria-label={t('wish.actions')}
|
|
onClick={() => setMenuOpen((v) => !v)}
|
|
>
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
</Button>
|
|
{menuOpen && (
|
|
<>
|
|
<div
|
|
className="fixed inset-0 z-10"
|
|
onClick={() => setMenuOpen(false)}
|
|
aria-hidden
|
|
/>
|
|
<div className="absolute right-0 z-20 mt-2 w-48 overflow-hidden rounded-md border border-border bg-surface shadow-pop">
|
|
{actions.map((a) => (
|
|
<button
|
|
key={a.key}
|
|
type="button"
|
|
onClick={() => {
|
|
setMenuOpen(false);
|
|
a.onClick();
|
|
}}
|
|
className={cn(
|
|
'flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors hover:bg-surface-muted',
|
|
a.danger && 'text-danger',
|
|
)}
|
|
>
|
|
<a.icon className="h-4 w-4" />
|
|
{a.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="wish-card__body">
|
|
<h3 className="wish-card__title">{wish.title}</h3>
|
|
<div className="flex items-center gap-2">
|
|
{priceLabel && <span className="wish-card__price">{priceLabel}</span>}
|
|
{wish.url && (
|
|
<a
|
|
href={wish.url}
|
|
target="_blank"
|
|
rel="noreferrer noopener"
|
|
className="inline-flex items-center gap-1 text-xs font-medium text-primary hover:text-primary-600"
|
|
>
|
|
<ExternalLink className="h-3 w-3" />
|
|
{t('wish.openLink')}
|
|
</a>
|
|
)}
|
|
</div>
|
|
{wish.comment && <p className="wish-card__comment">{wish.comment}</p>}
|
|
{footer}
|
|
</div>
|
|
</article>
|
|
);
|
|
}
|
|
|
|
export const WishCard = memo(WishCardInner);
|