mirror of
https://github.com/ThisTine/Snip.git
synced 2026-10-18 02:51:38 +07:00
feat: frontend redesign
This commit is contained in:
1 parent
faf2c97462
commit
e177c7775a
27 files changed
+761
-912
No files matched your search
+29
-58
@@ -7,6 +7,7 @@ import {
|
||||
shortUrl,
|
||||
} from "../lib/format";
|
||||
import { Sparkline } from "./Sparkline";
|
||||
import { Copy, Qr, Lock, Edit, Trash } from "./icons";
|
||||
|
||||
interface Props {
|
||||
link: ShortLink;
|
||||
@@ -17,26 +18,6 @@ interface Props {
|
||||
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,
|
||||
@@ -49,19 +30,16 @@ function IconButton({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<motion.button
|
||||
whileTap={{ scale: 0.88 }}
|
||||
whileHover={{ y: -2 }}
|
||||
transition={{ type: "spring", stiffness: 500, damping: 16 }}
|
||||
<button
|
||||
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"
|
||||
className={`focusable grid h-8 w-8 place-items-center rounded-lg border border-line bg-surface text-muted transition-colors ${
|
||||
danger ? "hover:border-red-400 hover:text-red-500" : "hover:bg-surface-2 hover:text-ink"
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</motion.button>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -87,45 +65,38 @@ export function UrlRow({
|
||||
return (
|
||||
<motion.div
|
||||
layout
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
initial={{ opacity: 0, y: 6 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.96, transition: { duration: 0.15 } }}
|
||||
transition={{ type: "spring", stiffness: 320, damping: 28 }}
|
||||
exit={{ opacity: 0, scale: 0.98, transition: { duration: 0.12 } }}
|
||||
transition={{ duration: 0.18, ease: "easeOut" }}
|
||||
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 className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<a
|
||||
href={fullShortUrl(link.code)}
|
||||
onClick={(e) => e.preventDefault()}
|
||||
className="focusable truncate rounded font-mono text-sm font-semibold text-ink hover:text-accent"
|
||||
>
|
||||
{shortUrl(link.code)}
|
||||
</a>
|
||||
{link.hasPin && <Lock size={12} className="shrink-0 text-muted" />}
|
||||
</div>
|
||||
<p className="truncate text-[12px] text-muted" title={link.longUrl}>
|
||||
{prettyHost(link.longUrl)}
|
||||
</p>
|
||||
</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">
|
||||
<p className="text-sm font-semibold leading-none text-ink">
|
||||
{compactNumber(link.totalClicks)}
|
||||
</p>
|
||||
<p className="text-[10px] font-medium uppercase tracking-wide text-muted">
|
||||
clicks
|
||||
</p>
|
||||
<p className="text-[10px] 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">
|
||||
<span className="hidden text-[12px] font-medium text-accent sm:inline">
|
||||
+{compactNumber(weekTotal)}
|
||||
</span>
|
||||
<div className="hidden sm:block">
|
||||
@@ -136,19 +107,19 @@ export function UrlRow({
|
||||
{/* actions */}
|
||||
<div className="flex shrink-0 items-center gap-1.5">
|
||||
<IconButton label="Copy link" onClick={copy}>
|
||||
⧉
|
||||
<Copy size={15} />
|
||||
</IconButton>
|
||||
<IconButton label="Show QR code" onClick={() => onShowQr(link)}>
|
||||
<QrGlyph />
|
||||
<Qr size={15} />
|
||||
</IconButton>
|
||||
<IconButton label="Manage PIN" onClick={() => onManagePin(link)}>
|
||||
🔒
|
||||
<Lock size={15} />
|
||||
</IconButton>
|
||||
<IconButton label="Edit destination" onClick={() => onEdit(link)}>
|
||||
✏️
|
||||
<Edit size={15} />
|
||||
</IconButton>
|
||||
<IconButton label="Delete link" danger onClick={() => onDelete(link)}>
|
||||
🗑
|
||||
<Trash size={15} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
Reference in new issue
Block a user