feat: first commit

This commit is contained in:
sittichok Ouamsiri
2026-06-15 21:25:57 +07:00
commit 3395ab6dd3
88 changed files with 10034 additions and 0 deletions
+142
View File
@@ -0,0 +1,142 @@
import { motion } from "framer-motion";
import type { ShortLink } from "../lib/types";
import {
compactNumber,
fullShortUrl,
prettyHost,
relativeTime,
shortUrl,
} from "../lib/format";
import { BarChart } from "./BarChart";
import { Button } from "./ui/Button";
interface Props {
link: ShortLink;
onEdit: (link: ShortLink) => void;
onDelete: (link: ShortLink) => void;
onCopy: () => void;
onManagePin: (link: ShortLink) => void;
onShowQr: (link: ShortLink) => void;
}
const modeLabel: Record<string, string> = {
random: "random",
memorable: "memorable",
custom: "custom",
};
export function UrlCard({
link,
onEdit,
onDelete,
onCopy,
onManagePin,
onShowQr,
}: Props) {
const weekTotal = link.last7Days.reduce((s, d) => s + d.count, 0);
async function copy() {
try {
await navigator.clipboard.writeText(fullShortUrl(link.code));
} catch {
/* ignore */
}
onCopy();
}
return (
<motion.div
layout
initial={{ opacity: 0, y: 24, scale: 0.97 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, scale: 0.9, transition: { duration: 0.2 } }}
transition={{ type: "spring", stiffness: 280, damping: 26 }}
className="card flex flex-col gap-4 p-5 sm:flex-row sm:items-stretch sm:gap-6"
>
{/* Left: identity + actions */}
<div className="flex min-w-0 flex-1 flex-col">
<div className="flex items-center gap-2">
<span className="rounded-full bg-accent px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-accent-ink">
{modeLabel[link.mode]}
</span>
{link.hasPin && (
<span className="rounded-full border-[1.5px] border-line px-2 py-0.5 text-[10px] font-medium text-muted">
🔒 PIN
</span>
)}
<span className="ml-auto text-[11px] text-muted">
{relativeTime(link.createdAt)}
</span>
</div>
<a
href={fullShortUrl(link.code)}
onClick={(e) => e.preventDefault()}
className="focusable mt-2 inline-block rounded font-mono text-lg font-bold text-ink hover:text-accent"
>
{shortUrl(link.code)}
</a>
<p
className="mt-1 truncate text-[13px] text-muted"
title={link.longUrl}
>
{prettyHost(link.longUrl)}
<span className="opacity-60">{new URL(link.longUrl).pathname}</span>
</p>
<div className="mt-auto flex flex-wrap items-center gap-2 pt-4">
<Button size="sm" variant="outline" onClick={copy}>
Copy
</Button>
<Button size="sm" variant="ghost" onClick={() => onShowQr(link)}>
QR
</Button>
<Button size="sm" variant="ghost" onClick={() => onEdit(link)}>
Edit
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => onManagePin(link)}
>
{link.hasPin ? "🔒 PIN" : "Add PIN"}
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => onDelete(link)}
className="ml-auto text-muted hover:!text-red-500"
>
Delete
</Button>
</div>
</div>
{/* Right: stats */}
<div className="flex w-full flex-col rounded-2xl border-[1.5px] border-line bg-surface-2 p-4 sm:w-72">
<div className="mb-3 flex items-end justify-between">
<div>
<p className="text-[11px] font-medium uppercase tracking-wide text-muted">
Total clicks
</p>
<p className="font-display text-2xl font-bold text-ink">
{compactNumber(link.totalClicks)}
</p>
</div>
<div className="text-right">
<p className="text-[11px] font-medium uppercase tracking-wide text-muted">
Last 7 days
</p>
<p className="font-display text-lg font-bold text-accent-ink">
<span className="rounded-md bg-accent px-1.5">
+{compactNumber(weekTotal)}
</span>
</p>
</div>
</div>
<BarChart data={link.last7Days} />
</div>
</motion.div>
);
}