mirror of
https://github.com/kbenestad/invoice.git
synced 2026-06-18 08:04:32 +00:00
Add footer with copyright, links, and optional About modal
Footer shows: © 2026 Kristian Benestad • docs.benestad.net • kbenestad/invoice and an About link when cfg.about is defined. Clicking About opens a modal with a title, markdown-rendered body (via marked.js from CDN), and a close button — all defined per language in config.yml under the `about` key. Remove the section to hide the link. Language switch updates the footer link title and the modal content. Esc key closes the modal. https://claude.ai/code/session_0151QtsUhzXmgzEhSvXG2SDt
This commit is contained in:
parent
131f6c5d25
commit
2795a593ef
2 changed files with 148 additions and 0 deletions
|
|
@ -21,6 +21,59 @@ languages:
|
|||
name: Norsk
|
||||
direction: ltr
|
||||
|
||||
# ── About modal ───────────────────────────────────────────────────────────────
|
||||
# Optional. Remove this section entirely to hide the About link in the footer.
|
||||
# Each language entry requires: title, content (Markdown), btn-close.
|
||||
about:
|
||||
en:
|
||||
title: "About"
|
||||
content: |
|
||||
### Invoice Generator
|
||||
A single-file browser app for generating freelance invoices as PDF or print preview.
|
||||
No backend, no build step — just a browser and a config file.
|
||||
|
||||
**Source:** [kbenestad/invoice](https://github.com/kbenestad/invoice)
|
||||
**Docs:** [docs.benestad.net/invoice](https://docs.benestad.net/invoice)
|
||||
|
||||
© 2026 Kristian Benestad. Licensed under the Apache 2 license.
|
||||
btn-close: "Close"
|
||||
de:
|
||||
title: "Über"
|
||||
content: |
|
||||
### Rechnungsgenerator
|
||||
Eine Single-File-Browser-App zur Erstellung von Freiberufler-Rechnungen als PDF oder Druckvorschau.
|
||||
Kein Backend, kein Build-Schritt.
|
||||
|
||||
**Quellcode:** [kbenestad/invoice](https://github.com/kbenestad/invoice)
|
||||
**Dokumentation:** [docs.benestad.net/invoice](https://docs.benestad.net/invoice)
|
||||
|
||||
© 2026 Kristian Benestad. Lizenziert unter Apache 2.
|
||||
btn-close: "Schließen"
|
||||
fr:
|
||||
title: "À propos"
|
||||
content: |
|
||||
### Générateur de factures
|
||||
Une application de navigateur en fichier unique pour générer des factures en PDF ou aperçu avant impression.
|
||||
Aucun backend, aucune étape de build.
|
||||
|
||||
**Code source :** [kbenestad/invoice](https://github.com/kbenestad/invoice)
|
||||
**Documentation :** [docs.benestad.net/invoice](https://docs.benestad.net/invoice)
|
||||
|
||||
© 2026 Kristian Benestad. Sous licence Apache 2.
|
||||
btn-close: "Fermer"
|
||||
"no":
|
||||
title: "Om"
|
||||
content: |
|
||||
### Fakturagenerator
|
||||
En nettleserapp i én fil for å generere frilansfakturaer som PDF eller utskriftsforhåndsvisning.
|
||||
Ingen backend, ingen byggtrinn.
|
||||
|
||||
**Kildekode:** [kbenestad/invoice](https://github.com/kbenestad/invoice)
|
||||
**Dokumentasjon:** [docs.benestad.net/invoice](https://docs.benestad.net/invoice)
|
||||
|
||||
© 2026 Kristian Benestad. Lisensiert under Apache 2.
|
||||
btn-close: "Lukk"
|
||||
|
||||
# ── Payment info visibility ───────────────────────────────────────────────────
|
||||
# Set to true to hide the payment info panel entirely (useful if payment info
|
||||
# should not appear on invoices, e.g. per company policy).
|
||||
|
|
|
|||
|
|
@ -220,6 +220,41 @@
|
|||
/* ── Error / loading ────────────────────────────────────────────────────── */
|
||||
#loading { padding: 48px; text-align: center; color: var(--text-muted); font-size: 14px; }
|
||||
.error-box { background: #fef2f2; border: 1px solid #fca5a5; color: #991b1b; padding: 16px 20px; border-radius: var(--radius); margin: 20px 0; font-size: 13px; }
|
||||
|
||||
/* ── Footer ─────────────────────────────────────────────────────────────── */
|
||||
#app-footer { text-align: center; font-size: 11px; color: var(--text-muted); padding: 18px 0 24px; }
|
||||
#app-footer a { color: var(--text-muted); text-decoration: none; }
|
||||
#app-footer a:hover { color: var(--accent); text-decoration: underline; }
|
||||
|
||||
/* ── About modal ────────────────────────────────────────────────────────── */
|
||||
#about-modal { display: none; position: fixed; inset: 0; z-index: 1000; }
|
||||
#about-overlay { position: absolute; inset: 0; background: rgba(0,0,0,.45); }
|
||||
#about-dialog {
|
||||
position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);
|
||||
background: var(--white); border-radius: 6px; box-shadow: 0 20px 60px rgba(0,0,0,.25);
|
||||
width: min(560px, 92vw); max-height: 80vh; display: flex; flex-direction: column;
|
||||
}
|
||||
#about-dialog-header {
|
||||
padding: 18px 20px 14px; border-bottom: 1px solid var(--border);
|
||||
font-size: 15px; font-weight: 700; color: var(--navy);
|
||||
}
|
||||
#about-dialog-body {
|
||||
padding: 16px 20px; overflow-y: auto; font-size: 13px; line-height: 1.6; color: var(--text);
|
||||
}
|
||||
#about-dialog-body h1,#about-dialog-body h2,#about-dialog-body h3 {
|
||||
font-size: 13px; font-weight: 700; margin: 12px 0 4px; color: var(--navy);
|
||||
}
|
||||
#about-dialog-body p { margin-bottom: 8px; }
|
||||
#about-dialog-body a { color: var(--accent); }
|
||||
#about-dialog-body ul,#about-dialog-body ol { padding-left: 20px; margin-bottom: 8px; }
|
||||
#about-dialog-footer {
|
||||
padding: 12px 20px; border-top: 1px solid var(--border); text-align: right;
|
||||
}
|
||||
#about-btn-close {
|
||||
background: var(--navy); color: var(--white); border: none; border-radius: var(--radius);
|
||||
font-size: 13px; padding: 6px 18px; cursor: pointer;
|
||||
}
|
||||
#about-btn-close:hover { background: var(--accent); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -246,8 +281,23 @@
|
|||
<!-- Main form (injected by JS) -->
|
||||
<div id="form-root"></div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer id="app-footer"></footer>
|
||||
|
||||
</div><!-- /.wrap -->
|
||||
|
||||
<!-- About modal (outside .wrap so it overlays everything) -->
|
||||
<div id="about-modal" role="dialog" aria-modal="true">
|
||||
<div id="about-overlay" onclick="closeAbout()"></div>
|
||||
<div id="about-dialog">
|
||||
<div id="about-dialog-header"></div>
|
||||
<div id="about-dialog-body"></div>
|
||||
<div id="about-dialog-footer">
|
||||
<button id="about-btn-close" onclick="closeAbout()">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- js-yaml -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"
|
||||
|
|
@ -255,6 +305,9 @@
|
|||
<!-- jsPDF -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"
|
||||
crossorigin="anonymous"></script>
|
||||
<!-- marked -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.6/marked.min.js"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
"use strict";
|
||||
|
|
@ -416,6 +469,7 @@ async function loadCfg() {
|
|||
function boot() {
|
||||
buildLangBar();
|
||||
buildForm();
|
||||
buildFooter();
|
||||
restoreStorage();
|
||||
if (!loadLines()) addLine();
|
||||
document.getElementById("loading").style.display = "none";
|
||||
|
|
@ -1088,6 +1142,7 @@ function relabel() {
|
|||
ctPick.value = cur;
|
||||
}
|
||||
|
||||
buildFooter();
|
||||
}
|
||||
|
||||
// ── Generate invoice ──────────────────────────────────────────────────────────
|
||||
|
|
@ -1541,6 +1596,46 @@ function resetLines() {
|
|||
calcTotals();
|
||||
}
|
||||
|
||||
// ── Footer & About modal ──────────────────────────────────────────────────────
|
||||
function aboutData() {
|
||||
const ab = cfg?.about;
|
||||
if (!ab) return null;
|
||||
return ab[lang] ?? ab[cfg["default-code"]] ?? Object.values(ab)[0] ?? null;
|
||||
}
|
||||
|
||||
function buildFooter() {
|
||||
const footer = document.getElementById("app-footer");
|
||||
if (!footer) return;
|
||||
const ab = aboutData();
|
||||
const aboutLink = ab
|
||||
? ` • <a href="#" onclick="openAbout();return false">${h(ab.title ?? "About")}</a>`
|
||||
: "";
|
||||
footer.innerHTML =
|
||||
`© 2026 Kristian Benestad`
|
||||
+ ` • <a href="https://docs.benestad.net/invoice" target="_blank" rel="noopener">docs.benestad.net</a>`
|
||||
+ ` • <a href="https://github.com/kbenestad/invoice" target="_blank" rel="noopener">kbenestad/invoice</a>`
|
||||
+ aboutLink;
|
||||
}
|
||||
|
||||
function openAbout() {
|
||||
const ab = aboutData();
|
||||
if (!ab) return;
|
||||
document.getElementById("about-dialog-header").textContent = ab.title ?? "";
|
||||
document.getElementById("about-dialog-body").innerHTML =
|
||||
typeof marked !== "undefined" ? marked.parse(ab.content ?? "") : h(ab.content ?? "");
|
||||
document.getElementById("about-btn-close").textContent = ab["btn-close"] ?? "Close";
|
||||
document.getElementById("about-modal").style.display = "block";
|
||||
document.getElementById("about-btn-close").focus();
|
||||
}
|
||||
|
||||
function closeAbout() {
|
||||
document.getElementById("about-modal").style.display = "none";
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", e => {
|
||||
if (e.key === "Escape") closeAbout();
|
||||
});
|
||||
|
||||
// ── Start ─────────────────────────────────────────────────────────────────────
|
||||
loadCfg();
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Reference in a new issue