Files
Snip/src/components/EditLinkModal.tsx
T

111 lines
3.5 KiB
TypeScript

import { AnimatePresence, motion } from "framer-motion";
import { useEffect, useState } from "react";
import { api } from "../lib/api";
import type { ShortLink } from "../lib/types";
import { prettyHost, shortUrl } from "../lib/format";
import { isValidUrl, normalizeUrl } from "../lib/shortcode";
import { Modal } from "./ui/Modal";
import { Button } from "./ui/Button";
interface Props {
link: ShortLink | null;
onClose: () => void;
onSaved: (updated: ShortLink, message: string) => void;
}
export function EditLinkModal({ link, onClose, onSaved }: Props) {
const open = Boolean(link);
const [draft, setDraft] = useState("");
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
// Reset the form whenever a different link opens the modal.
useEffect(() => {
setDraft(link?.longUrl ?? "");
setError(null);
setBusy(false);
}, [link?.id, link?.longUrl]);
if (!link) return <Modal open={false} onClose={onClose} children={null} />;
const changed = normalizeUrl(draft) !== link.longUrl;
async function save() {
if (!link) return;
if (!isValidUrl(draft)) {
setError("That doesn't look like a valid URL.");
return;
}
setBusy(true);
setError(null);
try {
const updated = await api.updateLink(link.id, {
longUrl: normalizeUrl(draft),
});
onSaved(updated, "Link updated");
} catch (e) {
setError(e instanceof Error ? e.message : "Could not save changes.");
setBusy(false);
}
}
return (
<Modal open={open} onClose={onClose} title="Edit link">
{/* Short code is permanent — shown read-only so existing shares keep working. */}
<div className="-mt-2 mb-4 flex items-center justify-between gap-3 rounded-xl border border-line bg-surface-2 px-4 py-3">
<div className="min-w-0">
<p className="text-[11px] uppercase tracking-wide text-muted">Short link</p>
<p className="truncate font-mono text-sm font-semibold text-ink">
{shortUrl(link.code)}
</p>
</div>
<span className="shrink-0 rounded-full border border-line px-2 py-0.5 text-[10px] text-muted">
permanent
</span>
</div>
<label className="mb-1.5 block text-[13px] font-medium text-muted">
Destination URL
</label>
<input
autoFocus
value={draft}
onChange={(e) => {
setDraft(e.target.value);
if (error) setError(null);
}}
onKeyDown={(e) => e.key === "Enter" && save()}
spellCheck={false}
className={`h-11 w-full rounded-xl border bg-surface-2 px-3.5 font-mono text-sm text-ink outline-none transition-colors ${
error ? "border-red-400" : "border-line focus:border-[var(--ink)]"
}`}
/>
<p className="mt-2 text-xs text-muted">
Currently points to {prettyHost(link.longUrl)}.
</p>
<AnimatePresence>
{error && (
<motion.p
initial={{ opacity: 0, y: -4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
className="mt-3 text-[13px] font-medium text-red-500"
>
{error}
</motion.p>
)}
</AnimatePresence>
<div className="mt-5 flex items-center gap-2">
<Button onClick={save} disabled={busy || !changed}>
{busy ? "Saving…" : "Save changes"}
</Button>
<Button variant="ghost" onClick={onClose} disabled={busy}>
Cancel
</Button>
</div>
</Modal>
);
}