feat: first commit

This commit is contained in:
sittichok Ouamsiri committed 2026-06-15 21:25:57 +07:00
commit 3395ab6dd3
88 files changed
+10034

No files matched your search

+118
View File
@@ -0,0 +1,118 @@
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 rounded-2xl border-[1.5px] border-line bg-surface-2 px-4 py-3">
<div className="min-w-0">
<p className="text-[11px] font-medium uppercase tracking-wide text-muted">
Short link
</p>
<p className="truncate font-mono text-base font-bold text-ink">
{shortUrl(link.code)}
</p>
</div>
<span className="shrink-0 rounded-full border-[1.5px] border-line px-2 py-0.5 text-[10px] font-medium text-muted">
can't change
</span>
</div>
<label className="mb-2 block text-sm font-semibold text-ink">
Destination URL
</label>
<div
className={`flex items-center gap-2 rounded-2xl border-[1.5px] bg-surface-2 px-3.5 transition-colors ${
error ? "border-red-400/70" : "border-line"
}`}
>
<span className="text-base opacity-50">🔗</span>
<input
autoFocus
value={draft}
onChange={(e) => {
setDraft(e.target.value);
if (error) setError(null);
}}
onKeyDown={(e) => e.key === "Enter" && save()}
spellCheck={false}
className="h-12 w-full bg-transparent font-mono text-sm text-ink outline-none"
/>
</div>
<p className="mt-2 text-xs text-muted">
Where <span className="font-mono text-ink">{shortUrl(link.code)}</span>{" "}
sends visitors — currently {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-sm 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>
);
}