mirror of
https://github.com/ThisTine/Snip.git
synced 2026-10-18 02:51:38 +07:00
feat: first commit
This commit is contained in:
commit
3395ab6dd3
88 files changed
+10034
No files matched your search
@@ -0,0 +1,156 @@
|
||||
import { motion } from "framer-motion";
|
||||
import type { ShortLink } from "../lib/types";
|
||||
import {
|
||||
compactNumber,
|
||||
fullShortUrl,
|
||||
prettyHost,
|
||||
shortUrl,
|
||||
} from "../lib/format";
|
||||
import { Sparkline } from "./Sparkline";
|
||||
|
||||
interface Props {
|
||||
link: ShortLink;
|
||||
onEdit: (link: ShortLink) => void;
|
||||
onDelete: (link: ShortLink) => void;
|
||||
onCopy: () => void;
|
||||
onManagePin: (link: ShortLink) => void;
|
||||
onShowQr: (link: ShortLink) => void;
|
||||
}
|
||||
|
||||
function QrGlyph() {
|
||||
return (
|
||||
<svg
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 15 15"
|
||||
fill="currentColor"
|
||||
aria-hidden
|
||||
>
|
||||
<path d="M1 1h5v5H1V1zm1 1v3h3V2H2z" />
|
||||
<path d="M9 1h5v5H9V1zm1 1v3h3V2h-3z" />
|
||||
<path d="M1 9h5v5H1V9zm1 1v3h3v-3H2z" />
|
||||
<rect x="9" y="9" width="2" height="2" />
|
||||
<rect x="12" y="9" width="2" height="2" />
|
||||
<rect x="9" y="12" width="2" height="2" />
|
||||
<rect x="12" y="12" width="2" height="2" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function IconButton({
|
||||
label,
|
||||
onClick,
|
||||
danger,
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
danger?: boolean;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<motion.button
|
||||
whileTap={{ scale: 0.88 }}
|
||||
whileHover={{ y: -2 }}
|
||||
transition={{ type: "spring", stiffness: 500, damping: 16 }}
|
||||
onClick={onClick}
|
||||
title={label}
|
||||
aria-label={label}
|
||||
className={`focusable grid h-9 w-9 place-items-center rounded-xl border-[1.5px] border-line bg-surface text-sm ${
|
||||
danger ? "hover:!border-red-400 hover:text-red-500" : "hover:bg-surface-2"
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</motion.button>
|
||||
);
|
||||
}
|
||||
|
||||
export function UrlRow({
|
||||
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: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.96, transition: { duration: 0.15 } }}
|
||||
transition={{ type: "spring", stiffness: 320, damping: 28 }}
|
||||
className="card flex flex-col gap-3 px-4 py-3 sm:flex-row sm:items-center sm:gap-4"
|
||||
>
|
||||
{/* identity */}
|
||||
<div className="flex min-w-0 flex-1 items-center gap-3">
|
||||
<span className="hidden h-9 w-9 shrink-0 place-items-center rounded-xl bg-accent text-xs font-bold text-accent-ink sm:grid">
|
||||
{link.mode === "custom" ? "✏️" : link.mode === "memorable" ? "🌿" : "🎲"}
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<a
|
||||
href={fullShortUrl(link.code)}
|
||||
onClick={(e) => e.preventDefault()}
|
||||
className="focusable truncate rounded font-mono text-[15px] font-bold text-ink hover:text-accent"
|
||||
>
|
||||
{shortUrl(link.code)}
|
||||
</a>
|
||||
{link.hasPin && <span className="text-[11px]">🔒</span>}
|
||||
</div>
|
||||
<p className="truncate text-[12px] text-muted" title={link.longUrl}>
|
||||
{prettyHost(link.longUrl)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* stats */}
|
||||
<div className="flex shrink-0 items-center gap-3">
|
||||
<div className="text-right">
|
||||
<p className="font-display text-base font-bold leading-none text-ink">
|
||||
{compactNumber(link.totalClicks)}
|
||||
</p>
|
||||
<p className="text-[10px] font-medium uppercase tracking-wide text-muted">
|
||||
clicks
|
||||
</p>
|
||||
</div>
|
||||
<span className="hidden items-center rounded-md bg-accent/15 px-1 text-[11px] font-bold text-accent-ink sm:inline-flex">
|
||||
+{compactNumber(weekTotal)}
|
||||
</span>
|
||||
<div className="hidden sm:block">
|
||||
<Sparkline data={link.last7Days} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* actions */}
|
||||
<div className="flex shrink-0 items-center gap-1.5">
|
||||
<IconButton label="Copy link" onClick={copy}>
|
||||
⧉
|
||||
</IconButton>
|
||||
<IconButton label="Show QR code" onClick={() => onShowQr(link)}>
|
||||
<QrGlyph />
|
||||
</IconButton>
|
||||
<IconButton label="Manage PIN" onClick={() => onManagePin(link)}>
|
||||
🔒
|
||||
</IconButton>
|
||||
<IconButton label="Edit destination" onClick={() => onEdit(link)}>
|
||||
✏️
|
||||
</IconButton>
|
||||
<IconButton label="Delete link" danger onClick={() => onDelete(link)}>
|
||||
🗑
|
||||
</IconButton>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user