mirror of
https://github.com/ThisTine/Snip.git
synced 2026-08-18 23:18:47 +07:00
feat: first commit
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user