feedback-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 4a4c54cd95c6
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 54b9c9d7fdc2b68029ce2e57
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/Changelog.tsx
// The changelog: what shipped, newest first, each entry linked to the ideas it delivered so their voters
// see it. Owners write, edit and delete entries.
import { useEffect, useState } from "react";
import { set } from "@bounded-sh/client";
import { explain } from "./lib/bounded";
import { date, plural } from "./lib/format";
import { toast } from "./ui";
import { useBoard, boardPath, Empty } from "./Board";
import { Rich } from "./Post";
import { EntrySheet } from "./Admin";
import { parseIds, type Entry } from "../functions/lib/feedback";

export function Changelog() {
  const { boardId, entries, posts, owner, voted } = useBoard();
  const [editing, setEditing] = useState<Entry | "new" | null>(null);

  useEffect(() => {
    const id = location.hash.slice(1);
    if (id) document.getElementById(`entry-${id}`)?.scrollIntoView({ block: "start" });
  }, []);

  const remove = async (e: Entry) => {
    if (!confirm(`Delete "${e.title}" from the changelog? The ideas it links keep their status.`)) return;
    try { await set(`boards/${boardId}/changelog/${e.id}`, null); toast("Deleted"); } catch (err) { toast(explain(err)); }
  };

  return (
    <div className="changelog">
      {owner && (
        <div className="changelog-bar">
          <p className="muted">Tell people what shipped. Link the ideas it delivered, and their voters see it on each idea.</p>
          <button className="btn" type="button" onClick={() => setEditing("new")}>Write an update</button>
        </div>
      )}
      {entries.length ? (
        <ol className="entries">
          {entries.map((e) => {
            const shipped = parseIds(e.posts).map((id) => posts.find((p) => p.id === id)).filter((p) => p && !p.hidden);
            return (
              <li key={e.id} id={`entry-${e.id}`} className="entry">
                <p className="entry-date">{date(e.at)}</p>
                <div className="entry-main">
                  <h2 className="entry-title">{e.title}</h2>
                  {e.body && <Rich text={e.body} className="entry-body" />}
                  {shipped.length > 0 && (
                    <div className="entry-shipped">
                      <p className="eyebrow">Shipped from the board</p>
                      <ul>
                        {shipped.map((p) => p && (
                          <li key={p.id}>
                            <a href={boardPath(boardId, `/p/${p.id}`)} data-nav>{p.title}</a>
                            <span className="muted"> {plural(p.votes, "vote")}{voted.has(p.id) ? ", including yours" : ""}</span>
                          </li>
                        ))}
                      </ul>
                    </div>
                  )}
                  {owner && (
                    <p className="row entry-actions">
                      <button className="btn btn-sm btn-line" type="button" onClick={() => setEditing(e)}>Edit</button>
                      <button className="btn btn-sm btn-quiet btn-danger" type="button" onClick={() => remove(e)}>Delete</button>
                    </p>
                  )}
                </div>
              </li>
            );
          })}
        </ol>
      ) : (
        <Empty title="No updates yet">
          <p className="muted">{owner ? "Write the first one when something ships." : "When the team ships something, it shows up here."}</p>
        </Empty>
      )}
      {editing && <EntrySheet entry={editing === "new" ? null : editing} onClose={() => setEditing(null)} />}
    </div>
  );
}

Change history

Point store links to openapps.free
4a4c54cd95 · 18 hours ago
Release with preview
1d0728818d · 19 hours ago
Retry release
1abc843b3f · 19 hours ago
Bind app agent address in policy
00eb15f50d · 19 hours ago
Use OpenApps service URLs and add contribution strip
347a7b8ca0 · 19 hours ago
Bounded source sync
a0aaceae63 · 1 day ago
Bounded source sync
3125b8113f · 1 day ago
Bounded source sync
882a76ba95 · 1 day ago
Bounded source sync
852d9782ee · 1 day ago
Bounded source sync
a8ee705109 · 1 day ago