when-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 2315d847681e
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 3e1cb86700a5e96238d7a2f1
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/ui.tsx
// Small shared pieces on the kit: dialog (a sheet on phones), dropdown menu, toast, copy, share, download.
import { useEffect, useRef, useState, type ReactNode } from "react";

const X = <svg width="18" height="18" viewBox="0 0 20 20" aria-hidden="true"><path d="M5 5l10 10M15 5 5 15" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" /></svg>;

/** Open and close state for a dialog that animates out: call close(), and onClosed runs once it has left. */
export function useClosing(onClosed: () => void) {
  const [leaving, setLeaving] = useState(false);
  return { leaving, close: () => setLeaving(true), onClosed };
}

export function Dialog({ title, leaving, close, onClosed, children, footer }: {
  title: string; leaving: boolean; close: () => void; onClosed: () => void; children: ReactNode; footer?: ReactNode;
}) {
  const ref = useRef<HTMLDivElement>(null);
  const closeRef = useRef(close);
  closeRef.current = close;
  useEffect(() => {
    const opener = document.activeElement instanceof HTMLElement ? document.activeElement : null;
    const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") closeRef.current(); };
    document.addEventListener("keydown", onKey);
    document.body.style.overflow = "hidden";
    // Focus the dialog itself, so phones do not pop the keyboard; Tab reaches its controls.
    ref.current?.focus();
    return () => {
      document.removeEventListener("keydown", onKey);
      document.body.style.overflow = "";
      opener?.focus({ preventScroll: true });
    };
  }, []);
  // The exit animation ends the dialog; a timer covers a browser that skips it.
  useEffect(() => {
    if (!leaving) return;
    const t = window.setTimeout(onClosed, 320);
    return () => window.clearTimeout(t);
  }, [leaving]); // eslint-disable-line react-hooks/exhaustive-deps
  return (
    <div className={`dialog-backdrop${leaving ? " leaving" : ""}`} onMouseDown={(e) => { if (e.target === e.currentTarget) close(); }}
      onAnimationEnd={(e) => { if (leaving && e.target === ref.current) onClosed(); }}>
      <div className="dialog" role="dialog" aria-modal="true" aria-labelledby="dialog-title" ref={ref} tabIndex={-1}>
        <div className="dialog-head">
          <h2 className="dialog-title" id="dialog-title">{title}</h2>
          <button className="btn btn-quiet btn-icon btn-sm" type="button" onClick={close} aria-label="Close">{X}</button>
        </div>
        <div className="dialog-body">{children}</div>
        {footer && <div className="dialog-foot">{footer}</div>}
      </div>
    </div>
  );
}

/** A menu item runs onSelect, or is a link (href) that opens in a new tab unless it downloads. */
export interface MenuItem { label: string; onSelect?: () => void; href?: string; download?: boolean; danger?: boolean; hidden?: boolean }

/** A button that opens a short menu under it. Arrow keys move between items; Escape closes it. */
export function Menu({ label, items, className = "btn btn-line", align = "end" }: { label: ReactNode; items: (MenuItem | "sep")[]; className?: string; align?: "start" | "end" }) {
  const [open, setOpen] = useState(false);
  const [leaving, setLeaving] = useState(false);
  const wrap = useRef<HTMLDivElement>(null);
  const button = useRef<HTMLButtonElement>(null);
  const shut = (refocus: boolean) => { setLeaving(true); if (refocus) button.current?.focus(); };
  // The exit animation ends the menu; a timer covers a browser that skips it.
  useEffect(() => {
    if (!leaving) return;
    const t = window.setTimeout(() => { setOpen(false); setLeaving(false); }, 220);
    return () => window.clearTimeout(t);
  }, [leaving]);
  useEffect(() => {
    if (!open || leaving) return;
    const away = (e: PointerEvent) => { if (!wrap.current?.contains(e.target as Node)) shut(false); };
    const key = (e: KeyboardEvent) => {
      const list = [...(wrap.current?.querySelectorAll<HTMLElement>(".dropdown-menu [role=menuitem]") ?? [])];
      const at = list.indexOf(document.activeElement as HTMLElement);
      if (e.key === "Escape") { e.preventDefault(); shut(true); }
      else if (e.key === "ArrowDown") { e.preventDefault(); list[(at + 1) % list.length]?.focus(); }
      else if (e.key === "ArrowUp") { e.preventDefault(); list[(at - 1 + list.length) % list.length]?.focus(); }
      else if (e.key === "Tab") shut(false);
    };
    document.addEventListener("pointerdown", away);
    document.addEventListener("keydown", key);
    wrap.current?.querySelector<HTMLElement>(".dropdown-menu [role=menuitem]")?.focus({ preventScroll: true });
    return () => { document.removeEventListener("pointerdown", away); document.removeEventListener("keydown", key); };
  }, [open, leaving]);
  const shown = items.filter((i) => i === "sep" || !i.hidden);
  return (
    <div className="dropdown" ref={wrap}>
      <button ref={button} className={className} type="button" aria-haspopup="menu" aria-expanded={open && !leaving}
        onClick={() => (open && !leaving ? shut(false) : (setLeaving(false), setOpen(true)))}>
        {label}
        <svg width="14" height="14" viewBox="0 0 20 20" aria-hidden="true"><path d="m5.5 8 4.5 4.5L14.5 8" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" /></svg>
      </button>
      {open && (
        <div className={`dropdown-menu${align === "end" ? " dropdown-menu-end" : ""}${leaving ? " leaving" : ""}`} role="menu"
          onAnimationEnd={() => { if (leaving) { setOpen(false); setLeaving(false); } }}>
          {shown.map((item, i) => item === "sep"
            ? <hr key={i} />
            : item.href
              ? <a key={item.label} role="menuitem" href={item.href} {...(item.download ? { download: "" } : { target: "_blank", rel: "noopener noreferrer" })} onClick={() => shut(false)}>{item.label}</a>
              : <button key={item.label} type="button" role="menuitem" className={item.danger ? "danger" : undefined} onClick={() => { shut(true); item.onSelect?.(); }}>{item.label}</button>)}
        </div>
      )}
    </div>
  );
}

