subs-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 1ae4feb2bebf
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 5af1a249bbbef1db1b4c5ec8
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 hero's quick add: the first subscription goes in right here, no sign-up. Returning
// people see what they pay and what renews next.
import { useState } from "react";
import { useStore, listSubs } from "./lib/store";
import { blank, totals, upcoming } from "./lib/subs";
import { money } from "./lib/money";
import { fmtDay, relDay } from "./lib/dates";
import { navigate } from "./lib/router";
import { Editor, draftFromQuery } from "./Editor";
import { A } from "./ui";
import { Next } from "./icons";

const COMMON = ["Netflix", "Spotify", "Amazon Prime", "Disney+", "YouTube Premium", "iCloud+", "ChatGPT Plus", "Duolingo Super", "Microsoft 365", "Headspace"];

/** One tap names a common service; the editor fills in its website, category and typical price. */
export function QuickPicks({ onPick }: { onPick: (name: string) => void }) {
  return (
    <ul className="picks" aria-label="Common subscriptions">
      {COMMON.map((n) => <li key={n}><button type="button" className="pick" onClick={() => onPick(n)}>{n}</button></li>)}
    </ul>
  );
}

export function Start() {
  const s = useStore();
  const [picked, setPicked] = useState<string | null>(null);
  const subs = listSubs(s);
  if (subs.length) {
    const t = totals(subs);
    const soon = upcoming(subs, 30).slice(0, 4);
    return (
      <div className="start card">
        <p className="eyebrow">Your subscriptions</p>
        <p className="start-total"><strong>{money(t.month, s.currency)}</strong> a month</p>
        <p className="muted small">{money(t.year, s.currency)} a year for {t.count} subscription{t.count === 1 ? "" : "s"}{t.trialCount ? `, plus ${t.trialCount} free trial${t.trialCount === 1 ? "" : "s"}` : ""}.</p>
        {soon.length > 0 && (
          <ul className="start-soon" aria-label="Coming up">
            {soon.map((u) => (
              <li key={u.sub.id + u.date}><span>{u.sub.name}</span><span className="muted">{u.trialEnds ? "trial ends " : ""}{relDay(u.date)} · {fmtDay(u.date, undefined, { month: "short", day: "numeric" })}</span></li>
            ))}
          </ul>
        )}
        <A className="btn btn-lg btn-block" href="/home">Open Subs <Next size={16} /></A>
      </div>
    );
  }
  const initial = picked ? draftFromQuery(new URLSearchParams({ name: picked }), s.currency)! : blank();
  return (
    <div className="start card">
      <p className="start-title">Add what you pay for</p>
      <p className="muted small">Pick one or type any name. No sign-up, no bank login.</p>
      <QuickPicks onPick={setPicked} />
      <Editor key={picked || "blank"} initial={initial} mode="start" onDone={() => navigate("/home")} />
    </div>
  );
}

Change history

Point store links to openapps.free
1ae4feb2be · 18 hours ago
Bind agent address in policy
99bbab3f0c · 19 hours ago
Use OpenApps service URLs and add contribution strip
781c3c23e5 · 19 hours ago
Bounded source sync
048e3257a5 · 1 day ago
Bounded source sync
7a358a3be6 · 1 day ago
Bounded source sync
0c833fcb0e · 1 day ago