santa-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 b8b257a837b1
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 c7891873549c9ca1004894b3
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/Create.tsx
// The make-an-exchange form in the landing hero. The data layer loads only when someone submits,
// so the landing page stays light.
import { useState, type FormEvent, type KeyboardEvent } from "react";
import { navigate } from "./lib/router";
import { rememberExchange, rememberPerson } from "./lib/mine";
import { LIMITS } from "../functions/lib/santa";

export function Create() {
  const [name, setName] = useState("");
  const [myName, setMyName] = useState("");
  const [people, setPeople] = useState<string[]>([]);
  const [draft, setDraft] = useState("");
  const [budget, setBudget] = useState("");
  const [date, setDate] = useState("");
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const addNames = (raw: string) => {
    const names = raw.split(",").map((s) => s.trim()).filter(Boolean);
    if (!names.length) return;
    setPeople((list) => {
      const next = [...list];
      for (const n of names) if (n.length <= LIMITS.person && !next.some((x) => x.toLowerCase() === n.toLowerCase())) next.push(n);
      return next.slice(0, LIMITS.people);
    });
    setDraft("");
  };

  const onKey = (e: KeyboardEvent<HTMLInputElement>) => {
    if (e.key === "Enter" || e.key === ",") {
      e.preventDefault();
      addNames(draft);
    } else if (e.key === "Backspace" && !draft && people.length) {
      setPeople((l) => l.slice(0, -1));
    }
  };

  const submit = async (e: FormEvent) => {
    e.preventDefault();
    const others = [...people, ...draft.split(",").map((s) => s.trim()).filter(Boolean)];
    const me = myName.trim();
    const all = [...(me ? [me] : []), ...others];
    const lower = all.map((n) => n.toLowerCase());
    const twice = all.find((n, i) => lower.indexOf(n.toLowerCase()) !== i);
    if (twice) return setError(`${twice} is in the list twice. Add a last initial so everyone can tell them apart.`);
    if (all.some((n) => n.length > LIMITS.person)) return setError(`Keep each name under ${LIMITS.person} characters.`);
    if (all.length > LIMITS.people) return setError(`An exchange holds up to ${LIMITS.people} people. Split the group into two exchanges.`);
    setBusy(true);
    setError(null);
    try {
      const { createExchange } = await import("./lib/data");
      const title = name.trim() || "Secret Santa";
      const made = await createExchange({
        name: title,
        budget: budget.trim(),
        date,
        people: [...(me ? [{ name: me, contact: "", mine: true }] : []), ...others.map((n) => ({ name: n, contact: "" }))],
      });
      rememberExchange({ id: made.id, name: title, key: made.key });
      if (made.mine) rememberPerson({ exchangeId: made.id, pid: made.mine.pid, key: made.mine.key, name: made.mine.name, exchangeName: title });
      localStorage.setItem(`santa:new:${made.id}`, "1");
      navigate(`/x/${made.id}/admin`);
    } catch (err) {
      const { explain } = await import("./lib/bounded");
      setError(explain(err));
      setBusy(false);
    }
  };

  return (
    <form className="create card" onSubmit={submit} noValidate aria-label="Make a Secret Santa exchange">
      <label className="field">
        <span>Name of the exchange</span>
        <input className="input" value={name} onChange={(e) => setName(e.target.value)} maxLength={LIMITS.name} placeholder="Family Secret Santa" autoComplete="off" />
      </label>
      <label className="field">
        <span>Your name <em className="optional">if you are taking part</em></span>
        <input className="input" value={myName} onChange={(e) => setMyName(e.target.value)} maxLength={LIMITS.person} placeholder="Sam" autoComplete="given-name" />
      </label>
      <div className="field">
        <label htmlFor="people-input" className="label">Everyone else</label>
        <div className="chips-input" onClick={() => document.getElementById("people-input")?.focus()}>
          {people.map((p) => (
            <span className="person-chip" key={p}>
              {p}
              <button type="button" aria-label={`Remove ${p}`} onClick={() => setPeople((l) => l.filter((x) => x !== p))}>×</button>
            </span>
          ))}
          <input id="people-input" value={draft} onChange={(e) => setDraft(e.target.value)} onKeyDown={onKey} onBlur={() => addNames(draft)}
            placeholder={people.length ? "Add another" : "Alex, Jo, Riley"} autoComplete="off" enterKeyHint="enter" />
        </div>
        <small>Type a name and press Enter. Or skip this and share a link so people add themselves.</small>
      </div>
      <div className="two">
        <label className="field">
          <span>Budget</span>
          <input className="input" value={budget} onChange={(e) => setBudget(e.target.value)} maxLength={LIMITS.budget} placeholder="$25" autoComplete="off" />
        </label>
        <label className="field">
          <span>Gift day</span>
          <input className="input" type="date" value={date} onChange={(e) => setDate(e.target.value)} />
        </label>
      </div>
      {error && <p className="notice notice-error" role="alert">{error}</p>}
      <button className="btn btn-lg btn-block" type="submit" disabled={busy}>{busy ? "Creating..." : "Create the exchange"}</button>
      <p className="hint center">Free, with no sign-up and no ads. Next you set who to keep apart and draw names.</p>
    </form>
  );
}

Change history

Point OpenApps links to openapps.free
b8b257a837 · 18 hours ago
Bind agent address in policy constants
9a72d0200b · 20 hours ago
Use functions.openapps.xyz alias, add OpenApp contribution strip
60038fa63d · 20 hours ago
Bounded source sync
a2e0247232 · 1 day ago
Bounded source sync
c84c736f8b · 1 day ago
Bounded source sync
3a54a80cdc · 1 day ago
Bounded source sync
2eed41b4de · 1 day ago