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/CardTile.tsx
// One card as it shows in a list: cover, labels, title, and badges for due date, description, checklist and people.
import type { KeyboardEvent, PointerEvent } from "react";
import { useBoard } from "./lib/board";
import { Icon } from "./ui";
import { COLORS, dueState, parseChecklist, type Card } from "../functions/lib/tasks";

export function dueLabel(due: string): string {
  if (!due) return "";
  const d = new Date(`${due.slice(0, 10)}T12:00:00`);
  const sameYear = d.getFullYear() === new Date().getFullYear();
  const day = d.toLocaleDateString(undefined, { month: "short", day: "numeric", ...(sameYear ? {} : { year: "numeric" }) });
  if (due.length < 16) return day;
  const [h, m] = due.slice(11).split(":").map(Number);
  const t = new Date(2000, 0, 1, h, m).toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });
  return `${day}, ${t}`;
}

export const initials = (name: string) => name.split(/\s+/).filter(Boolean).slice(0, 2).map((w) => w[0]).join("").toUpperCase() || "?";

export function DueBadge({ card }: { card: Card }) {
  if (!card.due) return null;
  const s = dueState(card.due, card.done);
  const words = { overdue: "Overdue", today: "Due today", soon: "Due", later: "Due", done: "Done", none: "" }[s];
  return (
    <span className={`badge due-${s}`} title={`${words}: ${dueLabel(card.due)}`}>
      <Icon name={s === "done" ? "check" : "clock"} size={14} />
      <span className="sr-only">{words}: </span>{dueLabel(card.due)}
    </span>
  );
}

export function CardTile({ card, lifted, dragging, onPointerDown, onClick, onKeyDown }: {
  card: Card; lifted?: boolean; dragging?: boolean;
  onPointerDown?: (e: PointerEvent<HTMLElement>) => void;
  onClick?: () => void;
  onKeyDown?: (e: KeyboardEvent<HTMLElement>) => void;
}) {
  const b = useBoard();
  const labels = b.cardLabels(card);
  const people = b.cardPeople(card);
  const check = parseChecklist(card.checklist);
  const done = check.filter((x) => x.done).length;
  const cover = COLORS[card.cover];
  return (
    <button
      type="button"
      className={`tile${lifted ? " lifted" : ""}${dragging ? " is-dragging" : ""}${cover ? " has-cover" : ""}`}
      data-card={card.id}
      aria-roledescription="card"
      aria-pressed={lifted || undefined}
      onPointerDown={onPointerDown}
      onClick={onClick}
      onKeyDown={onKeyDown}
      onKeyUp={(e) => { if (e.key === " ") e.preventDefault(); }}
      onContextMenu={(e) => { if (matchMedia("(pointer: coarse)").matches) e.preventDefault(); }}
    >
      {cover && <span className="tile-cover" style={{ background: cover.solid }} aria-hidden="true" />}
      {labels.length > 0 && (
        <span className="tile-labels">
          {labels.map((l) => (
            <span key={l.id} className="lbl" style={{ background: COLORS[l.color]?.bg, color: COLORS[l.color]?.fg }}>{l.name || <span className="lbl-swatch" style={{ background: COLORS[l.color]?.solid }} aria-label={COLORS[l.color]?.name} />}</span>
          ))}
        </span>
      )}
      <span className="tile-title">{card.title}</span>
      {(card.due || card.description || check.length > 0 || people.length > 0) && (
        <span className="tile-badges">
          <DueBadge card={card} />
          {card.description && <span className="badge" title="Has a description"><Icon name="text" size={14} /><span className="sr-only">Has a description</span></span>}
          {check.length > 0 && <span className={`badge${done === check.length ? " badge-done" : ""}`} title="Checklist"><Icon name="check" size={14} />{done}/{check.length}</span>}
          {people.length > 0 && (
            <span className="tile-people">
              {people.slice(0, 4).map((p) => <span key={p.id} className="avatar" title={p.name} aria-label={p.name}>{initials(p.name)}</span>)}
              {people.length > 4 && <span className="avatar avatar-more">+{people.length - 4}</span>}
            </span>
          )}
        </span>
      )}
    </button>
  );
}

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