tasks-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 9b0d0f7b9a93
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 f6f2112e77bc8c011e38c83a
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/CalendarView.tsx
// The calendar view: cards on their due dates, a month at a time. On a phone it is a list of days.
import { useState } from "react";
import { useBoard } from "./lib/board";
import { matches, type Filter } from "./lib/filter";
import { Icon } from "./ui";
import { COLORS, dueState, localDay, type Card } from "../functions/lib/tasks";

const sundayFirst = () => /-(US|CA|MX|BR|JP|PH|IL)$/i.test(navigator.language || "");

export function CalendarView({ filter, onOpen }: { filter: Filter; onOpen: (cardId: string) => void }) {
  const b = useBoard();
  const today = localDay(new Date());
  const [month, setMonth] = useState(today.slice(0, 7));
  const [open, setOpen] = useState<string | null>(null);
  const openListIds = new Set(b.openLists.map((l) => l.id));
  const cards = b.cards.filter((c) => openListIds.has(c.listId) && matches(c, filter, b.labels));
  const byDay = new Map<string, Card[]>();
  for (const c of cards) if (c.due) byDay.set(c.due.slice(0, 10), [...(byDay.get(c.due.slice(0, 10)) || []), c]);
  for (const list of byDay.values()) list.sort((a, x) => (a.due < x.due ? -1 : a.due > x.due ? 1 : a.pos - x.pos));
  const noDue = cards.filter((c) => !c.due).length;

  const [y, m] = month.split("-").map(Number);
  const first = new Date(y, m - 1, 1);
  const shift = (first.getDay() - (sundayFirst() ? 0 : 1) + 7) % 7;
  const start = new Date(y, m - 1, 1 - shift);
  const daysInMonth = new Date(y, m, 0).getDate();
  const weeks = Math.ceil((shift + daysInMonth) / 7);
  const cells = Array.from({ length: weeks * 7 }, (_, i) => localDay(new Date(start.getFullYear(), start.getMonth(), start.getDate() + i)));
  const weekdays = Array.from({ length: 7 }, (_, i) => new Date(start.getFullYear(), start.getMonth(), start.getDate() + i).toLocaleDateString(undefined, { weekday: "short" }));
  const go = (n: number) => { const d = new Date(y, m - 1 + n, 1); setMonth(localDay(d).slice(0, 7)); setOpen(null); };
  const title = first.toLocaleDateString(undefined, { month: "long", year: "numeric" });
  const inMonth = (d: string) => d.slice(0, 7) === month;
  const monthDays = cells.filter((d) => inMonth(d) && byDay.has(d));

  const chip = (c: Card) => {
    const s = dueState(c.due, c.done);
    return (
      <button key={c.id} type="button" className={`cal-chip due-${s}`} onClick={() => onOpen(c.id)} title={c.title}>
        {c.cover && <span className="dot" style={{ background: COLORS[c.cover]?.solid }} aria-hidden="true" />}
        {c.due.length === 16 && <span className="cal-time">{c.due.slice(11)}</span>}
        <span className="cal-chip-title">{c.title}</span>
      </button>
    );
  };

  return (
    <div className="calendar">
      <div className="cal-head">
        <h2 className="cal-title">{title}</h2>
        <div className="row" style={{ gap: 6 }}>
          <button type="button" className="icon-btn" onClick={() => go(-1)} aria-label="Previous month"><Icon name="left" /></button>
          <button type="button" className="btn btn-line btn-sm" onClick={() => { setMonth(today.slice(0, 7)); setOpen(null); }}>Today</button>
          <button type="button" className="icon-btn" onClick={() => go(1)} aria-label="Next month"><Icon name="right" /></button>
        </div>
      </div>
      {noDue > 0 && <p className="muted small cal-note">{noDue === 1 ? "1 card has" : `${noDue} cards have`} no due date. Open a card to give it one.</p>}

      <div className="cal-grid" role="grid" aria-label={title}>
        {weekdays.map((w) => <div key={w} className="cal-wd" role="columnheader">{w}</div>)}
        {cells.map((d) => {
          const list = byDay.get(d) || [];
          const expanded = open === d;
          const shown = expanded ? list : list.slice(0, 3);
          return (
            <div key={d} role="gridcell" className={`cal-day${inMonth(d) ? "" : " other"}${d === today ? " today" : ""}`} aria-label={new Date(`${d}T12:00:00`).toLocaleDateString(undefined, { weekday: "long", month: "long", day: "numeric" })}>
              <span className="cal-num">{Number(d.slice(8))}</span>
              {shown.map(chip)}
              {list.length > 3 && <button type="button" className="link-btn cal-more" onClick={() => setOpen(expanded ? null : d)}>{expanded ? "Show less" : `${list.length - 3} more`}</button>}
            </div>
          );
        })}
      </div>

      <div className="cal-agenda">
        {monthDays.length ? monthDays.map((d) => (
          <section key={d} className={`agenda-day${d === today ? " today" : ""}`}>
            <h3 className="agenda-date">{new Date(`${d}T12:00:00`).toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" })}{d === today ? " · Today" : ""}</h3>
            <div className="agenda-cards">{(byDay.get(d) || []).map(chip)}</div>
          </section>
        )) : <p className="muted view-empty">Nothing is due in {first.toLocaleDateString(undefined, { month: "long" })}.</p>}
      </div>
    </div>
  );
}

Change history

Point OpenApps links to openapps.free
9b0d0f7b9a · 18 hours ago
Bind agent address in policy
25f84633c0 · 19 hours ago
Use OpenApps service URLs and add contribution strip
07519eb348 · 19 hours ago
Bounded source sync
3296872001 · 1 day ago
Bounded source sync
ac8d2f5d18 · 1 day ago
Bounded source sync
2a5b08a001 · 1 day ago
Bounded source sync
d2242ddf34 · 1 day ago