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/All.tsx
// Every subscription, sorted by the next renewal, the price or the name, with cancelled ones apart.
import { useState } from "react";
import { useStore, listSubs } from "./lib/store";
import { active, CATEGORIES, cycleLabel, nextCharge, onTrial, perMonth, type Sub } from "./lib/subs";
import { useFreeApps, freeAlternative } from "./lib/alternatives";
import { money } from "./lib/money";
import { dayKey, fmtDay, relDay } from "./lib/dates";
import { A } from "./ui";
import { Plus } from "./icons";

type Sort = "next" | "price" | "name";
const SORTS: { v: Sort; label: string }[] = [{ v: "next", label: "Next renewal" }, { v: "price", label: "Price" }, { v: "name", label: "Name" }];
const SORT_KEY = "subs:sort";

function Row({ x, currency, today, hasAlt }: { x: Sub; currency: string; today: string; hasAlt: boolean }) {
  const next = x.cancelled ? null : nextCharge(x, today);
  return (
    <li>
      <A className="sub-row" href={`/s/${x.id}`}>
        <span className="sub-main">
          <strong>{x.name}</strong>
          <span className="sub-meta">
            {x.cancelled ? `Cancelled ${fmtDay(x.cancelled, today)}` : onTrial(x, today) ? `Trial ends ${relDay(x.next, today)}` : `Renews ${relDay(next!, today)}, ${fmtDay(next!, today, { month: "short", day: "numeric" })}`}
            {" · "}{CATEGORIES[x.category]}
          </span>
          {(x.unused || hasAlt || onTrial(x, today)) && !x.cancelled && (
            <span className="sub-tags">
              {onTrial(x, today) && <span className="chip chip-cobalt">Free trial</span>}
              {x.unused && <span className="chip">Barely used</span>}
              {hasAlt && <span className="chip chip-free">Free alternative</span>}
            </span>
          )}
        </span>
        <span className="sub-price">
          <strong>{money(x.amount, currency)}</strong>
          <span>{cycleLabel(x)}</span>
        </span>
      </A>
    </li>
  );
}

export function AllPage() {
  const s = useStore();
  const apps = useFreeApps();
  const today = dayKey();
  const [sort, setSort] = useState<Sort>(() => { try { return (localStorage.getItem(SORT_KEY) as Sort) || "next"; } catch { return "next"; } });
  const subs = listSubs(s);
  const by = (a: Sub, b: Sub) =>
    sort === "price" ? perMonth(b) - perMonth(a) : sort === "name" ? a.name.localeCompare(b.name) : nextCharge(a, today).localeCompare(nextCharge(b, today));
  const current = subs.filter(active).sort(by);
  const cancelled = subs.filter((x) => x.cancelled).sort((a, b) => b.cancelled.localeCompare(a.cancelled));
  const saved = cancelled.reduce((n, x) => n + perMonth(x) * 12, 0);
  return (
    <div className="page all">
      <header className="page-head">
        <div><p className="eyebrow">Subs</p><h1 className="h1">All subscriptions</h1></div>
        <A className="btn head-add" href="/add"><Plus size={18} /> Add</A>
      </header>
      {current.length > 1 && (
        <div className="segmented three" role="radiogroup" aria-label="Sort by">
          {SORTS.map((o) => (
            <button key={o.v} type="button" role="radio" aria-checked={sort === o.v}
              onClick={() => { setSort(o.v); try { localStorage.setItem(SORT_KEY, o.v); } catch { /* fine */ } }}>{o.label}</button>
          ))}
        </div>
      )}
      {current.length ? (
        <ul className="sub-list">{current.map((x) => <Row key={x.id} x={x} currency={s.currency} today={today} hasAlt={Boolean(freeAlternative(x.name, apps))} />)}</ul>
      ) : (
        <div className="empty"><p>No subscriptions yet.</p><A className="btn" href="/add"><Plus size={18} /> Add a subscription</A></div>
      )}
      {cancelled.length > 0 && (
        <section className="stack">
          <h2 className="section-label">Cancelled, saving {money(saved, s.currency)} a year</h2>
          <ul className="sub-list cancelled">{cancelled.map((x) => <Row key={x.id} x={x} currency={s.currency} today={today} hasAlt={false} />)}</ul>
        </section>
      )}
    </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