@bendyline/gilde 0.1.86 → 0.1.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/craftbook-templates/index.json +1 -1
- package/data/craftbook-templates/raw-index.json +1 -1
- package/data/craftbook-templates/ta/tailor-resume/versions/1.0.5/craftbook.json +226 -0
- package/data/craftbook-templates/ta/tailor-resume/versions/1.0.5/test.json +79 -0
- package/data/knowledge-catalogs/ca/caselaw-alaska-part-0001/manifest.json +24 -0
- package/data/knowledge-catalogs/ca/caselaw-alaska-part-0001/versions/2026.10.1/manifest.json +658 -0
- package/data/knowledge-catalogs/index.json +1 -1
- package/data/knowledge-catalogs/raw-index.json +1 -1
- package/data/project-types/ch/checkers/versions/1.2.1/about.md +9 -0
- package/data/project-types/ch/checkers/versions/1.2.1/game.json +77 -0
- package/data/project-types/ch/checkers/versions/1.2.1/manifest.json +168 -0
- package/data/project-types/ch/checkers/versions/1.2.1/mission.md +9 -0
- package/data/project-types/ch/checkers/versions/1.2.1/pages/board/index.html +1158 -0
- package/data/project-types/ch/chess/versions/1.0.4/about.md +9 -0
- package/data/project-types/ch/chess/versions/1.0.4/game.json +127 -0
- package/data/project-types/ch/chess/versions/1.0.4/manifest.json +182 -0
- package/data/project-types/ch/chess/versions/1.0.4/mission.md +8 -0
- package/data/project-types/ch/chess/versions/1.0.4/pages/board/index.html +851 -0
- package/data/project-types/de/design-scheme/versions/1.0.2/about.md +6 -0
- package/data/project-types/de/design-scheme/versions/1.0.2/manifest.json +70 -0
- package/data/project-types/de/design-scheme/versions/1.0.2/mission.md +3 -0
- package/data/project-types/de/design-scheme/versions/1.0.2/pages/gallery/index.html +212 -0
- package/data/project-types/de/design-scheme/versions/1.0.2/schemes/index.json +9 -0
- package/data/project-types/de/design-scheme/versions/1.0.2/schemes/warm-dusk.json +4 -0
- package/data/project-types/ev/event-planner/versions/1.0.2/about.md +7 -0
- package/data/project-types/ev/event-planner/versions/1.0.2/budget.json +5 -0
- package/data/project-types/ev/event-planner/versions/1.0.2/guests.json +10 -0
- package/data/project-types/ev/event-planner/versions/1.0.2/manifest.json +171 -0
- package/data/project-types/ev/event-planner/versions/1.0.2/mission.md +6 -0
- package/data/project-types/ev/event-planner/versions/1.0.2/pages/dashboard/index.html +283 -0
- package/data/project-types/fi/fitness-coach/versions/1.0.2/about.md +7 -0
- package/data/project-types/fi/fitness-coach/versions/1.0.2/manifest.json +126 -0
- package/data/project-types/fi/fitness-coach/versions/1.0.2/mission.md +7 -0
- package/data/project-types/fi/fitness-coach/versions/1.0.2/pages/dashboard/index.html +273 -0
- package/data/project-types/fi/fitness-coach/versions/1.0.2/training.json +6 -0
- package/data/project-types/fl/flashcards/versions/1.1.1/about.md +8 -0
- package/data/project-types/fl/flashcards/versions/1.1.1/deck.json +7 -0
- package/data/project-types/fl/flashcards/versions/1.1.1/manifest.json +168 -0
- package/data/project-types/fl/flashcards/versions/1.1.1/mission.md +6 -0
- package/data/project-types/fl/flashcards/versions/1.1.1/pages/review/index.html +477 -0
- package/data/project-types/fr/freelance-office/versions/1.0.2/about.md +6 -0
- package/data/project-types/fr/freelance-office/versions/1.0.2/invoices.json +9 -0
- package/data/project-types/fr/freelance-office/versions/1.0.2/manifest.json +143 -0
- package/data/project-types/fr/freelance-office/versions/1.0.2/mission.md +6 -0
- package/data/project-types/fr/freelance-office/versions/1.0.2/pages/dashboard/index.html +258 -0
- package/data/project-types/fu/fundraiser-hq/versions/1.0.2/about.md +7 -0
- package/data/project-types/fu/fundraiser-hq/versions/1.0.2/donations.json +7 -0
- package/data/project-types/fu/fundraiser-hq/versions/1.0.2/manifest.json +127 -0
- package/data/project-types/fu/fundraiser-hq/versions/1.0.2/mission.md +6 -0
- package/data/project-types/fu/fundraiser-hq/versions/1.0.2/pages/dashboard/index.html +261 -0
- package/data/project-types/go/go/versions/1.0.2/about.md +9 -0
- package/data/project-types/go/go/versions/1.0.2/game.json +103 -0
- package/data/project-types/go/go/versions/1.0.2/manifest.json +168 -0
- package/data/project-types/go/go/versions/1.0.2/mission.md +8 -0
- package/data/project-types/go/go/versions/1.0.2/pages/board/index.html +855 -0
- package/data/project-types/ho/household-budget/versions/1.0.3/about.md +6 -0
- package/data/project-types/ho/household-budget/versions/1.0.3/ledger.json +5 -0
- package/data/project-types/ho/household-budget/versions/1.0.3/manifest.json +164 -0
- package/data/project-types/ho/household-budget/versions/1.0.3/mission.md +5 -0
- package/data/project-types/ho/household-budget/versions/1.0.3/pages/dashboard/index.html +340 -0
- package/data/project-types/ho/household-manual/versions/1.0.2/about.md +7 -0
- package/data/project-types/ho/household-manual/versions/1.0.2/manifest.json +153 -0
- package/data/project-types/ho/household-manual/versions/1.0.2/mission.md +8 -0
- package/data/project-types/ho/household-manual/versions/1.0.2/pages/dashboard/index.html +258 -0
- package/data/project-types/ho/household-manual/versions/1.0.2/systems.json +8 -0
- package/data/project-types/index.json +1 -1
- package/data/project-types/jo/job-hunt/versions/1.0.2/about.md +10 -0
- package/data/project-types/jo/job-hunt/versions/1.0.2/activity.json +4 -0
- package/data/project-types/jo/job-hunt/versions/1.0.2/manifest.json +213 -0
- package/data/project-types/jo/job-hunt/versions/1.0.2/mission.md +9 -0
- package/data/project-types/jo/job-hunt/versions/1.0.2/pages/dashboard/index.html +430 -0
- package/data/project-types/jo/job-hunt/versions/1.0.2/pipeline.json +6 -0
- package/data/project-types/la/language-trainer/versions/1.0.2/about.md +6 -0
- package/data/project-types/la/language-trainer/versions/1.0.2/manifest.json +80 -0
- package/data/project-types/la/language-trainer/versions/1.0.2/mission.md +4 -0
- package/data/project-types/la/language-trainer/versions/1.0.2/pages/dashboard/index.html +268 -0
- package/data/project-types/la/language-trainer/versions/1.0.2/progress.json +7 -0
- package/data/project-types/li/life-binder/versions/1.0.3/about.md +7 -0
- package/data/project-types/li/life-binder/versions/1.0.3/documents.json +145 -0
- package/data/project-types/li/life-binder/versions/1.0.3/manifest.json +138 -0
- package/data/project-types/li/life-binder/versions/1.0.3/mission.md +6 -0
- package/data/project-types/li/life-binder/versions/1.0.3/pages/dashboard/index.html +248 -0
- package/data/project-types/me/meal-planner/versions/1.0.2/about.md +6 -0
- package/data/project-types/me/meal-planner/versions/1.0.2/manifest.json +160 -0
- package/data/project-types/me/meal-planner/versions/1.0.2/menu.json +5 -0
- package/data/project-types/me/meal-planner/versions/1.0.2/mission.md +5 -0
- package/data/project-types/me/meal-planner/versions/1.0.2/pages/dashboard/index.html +248 -0
- package/data/project-types/me/meal-planner/versions/1.0.2/pantry.json +9 -0
- package/data/project-types/no/novel-writing-room/versions/1.0.2/about.md +7 -0
- package/data/project-types/no/novel-writing-room/versions/1.0.2/chapters.json +10 -0
- package/data/project-types/no/novel-writing-room/versions/1.0.2/manifest.json +158 -0
- package/data/project-types/no/novel-writing-room/versions/1.0.2/mission.md +7 -0
- package/data/project-types/no/novel-writing-room/versions/1.0.2/pages/dashboard/index.html +264 -0
- package/data/project-types/no/novel-writing-room/versions/1.0.2/sessions.json +6 -0
- package/data/project-types/raw-index.json +1 -1
- package/data/project-types/tr/trip-planner/versions/1.0.2/about.md +7 -0
- package/data/project-types/tr/trip-planner/versions/1.0.2/journal.json +4 -0
- package/data/project-types/tr/trip-planner/versions/1.0.2/manifest.json +194 -0
- package/data/project-types/tr/trip-planner/versions/1.0.2/mission.md +4 -0
- package/data/project-types/tr/trip-planner/versions/1.0.2/packing.json +8 -0
- package/data/project-types/tr/trip-planner/versions/1.0.2/pages/dashboard/index.html +308 -0
- package/data/project-types/tr/trip-planner/versions/1.0.2/trip.json +7 -0
- package/package.json +1 -1
- package/schemas/project-type-version.schema.json +13 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<meta name="gezel-demo" content="standalone" />
|
|
7
|
+
<title>Freelance office</title>
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
color-scheme: light dark;
|
|
11
|
+
--bg: #faf7f2; --card: #fff; --ink: #2b2620; --muted: #7a7166;
|
|
12
|
+
--accent: #b5651d; --track: #ece5da; --good: #4a7c59; --warn: #a4552e;
|
|
13
|
+
}
|
|
14
|
+
:root[data-theme='dark'] {
|
|
15
|
+
--bg: #1c1a17; --card: #262320; --ink: #efe9e0; --muted: #a59a8c;
|
|
16
|
+
--accent: #e0894a; --track: #3a352f; --good: #7fae8d; --warn: #d98a63;
|
|
17
|
+
}
|
|
18
|
+
@media (prefers-color-scheme: dark) {
|
|
19
|
+
:root:not([data-theme='light']) {
|
|
20
|
+
--bg: #1c1a17; --card: #262320; --ink: #efe9e0; --muted: #a59a8c;
|
|
21
|
+
--accent: #e0894a; --track: #3a352f; --good: #7fae8d; --warn: #d98a63;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
* { box-sizing: border-box; }
|
|
25
|
+
body { margin: 0; font: 15px/1.5 system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; background: var(--bg); color: var(--ink); padding: 1.5rem 1.25rem; }
|
|
26
|
+
main { max-width: 820px; margin: 0 auto; }
|
|
27
|
+
h1 { font-family: Georgia, 'PT Serif', 'Times New Roman', serif; font-size: 1.35rem; font-weight: 600; margin: 0 0 0.15rem; }
|
|
28
|
+
.sub { color: var(--muted); font-size: 0.9rem; margin: 0 0 1rem; }
|
|
29
|
+
.tiles { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.6rem; margin-bottom: 1rem; }
|
|
30
|
+
.tile { background: var(--card); border: 1px solid var(--track); border-radius: 10px; padding: 0.7rem 0.85rem; }
|
|
31
|
+
.tile .n { font-size: 1.35rem; font-weight: 700; color: var(--accent); line-height: 1.2; }
|
|
32
|
+
.tile .n.warn { color: var(--warn); }
|
|
33
|
+
.tile .n.good { color: var(--good); }
|
|
34
|
+
.tile .l { font-size: 0.72rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
35
|
+
.board { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.6rem; }
|
|
36
|
+
.col h2 { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); margin: 0 0 0.4rem; font-weight: 600; display: flex; justify-content: space-between; }
|
|
37
|
+
.col h2 .count { color: var(--ink); }
|
|
38
|
+
.card { background: var(--card); border: 1px solid var(--track); border-radius: 6px; padding: 0.5rem 0.6rem; margin-bottom: 0.45rem; font-size: 0.85rem; }
|
|
39
|
+
.card .co { font-weight: 600; overflow-wrap: anywhere; }
|
|
40
|
+
.card .amt { color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
41
|
+
.chip { display: inline-block; border: 1px solid var(--warn); color: var(--warn); border-radius: 4px; padding: 0 0.3rem; font-size: 0.7rem; line-height: 1.4; margin-left: 0.3rem; }
|
|
42
|
+
.empty { color: var(--muted); font-style: italic; background: var(--card); border: 1px dashed var(--track); border-radius: 10px; padding: 0.85rem 1.25rem; text-align: center; margin-bottom: 0.75rem; }
|
|
43
|
+
@media (max-width: 640px) { .board, .tiles { grid-template-columns: 1fr; } }
|
|
44
|
+
.demo-banner {
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
justify-content: space-between;
|
|
48
|
+
gap: 0.75rem;
|
|
49
|
+
margin: 0 0 0.85rem;
|
|
50
|
+
padding: 0.5rem 0.65rem;
|
|
51
|
+
border: 1px solid var(--accent);
|
|
52
|
+
border-radius: 6px;
|
|
53
|
+
background: color-mix(in srgb, var(--card) 88%, var(--accent));
|
|
54
|
+
color: var(--ink);
|
|
55
|
+
font-size: 0.8rem;
|
|
56
|
+
}
|
|
57
|
+
.demo-banner[hidden] { display: none; }
|
|
58
|
+
.demo-banner strong { font-weight: 700; }
|
|
59
|
+
.demo-reset {
|
|
60
|
+
flex: none;
|
|
61
|
+
padding: 0.25rem 0.5rem;
|
|
62
|
+
border: 1px solid var(--accent);
|
|
63
|
+
border-radius: 6px;
|
|
64
|
+
background: var(--card);
|
|
65
|
+
color: var(--ink);
|
|
66
|
+
cursor: pointer;
|
|
67
|
+
font: inherit;
|
|
68
|
+
}
|
|
69
|
+
.demo-reset:hover { border-color: var(--ink); }
|
|
70
|
+
</style>
|
|
71
|
+
</head>
|
|
72
|
+
<body>
|
|
73
|
+
<main>
|
|
74
|
+
<div class="demo-banner" data-gezel-demo-banner hidden>
|
|
75
|
+
<span><strong>Demo mode</strong> — sample data stays in this page.</span>
|
|
76
|
+
<button type="button" class="demo-reset" onclick="location.reload()">Reset demo</button>
|
|
77
|
+
</div>
|
|
78
|
+
<h1>Freelance office</h1>
|
|
79
|
+
<p class="sub" id="sub">Loading the invoice book…</p>
|
|
80
|
+
<div class="tiles">
|
|
81
|
+
<div class="tile"><div class="n" id="t-out">—</div><div class="l">Outstanding</div></div>
|
|
82
|
+
<div class="tile"><div class="n warn" id="t-over">—</div><div class="l">Overdue</div></div>
|
|
83
|
+
<div class="tile"><div class="n good" id="t-paid">—</div><div class="l">Paid this month</div></div>
|
|
84
|
+
</div>
|
|
85
|
+
<div class="empty" id="empty" hidden>No invoices yet — tell your Kantoormeester when you bill something.</div>
|
|
86
|
+
<div class="board" id="board"></div>
|
|
87
|
+
</main>
|
|
88
|
+
<script>
|
|
89
|
+
/* gezel-page-demo-stub v1 */
|
|
90
|
+
/*
|
|
91
|
+
* The canonical demo stub for Gezel type pages. MIT License.
|
|
92
|
+
*
|
|
93
|
+
* Paste this file's bytes verbatim into a <script> block ahead of the
|
|
94
|
+
* page's own code, then call makeDemoGezel({ page, tools, data }) with the
|
|
95
|
+
* page's demo handlers. Served by Gezel, the real window.gezel is already
|
|
96
|
+
* injected and the stub steps aside; opened as a raw file (the live demos
|
|
97
|
+
* on gezelgilde.com), it installs a window.gezel whose tools and data are
|
|
98
|
+
* the page-supplied handler maps — one code path in every mode. `tools`
|
|
99
|
+
* and `data` map a tool name / declared read path to a function; data
|
|
100
|
+
* handlers back read() and list(), and url() returns the handler's value
|
|
101
|
+
* or passes a data: URI through. See docs/page-authoring.md.
|
|
102
|
+
*/
|
|
103
|
+
(function () {
|
|
104
|
+
'use strict';
|
|
105
|
+
window.makeDemoGezel = function (opts) {
|
|
106
|
+
if (window.gezel) return window.gezel;
|
|
107
|
+
var page = (opts && opts.page) || {};
|
|
108
|
+
var tools = (opts && opts.tools) || {};
|
|
109
|
+
var data = (opts && opts.data) || {};
|
|
110
|
+
var themeListeners = [];
|
|
111
|
+
var media = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
|
|
112
|
+
var ui = {
|
|
113
|
+
theme: { mode: media && media.matches ? 'dark' : 'light' },
|
|
114
|
+
onTheme: function (cb) {
|
|
115
|
+
themeListeners.push(cb);
|
|
116
|
+
return function () {
|
|
117
|
+
var at = themeListeners.indexOf(cb);
|
|
118
|
+
if (at >= 0) themeListeners.splice(at, 1);
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
if (media && media.addEventListener) {
|
|
123
|
+
media.addEventListener('change', function (ev) {
|
|
124
|
+
ui.theme = { mode: ev.matches ? 'dark' : 'light' };
|
|
125
|
+
themeListeners.slice().forEach(function (cb) { cb(ui.theme); });
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function fail(code, message) {
|
|
129
|
+
var err = new Error(message);
|
|
130
|
+
err.code = code;
|
|
131
|
+
return Promise.reject(err);
|
|
132
|
+
}
|
|
133
|
+
function run(map, key, arg) {
|
|
134
|
+
if (!Object.prototype.hasOwnProperty.call(map, key)) return null;
|
|
135
|
+
return Promise.resolve().then(function () { return map[key](arg); });
|
|
136
|
+
}
|
|
137
|
+
window.gezel = {
|
|
138
|
+
page: {
|
|
139
|
+
api: 1,
|
|
140
|
+
projectId: 'demo',
|
|
141
|
+
source: 'type',
|
|
142
|
+
entry: page.entry || 'index.html',
|
|
143
|
+
typeName: page.typeName || 'demo',
|
|
144
|
+
params: page.params || {},
|
|
145
|
+
mode: 'demo',
|
|
146
|
+
},
|
|
147
|
+
tools: {
|
|
148
|
+
list: function () { return Promise.resolve(Object.keys(tools)); },
|
|
149
|
+
invoke: function (name, input) {
|
|
150
|
+
var call = run(tools, name, input || {});
|
|
151
|
+
if (!call) return fail('not-allowed', "The demo does not handle the tool '" + name + "'.");
|
|
152
|
+
return call.then(function (output) { return { output: output }; });
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
data: {
|
|
156
|
+
read: function (path, o) {
|
|
157
|
+
return run(data, path, o || {}) || fail('not-allowed', "The demo has no data for '" + path + "'.");
|
|
158
|
+
},
|
|
159
|
+
list: function (path, o) {
|
|
160
|
+
return run(data, path, o || {}) || Promise.resolve([]);
|
|
161
|
+
},
|
|
162
|
+
watch: function () { return function () {}; },
|
|
163
|
+
url: function (path) {
|
|
164
|
+
if (Object.prototype.hasOwnProperty.call(data, path)) return String(data[path]({}));
|
|
165
|
+
return typeof path === 'string' && path.indexOf('data:') === 0 ? path : '';
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
ui: ui,
|
|
169
|
+
refresh: function () {},
|
|
170
|
+
};
|
|
171
|
+
return window.gezel;
|
|
172
|
+
};
|
|
173
|
+
})();
|
|
174
|
+
</script>
|
|
175
|
+
<script>
|
|
176
|
+
(function () {
|
|
177
|
+
'use strict';
|
|
178
|
+
var STAGES = ['draft', 'sent', 'paid'];
|
|
179
|
+
|
|
180
|
+
// ── demo invoice book (used only when the page opens as a raw file) ──
|
|
181
|
+
var demoMonth = new Date().toISOString();
|
|
182
|
+
var demoInvoices = { records: [
|
|
183
|
+
{ id: 'inv-104', status: 'draft', updatedAt: demoMonth, fields: { client: 'Northwind Studio', amountCents: 125000 } },
|
|
184
|
+
{ id: 'inv-103', status: 'sent', updatedAt: demoMonth, fields: { client: 'Juniper Press', amountCents: 240000, dueAt: new Date(Date.now() + 9 * 86400000).toISOString() } },
|
|
185
|
+
{ id: 'inv-102', status: 'sent', updatedAt: demoMonth, fields: { client: 'Canal House', amountCents: 87500, dueAt: new Date(Date.now() - 4 * 86400000).toISOString() } },
|
|
186
|
+
{ id: 'inv-101', status: 'paid', updatedAt: demoMonth, fields: { client: 'Veld Works', amountCents: 162500 } },
|
|
187
|
+
] };
|
|
188
|
+
|
|
189
|
+
makeDemoGezel({
|
|
190
|
+
page: { entry: 'dashboard/index.html', typeName: 'Freelance Office', params: { business: 'freelance work' } },
|
|
191
|
+
data: {
|
|
192
|
+
'invoices.json': function () { return demoInvoices; },
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// ── one code path from here: window.gezel exists in every mode ──
|
|
197
|
+
var demoMode = gezel.page.mode === 'demo';
|
|
198
|
+
|
|
199
|
+
function applyTheme(mode) {
|
|
200
|
+
document.documentElement.setAttribute('data-theme', mode === 'dark' ? 'dark' : 'light');
|
|
201
|
+
}
|
|
202
|
+
applyTheme(gezel.ui.theme.mode);
|
|
203
|
+
gezel.ui.onTheme(function (theme) { applyTheme(theme.mode); });
|
|
204
|
+
|
|
205
|
+
if (demoMode) document.querySelector('[data-gezel-demo-banner]').hidden = false;
|
|
206
|
+
|
|
207
|
+
function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); }
|
|
208
|
+
function fmt(cents) { var abs = Math.abs(cents); return (cents < 0 ? '-' : '') + Math.floor(abs / 100) + '.' + String(abs % 100).padStart(2, '0'); }
|
|
209
|
+
function cap(s) { return s.charAt(0).toUpperCase() + s.slice(1); }
|
|
210
|
+
|
|
211
|
+
function render(book) {
|
|
212
|
+
var records = Array.isArray(book.records) ? book.records : [];
|
|
213
|
+
var now = Date.now();
|
|
214
|
+
var month = new Date().toISOString().slice(0, 7);
|
|
215
|
+
var outstanding = 0, overdue = 0, paidMonth = 0;
|
|
216
|
+
records.forEach(function (r) {
|
|
217
|
+
var cents = (r.fields && r.fields.amountCents) || 0;
|
|
218
|
+
if (r.status === 'sent') {
|
|
219
|
+
outstanding += cents;
|
|
220
|
+
var due = r.fields && r.fields.dueAt ? Date.parse(r.fields.dueAt) : NaN;
|
|
221
|
+
if (isFinite(due) && due < now) overdue += 1;
|
|
222
|
+
}
|
|
223
|
+
if (r.status === 'paid' && r.updatedAt.slice(0, 7) === month) paidMonth += cents;
|
|
224
|
+
});
|
|
225
|
+
document.getElementById('sub').textContent = records.length + ' invoice(s) in the book';
|
|
226
|
+
document.getElementById('t-out').textContent = fmt(outstanding);
|
|
227
|
+
document.getElementById('t-over').textContent = overdue;
|
|
228
|
+
document.getElementById('t-paid').textContent = fmt(paidMonth);
|
|
229
|
+
document.getElementById('empty').hidden = records.length > 0;
|
|
230
|
+
|
|
231
|
+
document.getElementById('board').innerHTML = STAGES.map(function (stage) {
|
|
232
|
+
var here = records.filter(function (r) { return r.status === stage; });
|
|
233
|
+
var cards = here.map(function (r) {
|
|
234
|
+
var due = r.fields && r.fields.dueAt ? Date.parse(r.fields.dueAt) : NaN;
|
|
235
|
+
var isOver = stage === 'sent' && isFinite(due) && due < now;
|
|
236
|
+
return '<div class="card"><div class="co">' + esc((r.fields && r.fields.client) || r.id) +
|
|
237
|
+
(isOver ? '<span class="chip">overdue</span>' : '') + '</div><div class="amt">' +
|
|
238
|
+
fmt((r.fields && r.fields.amountCents) || 0) +
|
|
239
|
+
(r.fields && r.fields.dueAt ? ' · due ' + esc(String(r.fields.dueAt).slice(0, 10)) : '') +
|
|
240
|
+
'</div></div>';
|
|
241
|
+
}).join('');
|
|
242
|
+
return '<div class="col"><h2><span>' + cap(stage) + '</span><span class="count">' + here.length + '</span></h2>' + cards + '</div>';
|
|
243
|
+
}).join('');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function loadBook() {
|
|
247
|
+
return gezel.data
|
|
248
|
+
.read('invoices.json')
|
|
249
|
+
.then(function (next) { if (next) render(next); })
|
|
250
|
+
.catch(function () {});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
loadBook();
|
|
254
|
+
gezel.data.watch('invoices.json', function () { loadBook(); });
|
|
255
|
+
})();
|
|
256
|
+
</script>
|
|
257
|
+
</body>
|
|
258
|
+
</html>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Headquarters for the drive: **{{cause}}**. Your Aanjager keeps the thermometer
|
|
2
|
+
honest — every gift recorded the moment it lands, every donor thanked before the
|
|
3
|
+
drive forgets — and drafts the outreach and progress posts that keep momentum
|
|
4
|
+
visible.
|
|
5
|
+
|
|
6
|
+
The **Output** tab is the thermometer: raised against the goal, the latest
|
|
7
|
+
gifts, and the thank-yous still owed.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"releasedAt": "2026-10-06T00:00:00Z",
|
|
5
|
+
"meesterManaged": false,
|
|
6
|
+
"params": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"cause": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"title": "What are you raising for?",
|
|
12
|
+
"description": "e.g. 'new jerseys for the team', 'the library roof'.",
|
|
13
|
+
"default": "a good cause"
|
|
14
|
+
},
|
|
15
|
+
"goalCents": {
|
|
16
|
+
"type": "number",
|
|
17
|
+
"title": "Goal (in cents)",
|
|
18
|
+
"description": "The target, in cents — e.g. 500000 for 5,000.00.",
|
|
19
|
+
"default": 100000
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"required": [
|
|
23
|
+
"cause",
|
|
24
|
+
"goalCents"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"nameTemplate": "Fundraiser: {{cause}}",
|
|
28
|
+
"aboutTemplate": "about.md",
|
|
29
|
+
"missionTemplate": "mission.md",
|
|
30
|
+
"gezels": [
|
|
31
|
+
{
|
|
32
|
+
"templateId": "aanjager",
|
|
33
|
+
"voorman": true
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"tools": [
|
|
37
|
+
{
|
|
38
|
+
"name": "record_donation",
|
|
39
|
+
"description": "Record a gift the moment it lands (ordinary units, donor name).",
|
|
40
|
+
"script": "drive-store",
|
|
41
|
+
"inputs": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"properties": {
|
|
44
|
+
"donor": {
|
|
45
|
+
"type": "string"
|
|
46
|
+
},
|
|
47
|
+
"amount": {
|
|
48
|
+
"type": "number"
|
|
49
|
+
},
|
|
50
|
+
"note": {
|
|
51
|
+
"type": "string"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"required": [
|
|
55
|
+
"amount"
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
"bind": {
|
|
59
|
+
"action": "record_donation"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "mark_thanked",
|
|
64
|
+
"description": "Record that a thank-you went out for a gift.",
|
|
65
|
+
"script": "drive-store",
|
|
66
|
+
"inputs": {
|
|
67
|
+
"type": "object",
|
|
68
|
+
"properties": {
|
|
69
|
+
"id": {
|
|
70
|
+
"type": "string"
|
|
71
|
+
},
|
|
72
|
+
"donor": {
|
|
73
|
+
"type": "string"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"bind": {
|
|
78
|
+
"action": "mark_thanked"
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"name": "drive_status",
|
|
83
|
+
"description": "The thermometer and the thank-you debt.",
|
|
84
|
+
"script": "drive-store",
|
|
85
|
+
"inputs": {
|
|
86
|
+
"type": "object",
|
|
87
|
+
"properties": {}
|
|
88
|
+
},
|
|
89
|
+
"bind": {
|
|
90
|
+
"action": "status"
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"name": "donor_list",
|
|
95
|
+
"description": "Every gift, with thank-you state.",
|
|
96
|
+
"script": "drive-store",
|
|
97
|
+
"inputs": {
|
|
98
|
+
"type": "object",
|
|
99
|
+
"properties": {}
|
|
100
|
+
},
|
|
101
|
+
"bind": {
|
|
102
|
+
"action": "donor_list"
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
"craftbooks": [
|
|
107
|
+
"social-thread",
|
|
108
|
+
"drive-wrap-up"
|
|
109
|
+
],
|
|
110
|
+
"schedules": [],
|
|
111
|
+
"pages": {
|
|
112
|
+
"entry": "dashboard/index.html",
|
|
113
|
+
"api": 1,
|
|
114
|
+
"reads": [
|
|
115
|
+
{
|
|
116
|
+
"source": "workspace",
|
|
117
|
+
"path": "donations.json"
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
},
|
|
121
|
+
"workspaceSeed": [
|
|
122
|
+
"donations.json"
|
|
123
|
+
],
|
|
124
|
+
"scripts": {
|
|
125
|
+
"drive-store": "import { type InferredInput, defineScript, gezel } from '@bendyline/gezel-sdk';\nimport { ledgerStore } from '@bendyline/gezel-sdk/stores';\n\nexport const meta = defineScript({\n name: 'drive-store',\n description:\n \"Read and update the fundraising drive (donations.json). Call action:'status' for the thermometer + unthanked list; 'record_donation' the moment a gift lands (ordinary units, donor name); 'mark_thanked' once the thank-you goes out.\",\n inputs: {\n action: {\n type: 'string',\n description: \"One of 'status', 'record_donation', 'mark_thanked', or 'donor_list'.\",\n required: true,\n },\n donor: { type: 'string', description: \"Donor name ('anonymous' is fine).\" },\n amount: { type: 'number', description: 'Gift amount in ordinary units.' },\n note: { type: 'string', description: 'Anything worth remembering about the gift.' },\n id: { type: 'string', description: \"Donation id, e.g. 'ledger-4'.\" },\n },\n outputs: {\n raisedCents: { type: 'number', description: 'Total raised, in cents.' },\n donors: { type: 'number', description: 'Number of gifts.' },\n unthanked: { type: 'number', description: 'Gifts still awaiting a thank-you.' },\n summary: { type: 'string', description: 'The drive picture.' },\n },\n requires: ['workspace.read', 'workspace.write'],\n} as const);\n\nconst input = gezel.input as InferredInput<typeof meta>;\n\n// ── drive-rules-start ──\n// Pure derivations over ledgerStore-shaped donation entries: entries are\n// kind 'in', category 'donation', data {donor, thanked}.\n\nfunction toCents(amount) {\n if (typeof amount !== 'number' || !isFinite(amount) || amount <= 0) return null;\n return Math.round(amount * 100);\n}\n\nfunction fmtCents(cents) {\n var abs = Math.abs(cents);\n return (cents < 0 ? '-' : '') + Math.floor(abs / 100) + '.' + String(abs % 100).padStart(2, '0');\n}\n\nfunction driveShape(entries) {\n var raised = 0;\n var unthanked = [];\n entries.forEach(function (e) {\n raised += e.amountCents;\n if (!e.data || e.data.thanked !== true) unthanked.push(e);\n });\n return { raised: raised, unthanked: unthanked };\n}\n\nfunction driveLine(entries, goalCents) {\n var shape = driveShape(entries);\n var pct = goalCents > 0 ? Math.min(100, Math.round((shape.raised / goalCents) * 100)) : null;\n return (\n 'Raised ' +\n fmtCents(shape.raised) +\n (goalCents > 0 ? ' of ' + fmtCents(goalCents) + ' (' + pct + ' percent)' : '') +\n ' across ' +\n entries.length +\n ' gift(s). ' +\n (shape.unthanked.length === 0\n ? 'Everyone thanked.'\n : shape.unthanked.length + ' thank-you(s) still owed.')\n );\n}\n// ── drive-rules-end ──\n\nconst donations = ledgerStore(gezel.fs, 'donations.json');\n\nasync function readGoalCents(): Promise<number> {\n try {\n const parsed = JSON.parse(await gezel.fs.read('donations.json')) as { goalCents?: unknown };\n const goal = Number(parsed.goalCents);\n return Number.isFinite(goal) && goal > 0 ? Math.round(goal) : 0;\n } catch {\n return 0;\n }\n}\n\nif (input.action === 'record_donation') {\n const cents = toCents(input.amount);\n if (cents === null) throw new Error(\"record_donation needs a positive 'amount' (ordinary units).\");\n await donations.add({\n amountCents: cents,\n kind: 'in',\n category: 'donation',\n ...(input.note ? { note: input.note } : {}),\n data: { donor: input.donor || 'anonymous', thanked: false },\n });\n} else if (input.action === 'mark_thanked') {\n if (!input.id && !input.donor) throw new Error(\"Pass 'id' (e.g. 'ledger-4') or a unique 'donor'.\");\n // ledgerStore is append-only by design; thanked flips via a targeted\n // read-modify-write on the entry's data (the one mutation this drive needs).\n const raw = JSON.parse(await gezel.fs.read('donations.json')) as {\n entries: Array<{ id: string; data?: { donor?: string; thanked?: boolean } }>;\n };\n const matches = raw.entries.filter((e) =>\n input.id\n ? e.id === input.id\n : String(e.data?.donor ?? '').toLowerCase() === String(input.donor).toLowerCase() &&\n e.data?.thanked !== true,\n );\n if (matches.length === 0) throw new Error('No matching unthanked donation found.');\n if (matches.length > 1 && !input.id) {\n throw new Error(\n `Multiple unthanked gifts from '${input.donor}': ${matches.map((m) => m.id).join(', ')}. Pass id.`,\n );\n }\n matches[0]!.data = { ...(matches[0]!.data ?? {}), thanked: true };\n await gezel.fs.write('donations.json', `${JSON.stringify(raw, null, 2)}\\n`);\n} else if (input.action !== 'status' && input.action !== 'donor_list') {\n throw new Error(\n `Unknown action '${input.action}'. Use status, record_donation, mark_thanked, or donor_list.`,\n );\n}\n\nconst entries = await donations.list();\nconst goalCents = await readGoalCents();\nconst shape = driveShape(entries);\nconst listing =\n input.action === 'donor_list'\n ? entries\n .map(\n (e) =>\n `${e.id} ${fmtCents(e.amountCents)} from ${String(e.data?.donor ?? 'anonymous')}${e.data?.thanked === true ? '' : ' (thank-you owed)'}`,\n )\n .join('\\n') || 'No gifts yet.'\n : '';\n\ngezel.output({\n raisedCents: shape.raised,\n donors: entries.length,\n unthanked: shape.unthanked.length,\n summary: listing || driveLine(entries, goalCents),\n});\n"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
- Reach the goal for {{cause}} — the thermometer tells the truth the whole way.
|
|
2
|
+
- Every gift is recorded the day it lands; every donor is thanked, tracked to
|
|
3
|
+
done.
|
|
4
|
+
- Outreach stays warm and specific — what the money does, never guilt.
|
|
5
|
+
- The wrap-up report names the total, the impact, and the people, ready for the
|
|
6
|
+
next organizer.
|