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/Join.tsx
// Opening a shared project's link: say what the owner will see, ask for a name, and join.
import { useState } from "react";
import { useStore, joinShare } from "./lib/data";
import { explain } from "./lib/bounded";
import { navigate } from "./lib/router";
import { toast } from "./ui";
import { useJoinedStatus, setJoinedStatus } from "./joined";

export function Join({ share }: { share: string }) {
  const s = useStore();
  const [name, setName] = useState(s.ws.name);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState("");
  const status = useJoinedStatus();
  const mine = s.projects.find((p) => p.share === share);

  if (mine && !mine.team) {
    return (
      <div className="page narrow-page">
        <section className="card join-card">
          <h1 className="h2">This is your project</h1>
          <p className="muted">This is the link you share for {mine.name}. Send it to teammates so they can log time to it.</p>
          <button type="button" className="btn self-start" onClick={() => navigate("/projects")}>Go to projects</button>
        </section>
      </div>
    );
  }
  if (mine && mine.team && !mine.archived && !status.get(mine.id)) {
    return (
      <div className="page narrow-page">
        <section className="card join-card">
          <h1 className="h2">You already joined {mine.name}</h1>
          <p className="muted">Pick it as the project when you start a timer or add time.</p>
          <button type="button" className="btn self-start" onClick={() => navigate("/timer")}>Open the timer</button>
        </section>
      </div>
    );
  }

  const join = async () => {
    setError("");
    if (!name.trim()) return setError("Add your name, so the owner knows whose time it is.");
    setBusy(true);
    try {
      const p = await joinShare(s, share, name.trim().slice(0, 80));
      setJoinedStatus(new Map([...status].filter(([id]) => id !== p.id)));
      toast(`You joined ${p.name}. Pick it when you track time.`);
      navigate("/timer");
    } catch (err) {
      setError(explain(err));
      setBusy(false);
    }
  };

  return (
    <div className="page narrow-page">
      <section className="card join-card">
        <img className="art join-art" src="/art/card-time.webp" alt="" width="160" height="124" />
        <p className="eyebrow">Shared project</p>
        <h1 className="h2">Log your time to a shared project</h1>
        <p className="muted">Someone shared a project with you in Time, a free time tracker. Join it to log your hours to it. No sign-up.</p>
        <ul className="plain-list">
          <li>The project's owner sees your name and the time you log to this project.</li>
          <li>They do not see your other projects, your other time or your rates.</li>
          <li>You can leave at any time from Projects.</li>
        </ul>
        <form className="stack" onSubmit={(e) => { e.preventDefault(); void join(); }}>
          <label className="field"><span>Your name</span><input className="input" value={name} maxLength={80} onChange={(e) => setName(e.target.value)} placeholder="Alex Kim" autoFocus /></label>
          {error && <p className="notice notice-error" role="alert">{error}</p>}
          <button type="submit" className="btn" disabled={busy}>{busy ? "Joining..." : "Join the project"}</button>
        </form>
      </section>
    </div>
  );
}

Change history

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