habits-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 bc888334fc51
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 e8a688fd21351d04b05468df
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), syncing in the background, and the day changing at midnight.
import { useEffect } from "react";
import { createPortal } from "react-dom";
import { startSync, refresh, takeUpgraded } from "./lib/sync";
import { takeLoginError } from "./lib/bounded";
import { A, toast } from "./ui";
import { Today as TodayIcon, Week as WeekIcon, List, Gear } from "./icons";
import { TodayPage } from "./Today";
import { WeekPage } from "./Week";
import { HabitPage } from "./HabitPage";
import { HabitsPage } from "./Habits";
import { SettingsPage } from "./Settings";
import { JoinPage } from "./Join";
import { draftFromQuery, type Draft } from "./Editor";

export type Route =
  | { name: "today"; draft?: Draft }
  | { name: "week" }
  | { name: "habits" }
  | { name: "settings" }
  | { name: "habit"; id: string }
  | { name: "share"; id: string };

export function routeOf(path: string, query: URLSearchParams): Route | null {
  const p = path.split("/").filter(Boolean);
  if (p[0] === "today") return { name: "today" };
  if (p[0] === "new") return { name: "today", draft: draftFromQuery(query) };
  if (p[0] === "week") return { name: "week" };
  if (p[0] === "habits") return { name: "habits" };
  if (p[0] === "settings") return { name: "settings" };
  if (p[0] === "h" && p[1]) return { name: "habit", id: p[1] };
  if (p[0] === "s" && /^[a-z0-9]{20}$/.test(p[1] || "")) return { name: "share", id: p[1] };
  return null;
}

const TABS = [
  { href: "/today", label: "Today", Icon: TodayIcon, match: ["today", "habit"] },
  { href: "/week", label: "Week", Icon: WeekIcon, match: ["week"] },
  { href: "/habits", label: "Habits", Icon: List, match: ["habits"] },
  { href: "/settings", label: "Settings", Icon: Gear, match: ["settings", "share"] },
];

const TITLES: Record<Route["name"], string> = { today: "Today", week: "Week", habits: "Your habits", settings: "Settings", habit: "Habit", share: "Shared habit" };

function Tabs({ route, className }: { route: Route; className: string }) {
  return (
    <nav className={className} aria-label="Habits">
      {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 }) {
  useEffect(() => {
    startSync()
      .then(() => {
        if (takeUpgraded()) toast("Your habits are saved to your account. Sign in on any device to see them.");
        const err = takeLoginError();
        if (err) toast(err);
      })
      .catch(() => toast("You seem to be offline. Your habits are 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(() => { if (route.name !== "habit") document.title = `${TITLES[route.name]} | Habits`; }, [route.name]);

  let page: React.ReactNode;
  if (route.name === "today") page = <TodayPage draft={route.draft} />;
  else if (route.name === "week") page = <WeekPage />;
  else if (route.name === "habits") page = <HabitsPage />;
  else if (route.name === "settings") page = <SettingsPage />;
  else if (route.name === "habit") page = <HabitPage key={route.id} id={route.id} />;
  else page = <JoinPage key={route.id} id={route.id} />;

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

Change history

Point OpenApps links to openapps.free
bc888334fc · 17 hours ago
Bind agent address in policy
1f744a2b83 · 19 hours ago
Use OpenApp service URLs and add contribution strip
a9bb601ee0 · 19 hours ago
Bounded source sync
5646afc3a0 · 1 day ago
Bounded source sync
f1b1bac8d6 · 1 day ago
Bounded source sync
b1e14b41b0 · 1 day ago