feat: add sharepic HTML template, generation script, and client package task
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
"generate:flyer:png": "node ../scripts/generate-beta-flyer.mjs --png",
|
"generate:flyer:png": "node ../scripts/generate-beta-flyer.mjs --png",
|
||||||
"generate:flyer:all": "node ../scripts/generate-beta-flyer.mjs --all",
|
"generate:flyer:all": "node ../scripts/generate-beta-flyer.mjs --all",
|
||||||
"generate:flyer:setup": "playwright install chromium",
|
"generate:flyer:setup": "playwright install chromium",
|
||||||
|
"generate:sharepic": "node ../scripts/generate-sharepic.mjs",
|
||||||
"translate:locales": "node ../scripts/translate-locales.mjs",
|
"translate:locales": "node ../scripts/translate-locales.mjs",
|
||||||
"translate:flyer": "node ../scripts/translate-flyer.mjs",
|
"translate:flyer": "node ../scripts/translate-flyer.mjs",
|
||||||
"validate:i18n": "node ../scripts/validate-i18n-keys.mjs"
|
"validate:i18n": "node ../scripts/validate-i18n-keys.mjs"
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
@@ -0,0 +1,292 @@
|
|||||||
|
<!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-badge {
|
||||||
|
align-self: flex-start;
|
||||||
|
background: linear-gradient(135deg, #38bdf8 0%, #0284c7 100%);
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 800;
|
||||||
|
padding: 12px 28px;
|
||||||
|
border-radius: 12px;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
box-shadow: 0 4px 20px rgba(56, 189, 248, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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-badge">
|
||||||
|
kapteins-daagbok.eu
|
||||||
|
</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>
|
||||||
Executable
+84
@@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Generates the sharepic PNG from docs/marketing/sharepic.html
|
||||||
|
*
|
||||||
|
* 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 htmlPath = resolve(marketingDir, 'sharepic.html')
|
||||||
|
const pngPath = resolve(marketingDir, 'kapteins-daagbok-sharepic.png')
|
||||||
|
|
||||||
|
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 main() {
|
||||||
|
const playwright = loadPlaywright()
|
||||||
|
await ensurePlaywrightChromium(playwright)
|
||||||
|
|
||||||
|
console.log('Generating sharepic from HTML...')
|
||||||
|
const browser = await playwright.chromium.launch({ headless: true })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const context = await browser.newContext({
|
||||||
|
viewport: { width: 1200, height: 630 },
|
||||||
|
deviceScaleFactor: 2 // High-DPI for crisp text
|
||||||
|
})
|
||||||
|
const page = await context.newPage()
|
||||||
|
|
||||||
|
// Wait for fonts/images to load fully
|
||||||
|
await page.goto(pathToFileURL(htmlPath).href, { waitUntil: 'networkidle' })
|
||||||
|
|
||||||
|
// Take a screenshot of the viewport
|
||||||
|
await page.screenshot({
|
||||||
|
path: pngPath,
|
||||||
|
type: 'png'
|
||||||
|
})
|
||||||
|
console.log('Sharepic PNG written successfully to:', pngPath)
|
||||||
|
} finally {
|
||||||
|
await browser.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error('Error generating sharepic:', err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user