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/lib/store.ts
// The person's habits and settings. They live on the device, so Habits works offline and opens
// instantly, and sync to the person's Bounded account (src/lib/sync.ts).
import { useSyncExternalStore } from "react";
import { prune, type Habit } from "./habits";

export interface State {
  v: 1;
  owner: string | null; // the account this device copy belongs to
  name: string; // shown to friends on shared habits
  weekStart: 0 | 1; // 0 = Sunday, 1 = Monday
  habits: Record<string, Habit>;
  at: number; // last change to name or weekStart, ms
}

export function fresh(): State {
  const sunday = typeof navigator !== "undefined" && /^en-(US|CA)|^ja|^pt-BR|^he/.test(navigator.language || "");
  return { v: 1, owner: null, name: "", weekStart: sunday ? 0 : 1, habits: {}, at: 0 };
}

const KEY = "habits:v1";

function load(): State {
  try {
    const raw = localStorage.getItem(KEY);
    if (raw) return { ...fresh(), ...JSON.parse(raw) };
  } catch { /* storage blocked or corrupt: start fresh */ }
  return fresh();
}

let state: State = load();
const listeners = new Set<() => void>();
let afterUpdate: (() => void) | null = null;

export const getState = () => state;
export function onLocalChange(fn: () => void) { afterUpdate = fn; }

export function useStore(): State {
  return useSyncExternalStore((cb) => { listeners.add(cb); return () => listeners.delete(cb); }, () => state);
}

/** Replaces the device copy (after a merge with the account's copy). Does not trigger a sync. */
export function replace(next: State) {
  state = next;
  persist();
}

/** Changes state, saves it on the device and schedules a sync. */
export function update(fn: (s: State) => void) {
  const next: State = structuredClone(state);
  fn(next);
  for (const h of Object.values(next.habits)) prune(h);
  state = next;
  persist();
  afterUpdate?.();
}

/** Changes one habit's settings (name, schedule, order...) and stamps the change for merging. */
export function editHabit(id: string, fn: (h: Habit) => void) {
  update((s) => {
    const h = s.habits[id];
    if (!h) return;
    fn(h);
    h.at = Date.now();
  });
}

/** Sets one day's count (and note), stamped for merging. */
export function setDay(id: string, day: string, n: number, note?: string) {
  update((s) => {
    const h = s.habits[id];
    if (!h) return;
    const prev = h.days[day];
    const text = (note === undefined ? prev?.note : note)?.trim();
    h.days[day] = { n: Math.max(0, Math.round(n)), t: Date.now(), ...(text ? { note: text } : {}) };
    if (day < h.start) { h.start = day; h.at = Date.now(); }
  });
}

function persist() {
  try { localStorage.setItem(KEY, JSON.stringify(state)); } catch { /* full or blocked: the account copy still has it */ }
  for (const l of listeners) l();
}

/** Live habits (not deleted), in the person's order. */
export const listHabits = (s: State, archived = false) =>
  Object.values(s.habits).filter((h) => !h.deleted && h.archived === archived).sort((a, b) => a.order - b.order || a.at - b.at);

export const nextOrder = (s: State) => Math.max(0, ...Object.values(s.habits).map((h) => h.order + 1));

Change history

Point OpenApps links to openapps.free
bc888334fc · 18 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