633 lines
28 KiB
TypeScript
633 lines
28 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { CardItem } from '../types';
|
|
import { getShadowStyle } from '../utils/lighting';
|
|
import { Fastener, Stamp, PhotoCorners } from './Fastener';
|
|
import { Pencil, Globe, ExternalLink } from 'lucide-react';
|
|
|
|
interface PaperCardProps {
|
|
card: CardItem;
|
|
lightAngle: number;
|
|
isDragging: boolean;
|
|
isLocked?: boolean;
|
|
onPointerDown: (e: React.PointerEvent, card: CardItem) => void;
|
|
onDoubleClick: (card: CardItem) => void;
|
|
onToggleTodo?: (cardId: string, index: number) => void;
|
|
}
|
|
|
|
export const PaperCard: React.FC<PaperCardProps> = ({
|
|
card,
|
|
lightAngle,
|
|
isDragging,
|
|
isLocked = false,
|
|
onPointerDown,
|
|
onDoubleClick,
|
|
onToggleTodo
|
|
}) => {
|
|
const [isHovered, setIsHovered] = useState(false);
|
|
|
|
// Get dynamic shadow based on light angle and drag state
|
|
const shadowStyle = getShadowStyle(
|
|
lightAngle,
|
|
isDragging,
|
|
isHovered ? 1.25 : 1.0,
|
|
card.type === 'sticky'
|
|
);
|
|
|
|
return (
|
|
<div
|
|
id={card.id}
|
|
className={`absolute select-none transition-transform will-change-transform ${
|
|
isLocked ? 'cursor-default' : 'cursor-grab active:cursor-grabbing'
|
|
} ${isDragging ? 'z-50 scale-[1.03]' : ''}`}
|
|
style={{
|
|
left: `${card.x}px`,
|
|
top: `${card.y}px`,
|
|
width: `${card.width}px`,
|
|
zIndex: card.zIndex,
|
|
transform: `rotate(${card.rotation}deg)`,
|
|
transformOrigin: 'center center',
|
|
touchAction: 'none'
|
|
}}
|
|
onPointerDown={(e) => onPointerDown(e, card)}
|
|
onDoubleClick={() => onDoubleClick(card)}
|
|
onClick={(e) => {
|
|
if (isLocked && card.type === 'bookmark' && card.url) {
|
|
e.stopPropagation();
|
|
window.open(card.url.startsWith('http') ? card.url : `https://${card.url}`, '_blank', 'noopener,noreferrer');
|
|
}
|
|
}}
|
|
onMouseEnter={() => setIsHovered(true)}
|
|
onMouseLeave={() => setIsHovered(false)}
|
|
>
|
|
{/* Top Attachment (Washi tape, pin, or paperclip) */}
|
|
<Fastener
|
|
type={card.attachType}
|
|
color={card.attachColor}
|
|
angle={card.attachAngle}
|
|
lightAngle={lightAngle}
|
|
/>
|
|
|
|
{/* Main Paper Body */}
|
|
<div
|
|
className="relative rounded-sm overflow-hidden text-neutral-900 transition-shadow duration-150"
|
|
style={{
|
|
backgroundColor: card.color || '#FFF9A6',
|
|
boxShadow: shadowStyle.boxShadow,
|
|
minHeight: `${card.height}px`
|
|
}}
|
|
>
|
|
{/* Subtle Paper Grain Filter Overlay */}
|
|
<div className="absolute inset-0 pointer-events-none opacity-25 mix-blend-multiply bg-[radial-gradient(#000_1px,transparent_1px)] [background-size:3px_3px]" />
|
|
|
|
{/* Quick Edit button for Mobile & Hover */}
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onDoubleClick(card);
|
|
}}
|
|
className={`absolute top-1.5 right-1.5 z-30 p-1 rounded-full bg-black/5 hover:bg-black/15 text-neutral-700 transition ${
|
|
isHovered ? 'opacity-90' : 'opacity-0 md:opacity-0 max-md:opacity-60'
|
|
}`}
|
|
title="Edit note"
|
|
>
|
|
<Pencil size={11} />
|
|
</button>
|
|
|
|
{/* Paper Corner Curled Illusion for Sticky Notes */}
|
|
{card.type === 'sticky' && (
|
|
<div
|
|
className="absolute -bottom-1 -right-1 w-6 h-6 pointer-events-none"
|
|
style={{
|
|
background: 'linear-gradient(135deg, transparent 50%, rgba(0,0,0,0.12) 50%)',
|
|
borderTopLeftRadius: '2px'
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* Rubber Stamp */}
|
|
<Stamp type={card.stamp} angle={card.stampAngle} />
|
|
|
|
{/* 1. POLAROID CARD */}
|
|
{card.type === 'polaroid' && (
|
|
<div className="p-2.5 pb-4 bg-white flex flex-col items-center">
|
|
<PhotoCorners />
|
|
<div className="w-full aspect-[4/3] bg-neutral-200 overflow-hidden relative shadow-inner">
|
|
{card.imageUrl ? (
|
|
<img
|
|
src={card.imageUrl}
|
|
alt={card.title || 'Polaroid'}
|
|
className="w-full h-full object-cover pointer-events-none"
|
|
loading="lazy"
|
|
draggable={false}
|
|
/>
|
|
) : (
|
|
<div className="w-full h-full flex items-center justify-center text-xs text-neutral-400">
|
|
No Image
|
|
</div>
|
|
)}
|
|
{/* Glossy photo finish sheen */}
|
|
<div className="absolute inset-0 bg-gradient-to-tr from-transparent via-white/10 to-transparent pointer-events-none" />
|
|
</div>
|
|
|
|
<div className="mt-3 text-center w-full px-1">
|
|
<span className={`text-base font-medium text-neutral-800 whitespace-pre-line tracking-wide ${card.fontFamily}`}>
|
|
{card.photoCaption || card.title}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 2. STICKY NOTE */}
|
|
{card.type === 'sticky' && (
|
|
<div className={`p-4 flex flex-col justify-between h-full ${card.fontFamily}`}>
|
|
{card.title && (
|
|
<div className="text-base font-bold tracking-tight text-neutral-900/90 mb-1 border-b border-black/5 pb-1">
|
|
{card.title}
|
|
</div>
|
|
)}
|
|
|
|
{card.todoItems && card.todoItems.length > 0 ? (
|
|
<div className="space-y-1 my-1">
|
|
{card.todoItems.map((item, idx) => (
|
|
<div
|
|
key={idx}
|
|
className="flex items-center gap-2 cursor-pointer group"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onToggleTodo?.(card.id, idx);
|
|
}}
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={item.done}
|
|
readOnly
|
|
className="cursor-pointer accent-neutral-800 rounded-sm w-3.5 h-3.5 pointer-events-none"
|
|
/>
|
|
<span
|
|
className={`text-sm leading-tight transition-all ${
|
|
item.done ? 'line-through text-neutral-500/70' : 'text-neutral-800'
|
|
}`}
|
|
>
|
|
{item.text}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div
|
|
className={`text-base leading-snug whitespace-pre-line ${
|
|
card.color === '#282C34' ? 'text-white/90 text-2xl text-center my-auto py-2' : 'text-neutral-800'
|
|
}`}
|
|
>
|
|
{card.content}
|
|
</div>
|
|
)}
|
|
|
|
{card.date && (
|
|
<div className="text-[11px] text-neutral-500 text-right mt-2 font-mono">
|
|
{card.date}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* 3. KRAFT LUGGAGE / PARCEL TAG */}
|
|
{card.type === 'tag' && (
|
|
<div
|
|
className="p-3.5 pb-4 flex flex-col items-center justify-between text-neutral-800 relative [clip-path:polygon(20%_0%,80%_0%,100%_15%,100%_100%,0%_100%,0%_15%)]"
|
|
style={{ backgroundColor: card.color || '#CBB296', minHeight: `${card.height}px` }}
|
|
>
|
|
{/* Tag string & brass eyelet */}
|
|
<div className="w-5 h-5 rounded-full border-2 border-amber-900/60 bg-amber-100/50 shadow-inner mt-0.5 mb-2 flex items-center justify-center shrink-0">
|
|
<div className="w-2.5 h-2.5 rounded-full bg-neutral-900/80 shadow" />
|
|
</div>
|
|
|
|
<div className="text-center w-full my-auto space-y-1 px-1">
|
|
<div className="text-[10px] font-mono font-bold tracking-widest text-neutral-700/80 uppercase">
|
|
{card.tagNumber || card.subtitle || '#TAG'}
|
|
</div>
|
|
{card.title && (
|
|
<div className={`text-sm font-bold tracking-wide uppercase text-neutral-900 ${card.fontFamily}`}>
|
|
{card.title}
|
|
</div>
|
|
)}
|
|
{card.content && (
|
|
<div className={`text-xs text-neutral-900/90 leading-snug whitespace-pre-line py-1.5 border-y border-amber-900/20 my-1 ${card.fontFamily}`}>
|
|
{card.content}
|
|
</div>
|
|
)}
|
|
{card.date && (
|
|
<div className="text-[10px] font-mono text-neutral-700">
|
|
{card.date}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="w-full text-center border-t border-amber-900/20 pt-1.5 text-[9px] tracking-widest uppercase font-mono text-neutral-700 shrink-0">
|
|
PRIORITY PARCEL
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 4. TORN PAPER / LETTER */}
|
|
{card.type === 'torn' && (
|
|
<div
|
|
className="p-5 flex flex-col justify-between"
|
|
style={{
|
|
clipPath: 'polygon(0% 2%, 8% 0%, 18% 2.5%, 28% 0.5%, 40% 3%, 52% 0.5%, 65% 2%, 78% 0%, 90% 2.5%, 100% 0%, 100% 100%, 0% 100%)'
|
|
}}
|
|
>
|
|
<div className="flex justify-between items-baseline border-b border-neutral-400/30 pb-2 mb-3">
|
|
<span className="font-mono text-xs uppercase tracking-wider text-neutral-500 font-bold">
|
|
{card.title || 'Memo'}
|
|
</span>
|
|
<span className="text-[11px] font-mono text-neutral-400">
|
|
{card.date || 'Fri — late'}
|
|
</span>
|
|
</div>
|
|
|
|
{card.subtitle && (
|
|
<div className="text-sm font-semibold mb-2 font-courier text-neutral-700">
|
|
{card.subtitle}
|
|
</div>
|
|
)}
|
|
|
|
{card.todoItems && card.todoItems.length > 0 ? (
|
|
<div className="space-y-1.5 font-caveat text-lg">
|
|
{card.todoItems.map((item, idx) => (
|
|
<div
|
|
key={idx}
|
|
className="flex items-center gap-2 cursor-pointer group"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onToggleTodo?.(card.id, idx);
|
|
}}
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={item.done}
|
|
readOnly
|
|
className="cursor-pointer accent-neutral-800 rounded-sm w-4 h-4 pointer-events-none"
|
|
/>
|
|
<span className={`${item.done ? 'line-through text-neutral-400' : 'text-neutral-800'}`}>
|
|
{item.text}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className={`text-sm leading-relaxed whitespace-pre-line text-neutral-800 ${card.fontFamily}`}>
|
|
{card.content}
|
|
</div>
|
|
)}
|
|
|
|
<div className="mt-4 pt-2 border-t border-dashed border-neutral-300 text-[10px] text-neutral-400 font-mono flex justify-between">
|
|
<span>NOTES // LOG</span>
|
|
<span>CONFIDENTIAL</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 5. MINIMAL BUSINESS CARD */}
|
|
{card.type === 'business' && (
|
|
<div className="p-5 flex flex-col justify-between h-full font-courier border border-black/5">
|
|
<div className="text-right text-[10px] tracking-widest text-neutral-500 font-mono">
|
|
[ JL ]
|
|
</div>
|
|
<div className="my-auto">
|
|
<div className="text-sm font-bold tracking-widest text-neutral-900 uppercase">
|
|
{card.title || 'JULES LAURENT'}
|
|
</div>
|
|
<div className="text-[11px] text-neutral-600 tracking-wider mt-0.5 uppercase">
|
|
{card.subtitle || 'PRODUCT DESIGNER'}
|
|
</div>
|
|
</div>
|
|
<div className="text-[10px] text-neutral-500 whitespace-pre-line font-mono tracking-tight pt-2 border-t border-neutral-400/20">
|
|
{card.content}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 6. STORE RECEIPT */}
|
|
{card.type === 'receipt' && (
|
|
<div
|
|
className="p-4 flex flex-col justify-between text-neutral-800 font-mono text-[11px]"
|
|
style={{
|
|
clipPath: 'polygon(0% 0%, 5% 1.5%, 10% 0%, 15% 1.5%, 20% 0%, 25% 1.5%, 30% 0%, 35% 1.5%, 40% 0%, 45% 1.5%, 50% 0%, 55% 1.5%, 60% 0%, 65% 1.5%, 70% 0%, 75% 1.5%, 80% 0%, 85% 1.5%, 90% 0%, 95% 1.5%, 100% 0%, 100% 100%, 95% 98.5%, 90% 100%, 85% 98.5%, 80% 100%, 75% 98.5%, 70% 100%, 65% 98.5%, 60% 100%, 55% 98.5%, 50% 100%, 45% 98.5%, 40% 100%, 35% 98.5%, 30% 100%, 25% 98.5%, 20% 100%, 15% 98.5%, 10% 100%, 5% 98.5%, 0% 100%)'
|
|
}}
|
|
>
|
|
<div className="text-center pb-2 border-b border-dashed border-neutral-400">
|
|
<div className="font-bold text-xs tracking-wider">{card.title || 'MALL STORE'}</div>
|
|
<div className="text-[10px] text-neutral-500">{card.subtitle}</div>
|
|
<div className="text-[10px] text-neutral-400 mt-1">{card.date || '10/24 15:34'}</div>
|
|
</div>
|
|
|
|
<div className="py-3 space-y-1">
|
|
{card.receiptItems?.map((it, idx) => (
|
|
<div key={idx} className="flex justify-between items-center text-[10px]">
|
|
<span className={idx >= (card.receiptItems?.length || 0) - 2 ? 'font-bold' : ''}>
|
|
{it.name}
|
|
</span>
|
|
<span className="font-mono">{it.price}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Barcode representation */}
|
|
<div className="pt-2 border-t border-dashed border-neutral-400 flex flex-col items-center">
|
|
<div className="h-6 w-32 flex items-stretch gap-[1.5px] justify-center opacity-80 my-1">
|
|
{[2, 1, 3, 1, 2, 4, 1, 3, 2, 1, 3, 1, 2, 1, 4, 2, 1, 3, 2, 1].map((w, i) => (
|
|
<div key={i} className="bg-neutral-900" style={{ width: `${w}px` }} />
|
|
))}
|
|
</div>
|
|
<span className="text-[9px] tracking-widest text-neutral-500 font-mono">
|
|
* 8 4 9 2 0 1 8 5 *
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 7. GRAPH / WIREFRAME SKETCH */}
|
|
{card.type === 'graph' && (
|
|
<div
|
|
className="p-4 flex flex-col justify-between"
|
|
style={{
|
|
backgroundImage: 'linear-gradient(to right, rgba(100,140,200,0.15) 1px, transparent 1px), linear-gradient(to bottom, rgba(100,140,200,0.15) 1px, transparent 1px)',
|
|
backgroundSize: '12px 12px'
|
|
}}
|
|
>
|
|
<div className="flex justify-between items-baseline mb-2">
|
|
<span className="font-courier text-xs font-bold text-neutral-700">
|
|
{card.title}
|
|
</span>
|
|
<span className="text-[10px] font-mono text-neutral-400">Fig. 1A</span>
|
|
</div>
|
|
|
|
{/* SVG Hand-drawn Charts */}
|
|
<div className="my-auto py-2 flex items-center justify-center">
|
|
{card.graphType === 'barchart' && (
|
|
<svg width="150" height="90" viewBox="0 0 150 90" fill="none" stroke="#2563EB" strokeWidth="1.8">
|
|
<path d="M 10 80 L 140 80" stroke="#64748B" />
|
|
<path d="M 10 10 L 10 80" stroke="#64748B" />
|
|
{/* Hatched bars */}
|
|
<rect x="25" y="45" width="18" height="35" fill="none" />
|
|
<path d="M 25 75 L 43 55 M 25 65 L 43 45 M 25 55 L 35 45" stroke="#3B82F6" strokeWidth="1.2" />
|
|
<rect x="55" y="25" width="18" height="55" fill="none" />
|
|
<path d="M 55 75 L 73 55 M 55 65 L 73 45 M 55 55 L 73 35 M 55 45 L 73 25" stroke="#3B82F6" strokeWidth="1.2" />
|
|
<rect x="85" y="15" width="18" height="65" fill="none" />
|
|
<path d="M 85 75 L 103 55 M 85 65 L 103 45 M 85 55 L 103 35 M 85 45 L 103 25 M 85 35 L 103 15" stroke="#3B82F6" strokeWidth="1.2" />
|
|
<rect x="115" y="38" width="18" height="42" fill="none" />
|
|
<path d="M 115 75 L 133 55 M 115 65 L 133 45 M 115 55 L 133 35" stroke="#3B82F6" strokeWidth="1.2" />
|
|
</svg>
|
|
)}
|
|
|
|
{card.graphType === 'flowchart' && (
|
|
<svg width="160" height="85" viewBox="0 0 160 85" fill="none" stroke="#475569" strokeWidth="1.5">
|
|
<rect x="10" y="10" width="40" height="25" rx="2" fill="white" />
|
|
<text x="16" y="26" fontSize="9" fill="#475569" fontFamily="monospace">INPUT</text>
|
|
<path d="M 50 22 L 70 22" stroke="#475569" markerEnd="url(#arrow)" />
|
|
<rect x="70" y="10" width="40" height="25" rx="2" fill="white" />
|
|
<text x="74" y="26" fontSize="9" fill="#475569" fontFamily="monospace">TRANS</text>
|
|
<path d="M 110 22 L 130 22" stroke="#475569" />
|
|
<circle cx="140" cy="22" r="10" fill="white" />
|
|
{/* Branch down */}
|
|
<path d="M 90 35 L 90 55 L 110 55" stroke="#475569" />
|
|
<rect x="110" y="45" width="40" height="22" rx="2" fill="white" />
|
|
<text x="115" y="59" fontSize="8" fill="#475569" fontFamily="monospace">OUTPUT</text>
|
|
</svg>
|
|
)}
|
|
|
|
{card.graphType === 'curve' && (
|
|
<svg width="150" height="85" viewBox="0 0 150 85" fill="none" stroke="#2563EB" strokeWidth="2">
|
|
<path d="M 10 75 L 140 75" stroke="#94A3B8" strokeWidth="1" />
|
|
<path d="M 10 10 L 10 75" stroke="#94A3B8" strokeWidth="1" />
|
|
<path d="M 10 65 Q 40 70 70 40 T 135 15" stroke="#2563EB" strokeWidth="2" strokeDasharray="3 2" />
|
|
<circle cx="70" cy="40" r="3" fill="#2563EB" />
|
|
<circle cx="135" cy="15" r="3" fill="#EF4444" />
|
|
</svg>
|
|
)}
|
|
</div>
|
|
|
|
<div className="text-[10px] text-neutral-500 font-courier text-right">
|
|
scale 1:1 · mm
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 8. MOODBOARD CARD */}
|
|
{card.type === 'moodboard' && (
|
|
<div className="p-4 flex flex-col justify-between h-full font-caveat">
|
|
<div>
|
|
<div className="text-xl font-bold text-neutral-900 border-b border-neutral-300/60 pb-1 mb-2">
|
|
{card.title || 'Moodboard'}
|
|
</div>
|
|
<div className="text-base text-neutral-800 leading-snug whitespace-pre-line">
|
|
{card.content}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-3 pt-2 border-t border-neutral-200 flex items-center justify-between text-xs text-neutral-500 font-sans">
|
|
<span>{card.subtitle || 'Creative Direction'}</span>
|
|
<span className="font-mono text-[10px]">#mood</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 9. BOOKMARK / WEB PREVIEW CARD */}
|
|
{card.type === 'bookmark' && (
|
|
<div className="p-3 bg-[#FAF8F5] text-neutral-900 flex flex-col justify-between h-full rounded border border-neutral-300/80 shadow-xs">
|
|
{/* Header: Favicon + Domain + External Link */}
|
|
<div className="flex items-center justify-between gap-1.5 pb-1.5 mb-1.5 border-b border-neutral-200/90">
|
|
<div className="flex items-center gap-1.5 overflow-hidden">
|
|
{card.faviconUrl ? (
|
|
<img src={card.faviconUrl} alt="" className="w-3.5 h-3.5 rounded-xs shrink-0" />
|
|
) : (
|
|
<Globe size={13} className="text-neutral-500 shrink-0" />
|
|
)}
|
|
<span className="font-mono text-[10px] text-neutral-600 truncate font-semibold">
|
|
{card.url ? (() => {
|
|
try {
|
|
return new URL(card.url.startsWith('http') ? card.url : `https://${card.url}`).hostname.replace('www.', '');
|
|
} catch {
|
|
return 'link';
|
|
}
|
|
})() : 'bookmark'}
|
|
</span>
|
|
</div>
|
|
{card.url && (
|
|
<a
|
|
href={card.url.startsWith('http') ? card.url : `https://${card.url}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
onClick={(e) => e.stopPropagation()}
|
|
className="p-1 rounded bg-neutral-200/60 hover:bg-amber-400 hover:text-black text-neutral-600 transition shrink-0"
|
|
title="Open link in new window"
|
|
>
|
|
<ExternalLink size={11} />
|
|
</a>
|
|
)}
|
|
</div>
|
|
|
|
{/* Thumbnail Preview Image */}
|
|
<div
|
|
className="w-full aspect-[16/10] bg-neutral-200 rounded overflow-hidden relative shadow-inner mb-2 cursor-pointer group"
|
|
onClick={(e) => {
|
|
if (card.url) {
|
|
e.stopPropagation();
|
|
window.open(card.url.startsWith('http') ? card.url : `https://${card.url}`, '_blank', 'noopener,noreferrer');
|
|
}
|
|
}}
|
|
>
|
|
{card.imageUrl ? (
|
|
<img
|
|
src={card.imageUrl}
|
|
alt={card.title || 'Web preview'}
|
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-200"
|
|
loading="lazy"
|
|
/>
|
|
) : (
|
|
<div className="w-full h-full flex flex-col items-center justify-center text-neutral-400 gap-1 bg-neutral-100">
|
|
<Globe size={18} className="text-neutral-400" />
|
|
<span className="text-[10px] font-mono">Web Snapshot</span>
|
|
</div>
|
|
)}
|
|
{/* Hover visit pill */}
|
|
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/15 transition flex items-center justify-center">
|
|
<span className="opacity-0 group-hover:opacity-100 bg-black/80 text-white text-[10px] font-mono px-2 py-0.5 rounded shadow transition flex items-center gap-1">
|
|
<ExternalLink size={10} /> Visit
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Title & Description */}
|
|
<div className="flex-1 flex flex-col justify-between">
|
|
<div>
|
|
<h4
|
|
className="font-bold text-xs leading-snug line-clamp-2 text-neutral-900 cursor-pointer hover:text-amber-600 transition"
|
|
onClick={(e) => {
|
|
if (card.url) {
|
|
e.stopPropagation();
|
|
window.open(card.url.startsWith('http') ? card.url : `https://${card.url}`, '_blank', 'noopener,noreferrer');
|
|
}
|
|
}}
|
|
>
|
|
{card.title || card.url || 'Web Bookmark'}
|
|
</h4>
|
|
{card.content && (
|
|
<p className="text-[11px] text-neutral-600 line-clamp-2 mt-1 font-sans">
|
|
{card.content}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{card.url && (
|
|
<div className="mt-2 pt-1.5 border-t border-neutral-200/80 flex items-center justify-between text-[10px] text-neutral-500 font-mono">
|
|
<span className="truncate max-w-[130px]">{card.url}</span>
|
|
<span className="text-amber-600 font-bold flex items-center gap-0.5">
|
|
↗
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 10. INTERACTIVE CHECKLIST CARD */}
|
|
{card.type === 'checklist' && (
|
|
<div className="p-4 flex flex-col justify-between h-full bg-[#FCFAF2] text-neutral-900 border border-neutral-300/80">
|
|
<div>
|
|
<div className="flex justify-between items-center pb-2 mb-2 border-b border-neutral-300">
|
|
<span className="font-bold text-sm tracking-tight">{card.title || 'To-Do List'}</span>
|
|
{card.todoItems && card.todoItems.length > 0 && (
|
|
<span className="font-mono text-[10px] px-1.5 py-0.5 rounded bg-neutral-200 text-neutral-700">
|
|
{card.todoItems.filter((t) => t.done).length}/{card.todoItems.length}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{card.todoItems && card.todoItems.length > 0 ? (
|
|
<div className={`space-y-1.5 py-1 ${card.fontFamily || 'font-caveat'} text-base`}>
|
|
{card.todoItems.map((item, idx) => (
|
|
<div
|
|
key={idx}
|
|
className="flex items-center gap-2 cursor-pointer group"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onToggleTodo?.(card.id, idx);
|
|
}}
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={item.done}
|
|
readOnly
|
|
className="cursor-pointer accent-neutral-900 rounded-sm w-3.5 h-3.5 pointer-events-none"
|
|
/>
|
|
<span className={`${item.done ? 'line-through text-neutral-400' : 'text-neutral-800'}`}>
|
|
{item.text}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-xs text-neutral-500 italic py-2">
|
|
{card.content || 'No tasks listed'}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="pt-2 border-t border-neutral-200 flex justify-between text-[10px] font-mono text-neutral-500">
|
|
<span>CHECKLIST</span>
|
|
<span>{card.date || 'PENDING'}</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 11. VINTAGE TICKET / BOARDING PASS */}
|
|
{card.type === 'ticket' && (
|
|
<div
|
|
className="p-3.5 flex flex-col justify-between bg-[#F4EDE2] text-neutral-900 font-mono text-xs border border-amber-900/30"
|
|
style={{
|
|
clipPath: 'polygon(0% 0%, 100% 0%, 100% 40%, 96% 45%, 96% 55%, 100% 60%, 100% 100%, 0% 100%, 0% 60%, 4% 55%, 4% 45%, 0% 40%)'
|
|
}}
|
|
>
|
|
<div className="flex justify-between items-center pb-1.5 border-b border-amber-900/30">
|
|
<span className="text-[10px] font-black tracking-widest text-amber-900 uppercase">
|
|
{card.subtitle || 'PASS // TICKET'}
|
|
</span>
|
|
<span className="text-[10px] text-neutral-600">
|
|
NO. {card.tagNumber || '78429'}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="my-auto py-2 text-center">
|
|
<div className="text-sm font-bold tracking-wider text-neutral-900 uppercase">
|
|
{card.title || 'BOARDING PASS'}
|
|
</div>
|
|
<div className="text-[11px] text-neutral-700 mt-1 whitespace-pre-line">
|
|
{card.content || 'ADMIT ONE · GENERAL ACCESS'}
|
|
</div>
|
|
{card.date && (
|
|
<div className="text-[10px] text-amber-900 mt-1.5 font-bold">
|
|
{card.date}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Perforated barcode tear stub */}
|
|
<div className="pt-2 border-t border-dashed border-amber-900/40 flex flex-col items-center">
|
|
<div className="h-4 w-28 flex items-stretch gap-[1.5px] justify-center opacity-70">
|
|
{[1, 2, 1, 3, 1, 2, 1, 4, 1, 2, 3, 1, 2, 1, 3].map((w, i) => (
|
|
<div key={i} className="bg-neutral-900" style={{ width: `${w}px` }} />
|
|
))}
|
|
</div>
|
|
<span className="text-[8px] tracking-widest text-neutral-500 mt-0.5">
|
|
★ NON-TRANSFERABLE ★
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|