links-free.openapps.xyz

Source

This app’s code and revision history are public. Download a copy or explore the files below.

Public
The app’s code and revision history are public.
Owned app · rev 9e2af813edac
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone 3151231032497ed786b88a0a
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
← All files
src/Edit.tsx
// A link's private edit page (/e/<code>#k=<key>): change where it goes, see clicks per day, turn it off, get the QR code.
import { useEffect, useMemo, useState, type FormEvent, type PointerEvent } from "react";
import { set, useQuery } from "@bounded-sh/client";
import { explain } from "./lib/bounded";
import { navigate } from "./lib/router";
import { openLink, saveLink, savedKey, savedLinks, type Link } from "./lib/mine";
import { copy, share, canShare, toast, Icon } from "./ui";
import { QrPanel } from "./Qr";
import { SITE_URL } from "../functions/lib/config";
import { checkUrl, shortUrl, editUrl, dayNumber } from "../functions/lib/links";

const HOST = SITE_URL.replace(/^https?:\/\//, "");
const DAYS = 30;

function go(e: { preventDefault: () => void }, to: string) {
  e.preventDefault();
  navigate(to);
}

export function EditPage({ code }: { code: string }) {
  const [state, setState] = useState<"opening" | "ready" | "nokey" | "badkey" | "error">("opening");
  const [key, setKey] = useState("");
  const [error, setError] = useState("");
  useEffect(() => {
    const k = new URLSearchParams(location.hash.slice(1)).get("k") || savedKey(code) || "";
    setKey(k);
    if (!k) { setState("nokey"); return; }
    let live = true;
    openLink(code, k).then(
      (link) => {
        if (!live) return;
        if (!link) return setState("badkey");
        saveLink(code, k);
        setState("ready");
      },
      (err) => { if (live) { setError(explain(err)); setState("error"); } },
    );
    return () => { live = false; };
  }, [code]);
  useEffect(() => {
    const before = document.title;
    document.title = `${HOST}/${code} | Links`;
    return () => { document.title = before; };
  }, [code]);

  if (state === "opening") return <EditSkeleton code={code} />;
  if (state === "ready") return <EditView code={code} ownerKey={key} />;
  return (
    <div className="wrap page-pad narrow enter">
      <div className={state === "error" ? "error-state" : "empty-state"}>
        <img className="art" src="/art/card-links.webp" alt="" width="200" height="184" />
        <h1>{state === "nokey" ? "This edit link is missing its key." : state === "badkey" ? "This edit link does not work." : "This link could not be opened."}</h1>
        <p>
          {state === "nokey" ? "Open the full edit link you saved when you made the short link. It ends with #k= and a long code."
            : state === "badkey" ? "The link may not exist, or the edit link was cut short. Check that you copied all of it."
            : error}
        </p>
        {state === "error"
          ? <button className="btn" type="button" onClick={() => location.reload()}>Try again</button>
          : <a className="btn" href="/" onClick={(e) => go(e, "/")}>Make a short link</a>}
      </div>
    </div>
  );
}

function Back() {
  const mine = savedLinks().length > 0;
  return (
    <a className="back-link" href={mine ? "/#mine" : "/"} onClick={(e) => go(e, mine ? "/#mine" : "/")}>
      <Icon name="back" size={16} />{mine ? "Your links" : "Make a short link"}
    </a>
  );
}

function EditSkeleton({ code }: { code: string }) {
  return (
    <div className="edit" aria-busy="true" aria-label="Opening your link">
      <header className="edit-head">
        <div className="wrap edit-head-inner">
          <div className="edit-name">
            <Back />
            <h1 className="edit-title"><span className="short-host">{HOST}/</span>{code}</h1>
          </div>
        </div>
      </header>
      <div className="wrap edit-body">
        <div className="edit-main">
          <div className="card pad"><span className="skeleton" style={{ height: 112 }} /></div>
          <div className="card pad"><span className="skeleton" style={{ height: 196 }} /></div>
        </div>
        <div className="card pad"><span className="skeleton" style={{ height: 220 }} /></div>
      </div>
    </div>
  );
}

function EditView({ code, ownerKey }: { code: string; ownerKey: string }) {
  const { data: link } = useQuery(`links/${code}`) as { data: Link | null | undefined };
  const [draft, setDraft] = useState<string | null>(null);
  const [saving, setSaving] = useState(false);
  const [switching, setSwitching] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const short = shortUrl(code);

  if (link === undefined) return <EditSkeleton code={code} />;
  if (!link) return (
    <div className="wrap page-pad narrow enter">
      <div className="error-state"><h1>This link could not be loaded.</h1><p>Check your connection, then try again.</p>
        <button className="btn" type="button" onClick={() => location.reload()}>Try again</button></div>
    </div>
  );

  const value = draft ?? link.url;
  const changed = draft !== null && draft.trim() !== link.url;
  const save = async (e: FormEvent) => {
    e.preventDefault();
    if (!changed || saving) return;
    const checked = checkUrl(value);
    if ("error" in checked) return setError(checked.error);
    setSaving(true);
    setError(null);
    try {
      await set(`links/${code}`, { url: checked.url });
      setDraft(null);
      toast("Saved. The short link and QR code open the new page now.");
    } catch (err) {
      setError(explain(err));
    } finally {
      setSaving(false);
    }
  };
  const toggle = async () => {
    setSwitching(true);
    try {
      await set(`links/${code}`, { disabled: !link.disabled });
      toast(link.disabled ? "The link is on again." : "The link is off. It opens nothing until you turn it back on.");
    } catch (err) {
      toast(explain(err));
    } finally {
      setSwitching(false);
    }
  };

  return (
    <div className="edit enter">
      <header className="edit-head">
        <div className="wrap edit-head-inner">
          <div className="edit-name">
            <Back />
            <h1 className="edit-title"><span className="short-host">{HOST}/</span>{code}</h1>
          </div>
          <div className="edit-head-actions">
            <button className="btn" type="button" onClick={() => copy(short, "Short link copied")}>Copy link</button>
            {canShare() && <button className="btn btn-line" type="button" onClick={() => share(short, "Short link copied")}><Icon name="share" />Share</button>}
            <a className="btn btn-line" href={short} target="_blank" rel="noopener"><Icon name="open" />Open</a>
          </div>
        </div>
      </header>

      <div className="wrap edit-body">
        <div className="edit-main">
          <form className="card pad stack" onSubmit={save} noValidate>
            <div className="field">
              <label className="label" htmlFor="dest">Where it goes</label>
              <input id="dest" className="input" type="url" inputMode="url" value={value} onChange={(e) => { setDraft(e.target.value); setError(null); }}
                autoComplete="off" autoCapitalize="off" spellCheck={false}
                aria-invalid={error ? true : undefined} aria-describedby={error ? "dest-error" : "dest-hint"} />
              {error ? <p className="field-error" id="dest-error">{error}</p>
                : <small id="dest-hint">Change it any time. The short link and its QR code stay the same, even printed.</small>}
            </div>
            <div className="row">
              <button className="btn" type="submit" disabled={!changed} aria-busy={saving || undefined}><span>Save</span></button>
              {draft !== null && <button className="btn btn-quiet" type="button" onClick={() => { setDraft(null); setError(null); }}>Undo</button>}
            </div>
          </form>

          <Clicks code={code} link={link} />
        </div>

        <div className="edit-side">
          <aside className="card pad edit-qr">
            <h2 className="h3">QR code</h2>
            <QrPanel code={code} />
          </aside>
          <div className="card pad status-card">
            <div className="status-text">
              <span className={`chip ${link.disabled ? "chip-off" : "chip-free"}`}>{link.disabled ? "Off" : "On"}</span>
              <p>{link.disabled ? "People who open it see \"This link was turned off by its owner.\"" : "The link and its QR code open the page you set."}</p>
            </div>
            <button className="btn btn-sm btn-line" type="button" onClick={toggle} aria-busy={switching || undefined}>
              <span>{link.disabled ? "Turn on" : "Turn off"}</span>
            </button>
          </div>
        </div>

        <div className="notice edit-key">
          <p><strong>This page's address is your private edit link.</strong> Bookmark it or keep it somewhere safe. It works on any device, and anyone with it can change the link.</p>
          <button className="btn btn-sm btn-line" type="button" onClick={() => copy(editUrl(code, ownerKey), "Edit link copied")}>Copy edit link</button>
        </div>
      </div>
    </div>
  );
}

const fmtDay = (d: Date) => d.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" });

/** Total clicks, and a bar for each of the last 30 days (UTC). Pointing at a bar reads its day out. */
function Clicks({ code, link }: { code: string; link: Link }) {
  const since = useMemo(() => dayNumber(new Date(Date.now() - (DAYS - 1) * 86400000)), []);
  const { data: rows } = useQuery(`links/${code}/days`, { filter: { d: { $gte: since } }, limit: DAYS + 5 }) as { data: { d: number; clicks: number }[] | undefined };
  const [pick, setPick] = useState<number | null>(null);
  const days = useMemo(() => {
    const byDay = new Map((rows || []).map((r) => [r.d, r.clicks]));
    return Array.from({ length: DAYS }, (_, i) => {
      const date = new Date(Date.now() - (DAYS - 1 - i) * 86400000);
      return { date, clicks: byDay.get(dayNumber(date)) ?? 0 };
    });
  }, [rows]);
  const recent = days.reduce((a, d) => a + d.clicks, 0);
  const max = Math.max(...days.map((d) => d.clicks), 1);
  const point = (e: PointerEvent<HTMLDivElement>) => {
    const box = e.currentTarget.getBoundingClientRect();
    setPick(Math.max(0, Math.min(DAYS - 1, Math.floor(((e.clientX - box.left) / box.width) * DAYS))));
  };
  const picked = pick === null ? null : days[pick];
  const since0 = new Date(link.createdAt * 1000).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });

  return (
    <section className="card pad clicks" aria-labelledby="clicks-title">
      <div className="clicks-head">
        <div>
          <h2 className="label" id="clicks-title">Clicks</h2>
          <p className="clicks-total">{link.clicks.toLocaleString("en-US")}</p>
          <p className="muted small">since {since0}</p>
        </div>
        <p className="clicks-readout" aria-live="polite">
          {rows === undefined ? <span className="skeleton skeleton-text" style={{ width: 120 }} />
            : picked ? <><strong>{picked.clicks.toLocaleString("en-US")}</strong> on {fmtDay(picked.date)}</>
            : <><strong>{recent.toLocaleString("en-US")}</strong> in the last 30 days</>}
        </p>
      </div>
      <div className="chart" onPointerMove={point} onPointerDown={point} onPointerLeave={(e) => { if (e.pointerType === "mouse") setPick(null); }}
        role="img" aria-label={`Clicks per day for the last 30 days: ${recent} in total.`}>
        {recent === 0 && rows !== undefined ? <p className="chart-empty">No clicks in the last 30 days yet. Share the link or print its QR code.</p>
          : days.map((d, i) => (
            <span key={i} className={`bar${pick === i ? " is-picked" : ""}`}>
              {d.clicks > 0 && <i style={{ height: `${Math.max(4, (d.clicks / max) * 100)}%` }} />}
            </span>
          ))}
      </div>
      <div className="chart-axis muted"><span>{fmtDay(days[0].date)}</span><span>Today</span></div>
      <table className="sr-only">
        <caption>Clicks per day, last 30 days (UTC)</caption>
        <tbody>{days.filter((d) => d.clicks).map((d) => <tr key={d.date.toISOString()}><th scope="row">{fmtDay(d.date)}</th><td>{d.clicks}</td></tr>)}</tbody>
      </table>
    </section>
  );
}

Change history

Quality pass: clicks per day, CSV export, links on other devices
9e2af813ed · 5 hours ago
Bounded source sync
29eee7dafa · 1 day ago
Bounded source sync
c5e634046a · 1 day ago
Bounded source sync
12abf93e7e · 1 day ago
Bounded source sync
d0d6a654b1 · 1 day ago
Bounded source sync
e03049f905 · 1 day ago
Bounded source sync
eb5c793bfa · 1 day ago
Bounded source sync
5dd8ec26e1 · 1 day ago