lift-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 f9f4e37d6945
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 23e397d8a5d5bf7756362640
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/Shell.tsx
// The app around every page: routes, the tabs (at the bottom on phones, in the header on wider
// screens), the rest timer bar, syncing in the background and keeping the screen on mid-workout.
import { useEffect } from "react";
import { createPortal } from "react-dom";
import { startSync, refresh, takeUpgraded } from "./lib/sync";
import { takeLoginError } from "./lib/bounded";
import { useActive, useStore } from "./lib/store";
import { keepAwake } from "./lib/timer";
import type { Unit } from "./lib/model";
import { A, toast } from "./ui";
import { Dumbbell, Clock, Book, Person, Back } from "./icons";
import { WorkoutPage, ActiveBar } from "./Workout";
import { RoutineEditor, SharedRoutine } from "./Routines";
import { HistoryPage, WorkoutDetail } from "./History";
import { ExercisesPage, ExerciseDetail } from "./Exercises";
import { ProfilePage } from "./Profile";
import { PlateCalc } from "./Plates";

export type Route =
  | { name: "workout" }
  | { name: "routine"; id: string }
  | { name: "shared"; query: URLSearchParams }
  | { name: "history" }
  | { name: "session"; id: string; saved: boolean }
  | { name: "exercises" }
  | { name: "exercise"; id: string }
  | { name: "profile" }
  | { name: "plates"; w?: number; unit?: Unit };

export function routeOf(path: string, query: URLSearchParams): Route | null {
  const p = path.split("/").filter(Boolean).map(decodeURIComponent);
  if (p[0] === "workout") return { name: "workout" };
  if (p[0] === "routines" && p[1]) return { name: "routine", id: p[1] };
  if (p[0] === "r") return { name: "shared", query };
  if (p[0] === "history") return p[1] ? { name: "session", id: p[1], saved: query.has("saved") } : { name: "history" };
  if (p[0] === "exercises") return p[1] ? { name: "exercise", id: p[1] } : { name: "exercises" };
  if (p[0] === "profile") return { name: "profile" };
  if (p[0] === "plates") {
    const w = Number(query.get("w"));
    const u = query.get("unit");
    return { name: "plates", w: w > 0 ? w : undefined, unit: u === "kg" || u === "lb" ? u : undefined };
  }
  return null;
}

const TABS = [
  { href: "/workout", label: "Workout", Icon: Dumbbell, match: ["workout", "routine", "shared"] },
  { href: "/history", label: "History", Icon: Clock, match: ["history", "session"] },
  { href: "/exercises", label: "Exercises", Icon: Book, match: ["exercises", "exercise"] },
  { href: "/profile", label: "Profile", Icon: Person, match: ["profile", "plates"] },
];

const TITLES: Record<Route["name"], string> = {
  workout: "Workout", routine: "Routine", shared: "Routine", history: "History", session: "Workout", exercises: "Exercises", exercise: "Exercise", profile: "Profile", plates: "Plate calculator",
};

function Tabs({ route, className }: { route: Route; className: string }) {
  return (
    <nav className={className} aria-label="Lift">
      {TABS.map(({ href, label, Icon, match }) => (
        <A key={href} href={href} aria-current={match.includes(route.name) ? "page" : undefined}>
          <Icon /> <span>{label}</span>
        </A>
      ))}
    </nav>
  );
}

export function Shell({ route }: { route: Route }) {
  const s = useStore();
  const working = Boolean(useActive().workout);

  useEffect(() => {
    startSync()
      .then(() => {
        if (takeUpgraded()) toast("Your log is saved to your account. Sign in on any device to see it.");
        const err = takeLoginError();
        if (err) toast(err);
      })
      .catch(() => toast("You seem to be offline. Your log is saved on this device."));
    const back = () => { if (document.visibilityState === "visible") void refresh().catch(() => null); };
    document.addEventListener("visibilitychange", back);
    window.addEventListener("online", back);
    return () => { document.removeEventListener("visibilitychange", back); window.removeEventListener("online", back); };
  }, []);

  useEffect(() => { keepAwake(working && s.settings.awake); }, [working, s.settings.awake]);
  useEffect(() => { if (route.name !== "shared") document.title = `${TITLES[route.name]} | Lift`; }, [route.name]);

  let page: React.ReactNode;
  if (route.name === "workout") page = <WorkoutPage />;
  else if (route.name === "routine") page = <RoutineEditor key={route.id} id={route.id} />;
  else if (route.name === "shared") page = <SharedRoutine query={route.query} />;
  else if (route.name === "history") page = <HistoryPage />;
  else if (route.name === "session") page = <WorkoutDetail key={route.id} id={route.id} saved={route.saved} />;
  else if (route.name === "exercises") page = <ExercisesPage />;
  else if (route.name === "exercise") page = <ExerciseDetail key={route.id} id={route.id} />;
  else if (route.name === "profile") page = <ProfilePage />;
  else page = (
    <div className="page">
      <A href="/profile" className="back-link"><Back /> Profile</A>
      <header className="page-head"><div><p className="eyebrow">Tools</p><h1 className="h1">Plate calculator</h1></div></header>
      <section className="card pad"><PlateCalc initial={route.w} unit={route.unit} /></section>
    </div>
  );

  const slot = document.getElementById("app-nav");
  return (
    <div className="shell">
      {slot && createPortal(<Tabs route={route} className="head-tabs" />, slot)}
      {page}
      <div className="dock">
        <ActiveBar onWorkoutPage={route.name === "workout"} />
        <Tabs route={route} className="tabbar" />
      </div>
    </div>
  );
}

Change history

Point OpenApps store links to openapps.free
f9f4e37d69 · 17 hours ago
Add favicon.ico, local share image and openapi.json for links and guides
2796c0063c · 17 hours ago
Bind agent address in policy constants
cf80a3ca66 · 19 hours ago
Add OpenApp contribution strip and OpenApps source links
5e4cc2b9f3 · 19 hours ago
Bounded source sync
ca4a3d3631 · 1 day ago
Bounded source sync
7351063f4d · 1 day ago
Bounded source sync
0ce3238c46 · 1 day ago