Spaces:
Running
Running
Deploy billsprig to the hub
Browse files- billsprig/app.js +54 -4
- billsprig/index.html +1 -1
- billsprig/lib/pdf.js +6 -3
- billsprig/style.css +6 -0
- billsprig/sw.js +1 -1
billsprig/app.js
CHANGED
|
@@ -4,7 +4,12 @@
|
|
| 4 |
"use strict";
|
| 5 |
var C = window.BillCalc, P = window.BillPDF, X = window.CMXPro;
|
| 6 |
var KEY = "billsprig:v1";
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
var FREE_PDFS = 5;
|
| 9 |
var $ = function (id) { return document.getElementById(id); };
|
| 10 |
var app = $("app"), form = $("editor");
|
|
@@ -247,7 +252,7 @@
|
|
| 247 |
'<section class="d-bottom"><div class="d-left">' + (payOk ? '<a class="d-pay" href="' + esc(d.payLink) + '" target="_blank" rel="noopener">Pay online</a><span class="d-paylink">' + esc(d.payLink) + "</span>" : "") +
|
| 248 |
(d.notes ? '<div class="d-label">Notes</div><div class="d-notes">' + esc(d.notes) + "</div>" : "") + "</div>" +
|
| 249 |
'<table class="d-totals">' + tot.map(function (r) { return "<tr><td>" + r[0] + "</td><td>" + r[1] + "</td></tr>"; }).join("") + '<tr class="t-total"><td>' + totalLabel + "</td><td>" + m(T.total) + "</td></tr></table></section>" +
|
| 250 |
-
'<footer class="d-credit">
|
| 251 |
fit();
|
| 252 |
}
|
| 253 |
function fit() {
|
|
@@ -284,7 +289,7 @@
|
|
| 284 |
function pdfBytes() {
|
| 285 |
var d = C.clone(doc); d.template = tplFor();
|
| 286 |
var logo = pro() && S.logo && S.logo.data ? { bytes: b64ToBytes(S.logo.data), w: S.logo.w, h: S.logo.h } : null;
|
| 287 |
-
return P.build(d, { credit: !pro(), creditUrl:
|
| 288 |
}
|
| 289 |
$("btn-pdf").addEventListener("click", function () {
|
| 290 |
var q = X.quota("pdf", FREE_PDFS);
|
|
@@ -499,9 +504,54 @@
|
|
| 499 |
}
|
| 500 |
applyPreset();
|
| 501 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 502 |
/* ---------- start ---------- */
|
| 503 |
fillSelects();
|
| 504 |
toForm();
|
|
|
|
| 505 |
X.onChange(onPro);
|
| 506 |
-
window.Billsprig = { state: function () { return S; }, doc: function () { return doc; }, pdf: pdfBytes };
|
| 507 |
})();
|
|
|
|
| 4 |
"use strict";
|
| 5 |
var C = window.BillCalc, P = window.BillPDF, X = window.CMXPro;
|
| 6 |
var KEY = "billsprig:v1";
|
| 7 |
+
/* Recipient-facing credit on free output (Pro removes it): links to the app's page on cybermaxtools.com, tagged so the
|
| 8 |
+
* page's view counter records it (utm_source=billsprig-share, utm_campaign=pdf|print). */
|
| 9 |
+
var SITE = "https://cybermaxtools.com/apps/free-invoice-generator/";
|
| 10 |
+
var CREDIT_LEAD = "Made with Billsprig by CyberMax", CREDIT_ASK = "make your own free: cybermaxtools.com/apps/free-invoice-generator";
|
| 11 |
+
var CREDIT = CREDIT_LEAD + " \u2014 " + CREDIT_ASK;
|
| 12 |
+
function creditUrl(ch) { return SITE + "?utm_source=billsprig-share&utm_medium=made-with&utm_campaign=" + ch; }
|
| 13 |
var FREE_PDFS = 5;
|
| 14 |
var $ = function (id) { return document.getElementById(id); };
|
| 15 |
var app = $("app"), form = $("editor");
|
|
|
|
| 252 |
'<section class="d-bottom"><div class="d-left">' + (payOk ? '<a class="d-pay" href="' + esc(d.payLink) + '" target="_blank" rel="noopener">Pay online</a><span class="d-paylink">' + esc(d.payLink) + "</span>" : "") +
|
| 253 |
(d.notes ? '<div class="d-label">Notes</div><div class="d-notes">' + esc(d.notes) + "</div>" : "") + "</div>" +
|
| 254 |
'<table class="d-totals">' + tot.map(function (r) { return "<tr><td>" + r[0] + "</td><td>" + r[1] + "</td></tr>"; }).join("") + '<tr class="t-total"><td>' + totalLabel + "</td><td>" + m(T.total) + "</td></tr></table></section>" +
|
| 255 |
+
(pro() ? "" : '<footer class="d-credit">' + CREDIT_LEAD + ' \u2014 <a href="' + creditUrl("print") + '" target="_blank" rel="noopener">' + CREDIT_ASK + "</a></footer>");
|
| 256 |
fit();
|
| 257 |
}
|
| 258 |
function fit() {
|
|
|
|
| 289 |
function pdfBytes() {
|
| 290 |
var d = C.clone(doc); d.template = tplFor();
|
| 291 |
var logo = pro() && S.logo && S.logo.data ? { bytes: b64ToBytes(S.logo.data), w: S.logo.w, h: S.logo.h } : null;
|
| 292 |
+
return P.build(d, { credit: !pro(), creditText: CREDIT, creditUrl: creditUrl("pdf"), logo: logo, accent: accentFor() });
|
| 293 |
}
|
| 294 |
$("btn-pdf").addEventListener("click", function () {
|
| 295 |
var q = X.quota("pdf", FREE_PDFS);
|
|
|
|
| 504 |
}
|
| 505 |
applyPreset();
|
| 506 |
|
| 507 |
+
/* ---------- "Use this template": guide pages show the app's own worked examples; each table gets a button that loads
|
| 508 |
+
* that example into the editor above as the visitor's own copy. Also #example=<kind> (invoice, contractor, freelancer,
|
| 509 |
+
* receipt) opens the app pre-filled with one, so a guide or a shared link can say "start from this". Nothing leaves
|
| 510 |
+
* the browser. ---------- */
|
| 511 |
+
var KINDS = Array.prototype.map.call($("example-kind").options, function (o) { return o.value; });
|
| 512 |
+
function ownDraft() {
|
| 513 |
+
var cur = S.doc; if (!cur || !cur.to || !String(cur.to.name || "").trim()) return false;
|
| 514 |
+
return !/\.example\b/.test([cur.to.email, cur.from && cur.from.email, cur.payLink].join(" ")); // the built-in examples use .example addresses
|
| 515 |
+
}
|
| 516 |
+
function useTemplate(kind, quiet) {
|
| 517 |
+
if (KINDS.indexOf(kind) < 0) return false;
|
| 518 |
+
if (ownDraft() && !window.confirm("Replace your current draft with this template? Save it to history first if you need it.")) return false;
|
| 519 |
+
$("example-kind").value = kind;
|
| 520 |
+
S.doc = doc = C.example(kind);
|
| 521 |
+
save();
|
| 522 |
+
if (!quiet) {
|
| 523 |
+
toForm(); changed(); renderPreview();
|
| 524 |
+
toast("Template loaded as your own copy. Change anything; it saves in this browser.");
|
| 525 |
+
$("app").scrollIntoView({ behavior: "smooth", block: "start" });
|
| 526 |
+
}
|
| 527 |
+
return true;
|
| 528 |
+
}
|
| 529 |
+
function templateButtons() {
|
| 530 |
+
Array.prototype.forEach.call(document.querySelectorAll(".g-guide .g-tw"), function (box) {
|
| 531 |
+
var td = box.querySelector("tbody td"), first = td ? td.textContent.trim() : "";
|
| 532 |
+
var kind = KINDS.filter(function (k) { return C.example(k).items[0].desc === first; })[0];
|
| 533 |
+
if (!kind || (box.nextElementSibling && box.nextElementSibling.classList.contains("g-use"))) return;
|
| 534 |
+
var p = document.createElement("p");
|
| 535 |
+
p.className = "g-use";
|
| 536 |
+
p.innerHTML = '<button type="button" class="btn sm" data-use="' + kind + '">Use this template</button> <span>Opens it in the editor above as your own copy to edit. Nothing is uploaded.</span>';
|
| 537 |
+
box.parentNode.insertBefore(p, box.nextSibling);
|
| 538 |
+
});
|
| 539 |
+
}
|
| 540 |
+
document.addEventListener("click", function (e) {
|
| 541 |
+
var b = e.target.closest && e.target.closest("[data-use]");
|
| 542 |
+
if (b) useTemplate(b.getAttribute("data-use"));
|
| 543 |
+
});
|
| 544 |
+
(function exampleFromHash() {
|
| 545 |
+
var m = /[#&]example=([a-z]+)/.exec(location.hash || "");
|
| 546 |
+
if (!m) return;
|
| 547 |
+
history.replaceState(null, "", location.pathname + location.search);
|
| 548 |
+
if (useTemplate(m[1], true)) setTimeout(function () { toast("Template loaded as your own copy. Change anything; it saves in this browser."); }, 400);
|
| 549 |
+
})();
|
| 550 |
+
|
| 551 |
/* ---------- start ---------- */
|
| 552 |
fillSelects();
|
| 553 |
toForm();
|
| 554 |
+
templateButtons();
|
| 555 |
X.onChange(onPro);
|
| 556 |
+
window.Billsprig = { state: function () { return S; }, doc: function () { return doc; }, pdf: pdfBytes, useTemplate: useTemplate, CREDIT: CREDIT, creditUrl: creditUrl };
|
| 557 |
})();
|
billsprig/index.html
CHANGED
|
@@ -267,5 +267,5 @@
|
|
| 267 |
<script src="lib/calc.js"></script>
|
| 268 |
<script src="lib/pdf.js"></script>
|
| 269 |
<script src="app.js"></script>
|
| 270 |
-
<
|
| 271 |
</html>
|
|
|
|
| 267 |
<script src="lib/calc.js"></script>
|
| 268 |
<script src="lib/pdf.js"></script>
|
| 269 |
<script src="app.js"></script>
|
| 270 |
+
</body>
|
| 271 |
</html>
|
billsprig/lib/pdf.js
CHANGED
|
@@ -177,7 +177,7 @@
|
|
| 177 |
function pct(v) { return String(C.round(v, 3)) + "%"; }
|
| 178 |
|
| 179 |
/**
|
| 180 |
-
* Build the PDF for a document. opts: { credit: bool, creditUrl, logo: {bytes, w, h}, accent, date: Date }
|
| 181 |
* Returns a Uint8Array.
|
| 182 |
*/
|
| 183 |
function build(doc, opts) {
|
|
@@ -342,7 +342,7 @@
|
|
| 342 |
for (var i = 0; i < n; i++) {
|
| 343 |
wr.on(i);
|
| 344 |
if (opts.credit) {
|
| 345 |
-
var credit =
|
| 346 |
var cw = wr.text(L, H - 28, credit, { size: 7.5, color: "#8A8F9C" });
|
| 347 |
if (opts.creditUrl) wr.link(L, H - 36, cw, 11, opts.creditUrl);
|
| 348 |
}
|
|
@@ -353,6 +353,9 @@
|
|
| 353 |
return serialize(wr, { title: ty.title + (doc.number ? " " + doc.number : "") + (to.name ? " - " + to.name : ""), author: from.name || "", date: stamp }, opts.logo && opts.logo.bytes ? opts.logo : null);
|
| 354 |
}
|
| 355 |
|
|
|
|
|
|
|
|
|
|
| 356 |
/** Suggested file name: Invoice-INV-0042-Harbor-Pine-Bakery.pdf */
|
| 357 |
function fileName(doc) {
|
| 358 |
var ty = (C.TYPES[doc.type] || C.TYPES.invoice).title;
|
|
@@ -360,6 +363,6 @@
|
|
| 360 |
return parts.normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 90) + ".pdf";
|
| 361 |
}
|
| 362 |
|
| 363 |
-
var api = { build: build, fileName: fileName, encode: encode, canEncode: canEncode, width: width, wrap: wrap, STYLES: STYLES, pageSize: pageSize };
|
| 364 |
if (typeof module === "object" && module.exports) module.exports = api; else root.BillPDF = api;
|
| 365 |
})(typeof self !== "undefined" ? self : this);
|
|
|
|
| 177 |
function pct(v) { return String(C.round(v, 3)) + "%"; }
|
| 178 |
|
| 179 |
/**
|
| 180 |
+
* Build the PDF for a document. opts: { credit: bool, creditText, creditUrl, logo: {bytes, w, h}, accent, date: Date }
|
| 181 |
* Returns a Uint8Array.
|
| 182 |
*/
|
| 183 |
function build(doc, opts) {
|
|
|
|
| 342 |
for (var i = 0; i < n; i++) {
|
| 343 |
wr.on(i);
|
| 344 |
if (opts.credit) {
|
| 345 |
+
var credit = opts.creditText || CREDIT;
|
| 346 |
var cw = wr.text(L, H - 28, credit, { size: 7.5, color: "#8A8F9C" });
|
| 347 |
if (opts.creditUrl) wr.link(L, H - 36, cw, 11, opts.creditUrl);
|
| 348 |
}
|
|
|
|
| 353 |
return serialize(wr, { title: ty.title + (doc.number ? " " + doc.number : "") + (to.name ? " - " + to.name : ""), author: from.name || "", date: stamp }, opts.logo && opts.logo.bytes ? opts.logo : null);
|
| 354 |
}
|
| 355 |
|
| 356 |
+
/** Default footer credit on free PDFs (app.js passes the same text plus a tagged creditUrl). */
|
| 357 |
+
var CREDIT = "Made with Billsprig by CyberMax \u2014 make your own free: cybermaxtools.com/apps/free-invoice-generator";
|
| 358 |
+
|
| 359 |
/** Suggested file name: Invoice-INV-0042-Harbor-Pine-Bakery.pdf */
|
| 360 |
function fileName(doc) {
|
| 361 |
var ty = (C.TYPES[doc.type] || C.TYPES.invoice).title;
|
|
|
|
| 363 |
return parts.normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 90) + ".pdf";
|
| 364 |
}
|
| 365 |
|
| 366 |
+
var api = { build: build, fileName: fileName, encode: encode, canEncode: canEncode, width: width, wrap: wrap, STYLES: STYLES, pageSize: pageSize, CREDIT: CREDIT };
|
| 367 |
if (typeof module === "object" && module.exports) module.exports = api; else root.BillPDF = api;
|
| 368 |
})(typeof self !== "undefined" ? self : this);
|
billsprig/style.css
CHANGED
|
@@ -171,6 +171,8 @@ input:focus,select:focus,textarea:focus{border-color:var(--accent);outline:none;
|
|
| 171 |
.d-totals tr.t-total td:last-child{color:var(--acc);font-size:17px}
|
| 172 |
.d-credit{position:absolute;left:61px;bottom:30px;font-size:10px;color:#8A8F9C}
|
| 173 |
html.cmx-pro .d-credit{display:none}
|
|
|
|
|
|
|
| 174 |
/* modern */
|
| 175 |
.tpl-modern{padding-top:0}
|
| 176 |
.tpl-modern .d-band{display:flex;justify-content:space-between;align-items:center;background:var(--acc);color:#fff;margin:0 -61px 32px;padding:26px 61px;min-height:128px}
|
|
@@ -292,3 +294,7 @@ footer.more{border-top:1px solid var(--line);margin-top:30px;padding:22px 0 40px
|
|
| 292 |
#doc .d-credit{position:fixed;bottom:8mm}
|
| 293 |
.d-items tr{break-inside:avoid}
|
| 294 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
.d-totals tr.t-total td:last-child{color:var(--acc);font-size:17px}
|
| 172 |
.d-credit{position:absolute;left:61px;bottom:30px;font-size:10px;color:#8A8F9C}
|
| 173 |
html.cmx-pro .d-credit{display:none}
|
| 174 |
+
.d-credit a{color:inherit;text-decoration:none}
|
| 175 |
+
.d-credit a:hover{text-decoration:underline}
|
| 176 |
/* modern */
|
| 177 |
.tpl-modern{padding-top:0}
|
| 178 |
.tpl-modern .d-band{display:flex;justify-content:space-between;align-items:center;background:var(--acc);color:#fff;margin:0 -61px 32px;padding:26px 61px;min-height:128px}
|
|
|
|
| 294 |
#doc .d-credit{position:fixed;bottom:8mm}
|
| 295 |
.d-items tr{break-inside:avoid}
|
| 296 |
}
|
| 297 |
+
|
| 298 |
+
/* "Use this template" under a guide's worked example */
|
| 299 |
+
.g-use{display:flex;flex-wrap:wrap;align-items:center;gap:6px 10px;margin:4px 0 14px;font-size:13.5px;opacity:.95}
|
| 300 |
+
.g-use span{opacity:.8}
|
billsprig/sw.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
/* Billsprig offline cache. © 2026 CyberMax. All rights reserved. */
|
| 2 |
-
var CACHE = "billsprig-
|
| 3 |
var FILES = ["./", "index.html", "style.css", "pro.js", "lib/calc.js", "lib/pdf.js", "app.js", "manifest.webmanifest", "icon.svg", "icon-192.png", "icon-512.png"];
|
| 4 |
self.addEventListener("install", function (e) {
|
| 5 |
e.waitUntil(caches.open(CACHE).then(function (c) { return c.addAll(FILES); }).then(function () { return self.skipWaiting(); }));
|
|
|
|
| 1 |
/* Billsprig offline cache. © 2026 CyberMax. All rights reserved. */
|
| 2 |
+
var CACHE = "billsprig-v3";
|
| 3 |
var FILES = ["./", "index.html", "style.css", "pro.js", "lib/calc.js", "lib/pdf.js", "app.js", "manifest.webmanifest", "icon.svg", "icon-192.png", "icon-512.png"];
|
| 4 |
self.addEventListener("install", function (e) {
|
| 5 |
e.waitUntil(caches.open(CACHE).then(function (c) { return c.addAll(FILES); }).then(function () { return self.skipWaiting(); }));
|