Compare commits

..

12 Commits

11 changed files with 803 additions and 9 deletions
+1 -1
View File
@@ -1 +1 @@
0.1.1.13
0.1.1.16
+1
View File
@@ -13,6 +13,7 @@
"generate:flyer:png": "node ../scripts/generate-beta-flyer.mjs --png",
"generate:flyer:all": "node ../scripts/generate-beta-flyer.mjs --all",
"generate:flyer:setup": "playwright install chromium",
"generate:sharepic": "node ../scripts/generate-sharepic.mjs",
"translate:locales": "node ../scripts/translate-locales.mjs",
"translate:flyer": "node ../scripts/translate-flyer.mjs",
"validate:i18n": "node ../scripts/validate-i18n-keys.mjs"
+42
View File
@@ -5642,6 +5642,48 @@ html.theme-cupertino .events-scroll-container {
border-color: rgba(255, 94, 91, 0.32);
}
.mail-footer-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 999px;
font-size: 13px;
font-weight: 500;
color: #94a3b8;
text-decoration: none;
background: rgba(56, 189, 248, 0.08);
border: 1px solid rgba(56, 189, 248, 0.18);
transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
}
.mail-footer-badge:hover {
color: #bae6fd;
background: rgba(56, 189, 248, 0.14);
border-color: rgba(56, 189, 248, 0.32);
}
.knorrlabs-footer-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 999px;
font-size: 13px;
font-weight: 500;
color: #94a3b8;
text-decoration: none;
background: rgba(139, 92, 246, 0.08);
border: 1px solid rgba(139, 92, 246, 0.18);
transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
}
.knorrlabs-footer-badge:hover {
color: #ddd6fe;
background: rgba(139, 92, 246, 0.14);
border-color: rgba(139, 92, 246, 0.32);
}
.demo-badge {
display: inline-flex;
align-items: center;
+26 -8
View File
@@ -1,4 +1,4 @@
import { Coffee } from 'lucide-react'
import { Coffee, Mail, Compass } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { PlausibleEvents, trackPlausibleEvent } from '../services/analytics.js'
@@ -15,17 +15,35 @@ export default function AppFooter() {
·
</span>
<span className="app-version-footer__copyright">
© 2026 KnorrLabs/
<a
href="mailto:moin@kapteins-daagbok.eu"
onClick={() => trackPlausibleEvent(PlausibleEvents.FOOTER_LINK_CLICKED)}
>
Markus F.J. Busche
</a>
© 2026
</span>
<span className="app-version-footer__sep" aria-hidden="true">
·
</span>
<a
className="knorrlabs-footer-badge"
href="https://dashy.elpatron.me/"
target="_blank"
rel="noopener noreferrer"
onClick={() => trackPlausibleEvent(PlausibleEvents.FOOTER_LINK_CLICKED)}
>
<Compass size={14} aria-hidden="true" />
<span>KnorrLabs</span>
</a>
<span className="app-version-footer__sep" aria-hidden="true">
·
</span>
<a
className="mail-footer-badge"
href="mailto:moin@kapteins-daagbok.eu"
onClick={() => trackPlausibleEvent(PlausibleEvents.FOOTER_LINK_CLICKED)}
>
<Mail size={14} aria-hidden="true" />
<span>moin@kapteins-daagbok.eu</span>
</a>
<span className="app-version-footer__sep" aria-hidden="true">
·
</span>
<a
className="kofi-footer-badge"
href={KOFI_URL}
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 KiB

+318
View File
@@ -0,0 +1,318 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<title>Kapteins Daagbok — Sharepic (Portrait)</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
width: 1080px;
height: 1920px;
font-family: 'Plus Jakarta Sans', sans-serif;
color: #e2e8f0;
background: #0f172a;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
padding: 100px 80px;
background:
radial-gradient(circle at 50% 10%, rgba(56, 189, 248, 0.18) 0%, transparent 45%),
radial-gradient(circle at 50% 90%, rgba(134, 59, 255, 0.22) 0%, transparent 45%),
linear-gradient(180deg, #090d16 0%, #111827 50%, #090d16 100%);
position: relative;
}
/* Subtle background grid pattern */
body::after {
content: "";
position: absolute;
inset: 0;
background-image: linear-gradient(rgba(148, 163, 184, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(148, 163, 184, 0.03) 1px, transparent 1px);
background-size: 40px 40px;
pointer-events: none;
z-index: 0;
}
/* Outer border */
.outer-border {
position: absolute;
inset: 40px;
border: 1px solid rgba(148, 163, 184, 0.1);
border-radius: 30px;
pointer-events: none;
z-index: 1;
}
.brand {
display: flex;
flex-direction: column;
align-items: center;
gap: 30px;
z-index: 2;
text-align: center;
margin-top: 40px;
}
.logo {
width: 140px;
height: 140px;
object-fit: contain;
filter: drop-shadow(0 8px 24px rgba(56, 189, 248, 0.3));
}
.title-group h1 {
font-size: 64px;
font-weight: 800;
letter-spacing: -0.03em;
color: #ffffff;
line-height: 1.1;
background: linear-gradient(135deg, #ffffff 60%, #94a3b8 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.title-group p {
font-size: 26px;
color: #38bdf8;
font-weight: 600;
margin-top: 8px;
letter-spacing: -0.01em;
}
.main-content {
width: 100%;
display: flex;
flex-direction: column;
gap: 50px;
z-index: 2;
}
.intro-text {
font-size: 26px;
line-height: 1.6;
color: #cbd5e1;
font-weight: 400;
text-align: center;
padding: 0 20px;
}
.intro-text strong {
color: #ffffff;
font-weight: 600;
}
.features-card {
background: rgba(30, 41, 59, 0.45);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 24px;
padding: 50px 60px;
box-shadow: 0 30px 60px rgba(0, 0, 0, 0.4);
position: relative;
}
.features-card::before {
content: "";
position: absolute;
inset: 0;
border-radius: 24px;
padding: 1px;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.02));
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
}
.card-title {
font-size: 20px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #94a3b8;
margin-bottom: 35px;
display: flex;
align-items: center;
gap: 12px;
}
.card-title::after {
content: "";
flex: 1;
height: 1px;
background: rgba(148, 163, 184, 0.15);
}
.badge-premium {
background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
color: #1e293b;
font-size: 16px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.1em;
padding: 6px 14px;
border-radius: 8px;
}
.features-list {
display: flex;
flex-direction: column;
gap: 30px;
list-style: none;
}
.feature-item {
display: flex;
align-items: flex-start;
gap: 20px;
font-size: 24px;
line-height: 1.4;
color: #cbd5e1;
font-weight: 500;
}
.feature-icon {
color: #38bdf8;
font-size: 26px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 3px;
text-shadow: 0 0 10px rgba(56, 189, 248, 0.6);
}
.bottom-section {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 50px;
z-index: 2;
margin-bottom: 40px;
}
.cta-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 30px;
}
.cta-badge {
background: linear-gradient(135deg, #38bdf8 0%, #0284c7 100%);
color: #0f172a;
font-size: 32px;
font-weight: 800;
padding: 20px 45px;
border-radius: 16px;
letter-spacing: -0.02em;
box-shadow: 0 10px 30px rgba(56, 189, 248, 0.25);
}
.qr-code {
width: 160px;
height: 160px;
background: #ffffff;
padding: 10px;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
}
.qr-code img {
width: 100%;
height: 100%;
object-fit: contain;
}
footer {
font-size: 18px;
color: #64748b;
text-align: center;
}
footer strong {
color: #94a3b8;
font-weight: 600;
}
</style>
</head>
<body>
<div class="outer-border"></div>
<div class="brand">
<img class="logo" src="../../client/public/logo.png" alt="Kapteins Daagbok" />
<div class="title-group">
<h1>Kapteins Daagbok</h1>
<p>Digitales Yacht-Logbuch</p>
</div>
</div>
<div class="main-content">
<p class="intro-text">
Führe dein Bordlogbuch modern & digital: Reisetage, GPS-Tracks, Crew- und Schiffsdaten —
<strong>Ende-zu-Ende-verschlüsselt</strong>, als App installierbar und <strong>auch offline</strong> auf See nutzbar.
</p>
<div class="features-card">
<div class="card-title">Top Features <span class="badge-premium">Kostenlos & Werbefrei</span></div>
<ul class="features-list">
<li class="feature-item">
<span class="feature-icon"></span>
<span>Nautisches Logbuch-Format & Streckenstatistik</span>
</li>
<li class="feature-item">
<span class="feature-icon"></span>
<span>Offline-first PWA — läuft auf allen Smartphones & Tablets</span>
</li>
<li class="feature-item">
<span class="feature-icon"></span>
<span>Ende-zu-Ende Verschlüsselung (Zero-Knowledge)</span>
</li>
<li class="feature-item">
<span class="feature-icon"></span>
<span>Einfache passwortlose Passkey-Anmeldung</span>
</li>
<li class="feature-item">
<span class="feature-icon"></span>
<span>GPS-Track-Upload & automatische NMEA-Erfassung</span>
</li>
<li class="feature-item">
<span class="feature-icon"></span>
<span>Crew-Einladung zur gemeinsamen Logbuch-Arbeit</span>
</li>
</ul>
</div>
</div>
<div class="bottom-section">
<div class="cta-container">
<div class="cta-badge">
kapteins-daagbok.eu
</div>
<div class="qr-code">
<img src="assets/qr-kapteins-daagbok.eu.png" alt="QR Code" />
</div>
</div>
<footer>
<strong>Kapteins Daagbok</strong> ist ein werbefreies, privates Hobbyprojekt.
</footer>
</div>
</body>
</html>
+320
View File
@@ -0,0 +1,320 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<title>Kapteins Daagbok — Sharepic</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
width: 1200px;
height: 630px;
font-family: 'Plus Jakarta Sans', sans-serif;
color: #e2e8f0;
background: #0f172a;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 60px 80px;
background:
radial-gradient(circle at 90% 10%, rgba(56, 189, 248, 0.15) 0%, transparent 45%),
radial-gradient(circle at 10% 90%, rgba(134, 59, 255, 0.18) 0%, transparent 45%),
linear-gradient(165deg, #090d16 0%, #111827 50%, #090d16 100%);
position: relative;
}
/* Subtle background grid pattern */
body::after {
content: "";
position: absolute;
inset: 0;
background-image: linear-gradient(rgba(148, 163, 184, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(148, 163, 184, 0.03) 1px, transparent 1px);
background-size: 40px 40px;
pointer-events: none;
z-index: 0;
}
/* Outer border */
.outer-border {
position: absolute;
inset: 30px;
border: 1px solid rgba(148, 163, 184, 0.1);
border-radius: 20px;
pointer-events: none;
z-index: 1;
}
.content-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
height: 100%;
width: 100%;
z-index: 2;
position: relative;
gap: 50px;
}
.left-col {
flex: 1.1;
display: flex;
flex-direction: column;
justify-content: center;
gap: 30px;
}
.brand {
display: flex;
align-items: center;
gap: 20px;
}
.logo {
width: 80px;
height: 80px;
object-fit: contain;
filter: drop-shadow(0 4px 12px rgba(56, 189, 248, 0.3));
}
.title-group h1 {
font-size: 44px;
font-weight: 800;
letter-spacing: -0.03em;
color: #ffffff;
line-height: 1.1;
background: linear-gradient(135deg, #ffffff 60%, #94a3b8 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.title-group p {
font-size: 18px;
color: #38bdf8;
font-weight: 600;
margin-top: 4px;
letter-spacing: -0.01em;
}
.intro-text {
font-size: 20px;
line-height: 1.6;
color: #cbd5e1;
font-weight: 400;
}
.intro-text strong {
color: #ffffff;
font-weight: 600;
}
.cta-container {
display: flex;
align-items: center;
gap: 20px;
}
.cta-badge {
background: linear-gradient(135deg, #38bdf8 0%, #0284c7 100%);
color: #0f172a;
font-size: 22px;
font-weight: 800;
padding: 14px 28px;
border-radius: 12px;
letter-spacing: -0.02em;
box-shadow: 0 4px 20px rgba(56, 189, 248, 0.25);
}
.qr-code {
width: 60px;
height: 60px;
background: #ffffff;
padding: 4px;
border-radius: 8px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
display: flex;
align-items: center;
justify-content: center;
}
.qr-code img {
width: 100%;
height: 100%;
object-fit: contain;
}
.right-col {
flex: 0.9;
display: flex;
flex-direction: column;
justify-content: center;
}
.features-card {
background: rgba(30, 41, 59, 0.45);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 20px;
padding: 35px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
position: relative;
}
.features-card::before {
content: "";
position: absolute;
inset: 0;
border-radius: 20px;
padding: 1px;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.02));
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
}
.features-list {
display: flex;
flex-direction: column;
gap: 18px;
list-style: none;
}
.feature-item {
display: flex;
align-items: flex-start;
gap: 14px;
font-size: 16px;
line-height: 1.4;
color: #cbd5e1;
font-weight: 500;
}
.feature-icon {
color: #38bdf8;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 2px;
text-shadow: 0 0 8px rgba(56, 189, 248, 0.6);
}
.badge-premium {
background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
color: #1e293b;
font-size: 12px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.1em;
padding: 4px 10px;
border-radius: 6px;
margin-left: auto;
}
.card-title {
font-size: 14px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #94a3b8;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 8px;
}
.card-title::after {
content: "";
flex: 1;
height: 1px;
background: rgba(148, 163, 184, 0.15);
}
footer {
position: absolute;
bottom: 45px;
left: 80px;
font-size: 12px;
color: #64748b;
z-index: 2;
}
footer strong {
color: #94a3b8;
font-weight: 600;
}
</style>
</head>
<body>
<div class="outer-border"></div>
<div class="content-wrapper">
<div class="left-col">
<div class="brand">
<img class="logo" src="../../client/public/logo.png" alt="Kapteins Daagbok" />
<div class="title-group">
<h1>Kapteins Daagbok</h1>
<p>Digitales Yacht-Logbuch</p>
</div>
</div>
<p class="intro-text">
Führe dein Bordlogbuch modern & digital: Reisetage, GPS-Tracks, Crew- und Schiffsdaten —
<strong>Ende-zu-Ende-verschlüsselt</strong>, als App installierbar und <strong>auch offline</strong> auf See nutzbar.
</p>
<div class="cta-container">
<div class="cta-badge">
kapteins-daagbok.eu
</div>
<div class="qr-code">
<img src="assets/qr-kapteins-daagbok.eu.png" alt="QR Code" />
</div>
</div>
</div>
<div class="right-col">
<div class="features-card">
<div class="card-title">Top Features <span class="badge-premium">Kostenlos & Werbefrei</span></div>
<ul class="features-list">
<li class="feature-item">
<span class="feature-icon"></span>
<span>Nautisches Logbuch-Format & Streckenstatistik</span>
</li>
<li class="feature-item">
<span class="feature-icon"></span>
<span>Offline-first PWA — läuft auf allen Smartphones & Tablets</span>
</li>
<li class="feature-item">
<span class="feature-icon"></span>
<span>Ende-zu-Ende Verschlüsselung (Zero-Knowledge)</span>
</li>
<li class="feature-item">
<span class="feature-icon"></span>
<span>Einfache passwortlose Passkey-Anmeldung</span>
</li>
<li class="feature-item">
<span class="feature-icon"></span>
<span>GPS-Track-Upload & automatische NMEA-Erfassung</span>
</li>
<li class="feature-item">
<span class="feature-icon"></span>
<span>Crew-Einladung zur gemeinsamen Logbuch-Arbeit</span>
</li>
</ul>
</div>
</div>
</div>
<footer>
<strong>Kapteins Daagbok</strong> ist ein werbefreies, privates Hobbyprojekt.
</footer>
</body>
</html>
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env node
/**
* Generates the sharepic PNGs (landscape & portrait) from HTML files
*
* Usage:
* node scripts/generate-sharepic.mjs
*/
import { execSync } from 'node:child_process'
import { dirname, resolve } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { createRequire } from 'node:module'
const __dirname = dirname(fileURLToPath(import.meta.url))
const repoRoot = resolve(__dirname, '..')
const clientDir = resolve(repoRoot, 'client')
const marketingDir = resolve(repoRoot, 'docs/marketing')
const require = createRequire(resolve(clientDir, 'package.json'))
function isMissingBrowserError(err) {
const msg = err instanceof Error ? err.message : String(err)
return msg.includes("Executable doesn't exist") || msg.includes('browserType.launch')
}
async function ensurePlaywrightChromium(playwright) {
try {
const browser = await playwright.chromium.launch({ headless: true })
await browser.close()
return
} catch (err) {
if (!isMissingBrowserError(err)) throw err
}
console.log('Playwright Chromium fehlt — installiere Browser (einmalig)…')
execSync('npx playwright install chromium', {
cwd: clientDir,
stdio: 'inherit'
})
}
function loadPlaywright() {
try {
return require('playwright')
} catch {
console.error('Fehlende Abhängigkeit: "npm install -D playwright" in client/ ausführen.')
process.exit(1)
}
}
async function renderSharepic(browser, htmlName, pngName, width, height) {
const htmlPath = resolve(marketingDir, htmlName)
const pngPath = resolve(marketingDir, pngName)
console.log(`Generating sharepic (${width}x${height}) from ${htmlName}...`)
const context = await browser.newContext({
viewport: { width, height },
deviceScaleFactor: 2 // High-DPI for crisp text
})
const page = await context.newPage()
try {
await page.goto(pathToFileURL(htmlPath).href, { waitUntil: 'networkidle' })
await page.screenshot({
path: pngPath,
type: 'png'
})
console.log('Successfully wrote:', pngPath)
} finally {
await page.close()
}
}
async function main() {
const playwright = loadPlaywright()
await ensurePlaywrightChromium(playwright)
const browser = await playwright.chromium.launch({ headless: true })
try {
// Landscape 1200x630
await renderSharepic(browser, 'sharepic.html', 'kapteins-daagbok-sharepic.png', 1200, 630)
// Portrait 1080x1920
await renderSharepic(browser, 'sharepic-portrait.html', 'kapteins-daagbok-sharepic-portrait.png', 1080, 1920)
} finally {
await browser.close()
}
}
main().catch((err) => {
console.error('Error generating sharepics:', err)
process.exit(1)
})