time-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 4857f8a940b5
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 703ed60f2cc5ad3d98a5bf4a
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/Start.tsx
// The landing page's timer: type what you are working on and press Start. The first press makes a
// guest workspace (no sign-up) and opens the app; returning visitors see their running timer.
import { useEffect, useState } from "react";
import { clock } from "../functions/lib/time";
import { session } from "./lib/session";
import { rememberedTimer } from "./lib/data";
import { explain } from "./lib/bounded";
import { navigate } from "./lib/router";
import { Play } from "./ui";
import { useNow } from "./lib/hooks";

const START_KEY = "time:start";

/** A description typed on the landing page, started by the timer page once the workspace is open. */
export function takeLandingStart(): string | null {
  try {
    const v = JSON.parse(sessionStorage.getItem(START_KEY) || "null") as { description?: string } | null;
    sessionStorage.removeItem(START_KEY);
    return v ? String(v.description ?? "") : null;
  } catch {
    return null;
  }
}

export function Start() {
  const [text, setText] = useState("");
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState("");
  const [running] = useState(rememberedTimer);
  const now = useNow(Boolean(running));
  useEffect(() => { if (running) void session().catch(() => null); }, [running]);

  if (running) {
    return (
      <div className="start card">
        <p className="start-label">Your timer is running</p>
        <p className="start-running"><span className="pulse" aria-hidden="true" /><strong>{clock(now - running.start)}</strong> <span className="muted">{running.description || running.projectName || "No description"}</span></p>
        <a className="btn btn-lg btn-block" href="/timer" data-app>Open the timer</a>
        <p className="hint center"><a href="/timesheet" data-app>Timesheet</a> · <a href="/reports" data-app>Reports</a></p>
      </div>
    );
  }

  const start = async () => {
    setBusy(true);
    setError("");
    try {
      await session();
      try { sessionStorage.setItem(START_KEY, JSON.stringify({ description: text.trim() })); } catch { /* the timer page then starts nothing */ }
      navigate("/timer");
    } catch (err) {
      setError(explain(err));
      setBusy(false);
    }
  };
  return (
    <form className="start card" onSubmit={(e) => { e.preventDefault(); void start(); }}>
      <label className="start-label" htmlFor="start-what">What are you working on?</label>
      <input id="start-what" className="input start-input" value={text} maxLength={1000} placeholder="Homepage design for Acme" onChange={(e) => setText(e.target.value)} />
      <button type="submit" className="btn btn-lg btn-block" disabled={busy}><Play size={16} /> {busy ? "Starting..." : "Start the timer"}</button>
      {error && <p className="notice notice-error" role="alert">{error}</p>}
      <p className="hint center">Free. No sign-up. <a href="/timer" data-app>Open the app</a></p>
    </form>
  );
}

Change history

Point OpenApps links to openapps.free
4857f8a940 · 19 hours ago
Bind agent address constants in policy
62b1e6acb2 · 20 hours ago
Use functions.openapps.xyz alias and add OpenApp contribution strip
61d972193a · 20 hours ago
Bounded source sync
fc644b4327 · 1 day ago
Bounded source sync
22f511f3db · 1 day ago
Bounded source sync
351a8d754c · 1 day ago
Bounded source sync
0da238a77e · 1 day ago