type Toast = { text: string; key: number };
let pushToast: ((text: string) => void) | null = null;
export function toast(text: string) { pushToast?.(text); }

export function Toaster() {
  const [t, setT] = useState<Toast | null>(null);
  const [leaving, setLeaving] = useState(false);
  useEffect(() => {
    let timer: number | undefined;
    pushToast = (text) => {
      setT({ text, key: Date.now() });
      setLeaving(false);
      window.clearTimeout(timer);
      timer = window.setTimeout(() => setLeaving(true), 2800);
    };
    return () => { pushToast = null; window.clearTimeout(timer); };
  }, []);
  if (!t) return null;
  return <div key={t.key} className={`toast${leaving ? " leaving" : ""}`} role="status" onAnimationEnd={() => { if (leaving) setT(null); }}>{t.text}</div>;
}

export async function copy(text: string, done = "Copied") {
  try {
    await navigator.clipboard.writeText(text);
    toast(done);
  } catch {
    window.prompt("Copy this:", text);
  }
}

/** The phone's share sheet where there is one; elsewhere, copies the text. */
export async function share(data: { title: string; url?: string; text?: string }, done: string) {
  if (navigator.share && matchMedia("(pointer: coarse)").matches) {
    try { await navigator.share(data); return; } catch (e) { if ((e as Error)?.name === "AbortError") return; }
  }
  await copy([data.text, data.url].filter(Boolean).join(" "), done);
}

/** Saves text as a file on the person's device. */
export function download(name: string, text: string, type: string) {
  const url = URL.createObjectURL(new Blob([text], { type }));
  const a = document.createElement("a");
  a.href = url;
  a.download = name;
  document.body.appendChild(a);
  a.click();
  a.remove();
  window.setTimeout(() => URL.revokeObjectURL(url), 10_000);
}

/** The poll page's shape while it loads, so nothing jumps when it arrives. */
export function PollSkeleton() {
  return (
    <div className="poll" aria-busy="true" aria-label="Loading the poll">
      <header className="poll-head">
        <div className="wrap poll-head-inner">
          <div className="poll-title-block">
            <span className="skeleton" style={{ width: 90, height: 12 }} />
            <span className="skeleton" style={{ width: "min(520px, 80vw)", height: 40, marginTop: 14 }} />
            <span className="skeleton" style={{ width: 200, height: 14, marginTop: 16 }} />
          </div>
        </div>
      </header>
      <div className="wrap poll-body">
        <span className="skeleton" style={{ width: 260, height: 18 }} />
        <span className="skeleton" style={{ width: "100%", maxWidth: 820, height: 300, borderRadius: 16 }} />
      </div>
    </div>
  );
}

Change history

Quality pass: comments, CSV export, share the final time, calendar menu, fixes
2315d84768 · 4 hours ago
Bounded source sync
145324066b · 1 day ago
Bounded source sync
ad9b674dac · 1 day ago
Bounded source sync
e5b69608f3 · 1 day ago
Bounded source sync
a1685914b0 · 1 day ago
Bounded source sync
7108f42d4d · 1 day ago
Bounded source sync
ffb51b672e · 1 day ago
Bounded source sync
23b5d0c53b · 1 day ago