feat: Scorecard-Default = portrait, Instagram via Web-Share-API, Padding gestrafft

User-Feedback in drei Punkten:

1. 'Standard auch fuer die Scorecard sein' — /v2/scorecard und
   /api/assessment/scorecard.{png,pdf} default jetzt format=portrait
   statt og. Wer das alte OG-Layout will, muss explizit ?format=og
   setzen (oder square). Externe OG-Tags sind nicht betroffen, die
   nutzen ein anderes Template (v2/og_template.html).

2. 'Instagram-Button sollte den Teilen-Dialog aufrufen' — implementiert
   mit navigator.share() + File-Blob. Auf Mobile (Safari iOS / Chrome
   Android) oeffnet der native Share-Sheet und Instagram erscheint
   direkt als Ziel; Bild + Text gehen mit. Auf Desktop / Browsern
   ohne canShare({files:…}) falle auf den vorigen Fallback zurueck:
   Bild in neuem Tab + Text in Zwischenablage.

3. 'Card nutzt Platz besser, viel Rand' — alle Paddings reduziert:
   - Card-Padding portrait: 54/56/32 → 34/38/24
   - Body gap: 22 → 16, margin-top: 26 → 16
   - Title: 32pt → 36pt
   - Score-Number: 110pt → 130pt
   - Matrix: 380×380 → 460×460 (groesser, mehr Detail erkennbar)
   - Footer: enger an den Rand
   Inhalt nimmt jetzt mehr Platz ein, weniger Whitespace-Verschwendung.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dotty Dotter 2026-05-07 13:06:46 +02:00
parent 8c8dfbe625
commit 0e5b2180ab
3 changed files with 68 additions and 45 deletions

View File

@ -3476,13 +3476,13 @@ async def og_template(request: Request, drucksache: str = ""):
@app.get("/v2/scorecard")
async def scorecard_template(
request: Request, drucksache: str, bundesland: str = "NRW",
format: str = "og",
format: str = "portrait",
):
"""Internes Render-Template für Scorecards (#179).
`format=og` 1200×630 (LinkedIn/Twitter-OG)
`format=portrait` 1080×1350 (Instagram 4:5 Hochformat DEFAULT)
`format=square` 1080×1080 (Instagram quadratisch)
`format=portrait` 1080×1350 (Instagram 4:5 Hochformat Default fuer Feed)
`format=og` 1200×630 (LinkedIn/Twitter-OG)
"""
from .config import settings as _settings
drucksache = validate_drucksache(drucksache)
@ -3587,7 +3587,7 @@ async def _render_scorecard_pdf(
@app.get("/api/assessment/scorecard.png")
async def api_scorecard_png(
drucksache: str, bundesland: str = "NRW", format: str = "og", scale: float = 2.0,
drucksache: str, bundesland: str = "NRW", format: str = "portrait", scale: float = 2.0,
):
"""Liefert die Scorecard als PNG via WeasyPrint→PyMuPDF.
@ -3621,7 +3621,7 @@ async def api_scorecard_png(
@app.get("/api/assessment/scorecard.pdf")
async def api_scorecard_pdf(
drucksache: str, bundesland: str = "NRW", format: str = "og",
drucksache: str, bundesland: str = "NRW", format: str = "portrait",
):
"""Liefert die Scorecard als PDF via WeasyPrint (#179).

View File

@ -942,22 +942,41 @@ window.v2ShowMatrixFieldInfo = function(field) {
window.open(url, '_blank', 'noopener');
};
/* Instagram-Sharing: oeffnet das Hochkant-PNG (1080×1350, 4:5 — das von
Instagram empfohlene Feed-Format) in neuem Tab, legt den Begleittext
in die Zwischenablage. Instagram hat keinen Web-Share-Endpoint —
User muss das Bild lokal speichern und in der Instagram-App posten,
der Text liegt dann zum Einfuegen bereit. */
window.v2DetailShareInstagram = function() {
/* Instagram-Sharing: bevorzugt Web-Share-API mit Datei-Blob — auf
Mobile-Browsern (Safari iOS, Chrome Android) oeffnet das den nativen
Share-Sheet, in dem Instagram als Ziel auftaucht und den User direkt
in die Instagram-App pusht. Auf Desktop (kein File-Sharing) oder
unsupported Browsern: PNG im neuen Tab + Text in Zwischenablage. */
window.v2DetailShareInstagram = async function() {
var url = '/api/assessment/scorecard.png?drucksache=' + encodeURIComponent(DRS)
+ '&bundesland=' + encodeURIComponent(BL || 'NRW')
+ '&format=portrait&scale=2';
var win = window.open(url, '_blank', 'noopener');
var body = buildLongShareText();
// Native Web-Share-API mit Datei probieren (nur Mobile-Browser
// koennen files: mit canShare positiv beantworten).
try {
var resp = await fetch(url);
if (resp.ok) {
var blob = await resp.blob();
var safeDrs = (DRS || 'antrag').replace(/[^a-zA-Z0-9_-]/g, '-');
var file = new File([blob], 'gwoe-' + safeDrs + '.png', { type: 'image/png' });
var data = { title: TITLE, text: body, files: [file] };
if (navigator.canShare && navigator.canShare(data) && navigator.share) {
await navigator.share(data);
v2ShareToast('Share-Dialog geöffnet — Instagram als Ziel auswählen.');
return;
}
}
} catch (_) {
// Cancel oder Fehler — fallthrough zum Fallback
}
// Fallback Desktop / unsupported: PNG in neuem Tab + Text kopieren
var win = window.open(url, '_blank', 'noopener');
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(body).then(function() {
v2ShareToast('Hochkant-Bild öffnet — Bild speichern, in Instagram posten. Text liegt in der Zwischenablage.');
}, function() {
v2ShareToast('Hochkant-Bild öffnet. Text-Kopieren manuell.');
v2ShareToast('Bild öffnet — speichern und in Instagram posten. Text liegt in der Zwischenablage.');
});
}
if (!win) v2ShareToast('Bitte Pop-up-Blocker prüfen.');

View File

@ -30,7 +30,7 @@
.card {
width: 100%;
height: 100%;
padding: {% if is_og %}38px 48px{% elif is_portrait %}54px 56px 32px{% else %}32px 36px{% endif %};
padding: {% if is_og %}28px 38px{% elif is_portrait %}34px 38px 24px{% else %}26px 30px{% endif %};
display: flex;
flex-direction: column;
justify-content: space-between;
@ -150,23 +150,23 @@
aspect-ratio: 1 / 1;
}
/* ── portrait Layout ── */
/* ── portrait Layout — kompakt, weniger Rand ── */
.portrait-body {
flex: 1;
display: flex;
flex-direction: column;
gap: 22px;
margin-top: 26px;
gap: 16px;
margin-top: 16px;
}
.portrait-title {
font-size: 32pt;
line-height: 1.15;
font-size: 36pt;
line-height: 1.1;
color: #1f1f1f;
font-weight: 700;
}
.portrait-fraktionen {
font-family: 'Source Code Pro', monospace;
font-size: 13pt;
font-size: 14pt;
color: #555;
}
.portrait-fraktionen .pill {
@ -181,19 +181,19 @@
display: flex;
align-items: center;
gap: 28px;
padding: 16px 0;
padding: 12px 0;
border-top: 1px solid rgba(0,0,0,0.08);
border-bottom: 1px solid rgba(0,0,0,0.08);
}
.portrait-score-num {
font-family: 'Source Code Pro', monospace;
font-size: 110pt;
font-size: 130pt;
font-weight: 700;
line-height: 0.95;
line-height: 0.92;
color: {{ score_color }};
flex-shrink: 0;
}
.portrait-score-num .slash { font-size: 60pt; opacity: 0.45; }
.portrait-score-num .slash { font-size: 64pt; opacity: 0.45; }
.portrait-score-side {
display: flex;
flex-direction: column;
@ -201,44 +201,48 @@
}
.portrait-score-label {
font-family: 'Source Code Pro', monospace;
font-size: 12pt;
font-size: 13pt;
text-transform: uppercase;
letter-spacing: 0.12em;
color: #555;
}
.portrait-verdict {
font-size: 24pt;
font-size: 28pt;
color: {{ score_color }};
font-weight: 700;
line-height: 1.15;
line-height: 1.12;
}
.portrait-matrix-block {
display: flex;
align-items: center;
gap: 24px;
align-items: stretch;
gap: 22px;
}
.portrait-matrix {
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-template-rows: repeat(5, 1fr);
gap: 4px;
width: 380px;
height: 380px;
width: 460px;
height: 460px;
flex-shrink: 0;
}
.portrait-matrix-legend {
flex: 1;
font-family: 'Source Code Pro', monospace;
font-size: 11pt;
font-size: 12pt;
color: #555;
line-height: 1.5;
display: flex;
flex-direction: column;
justify-content: center;
}
.portrait-matrix-legend .l-title {
font-weight: 700;
text-transform: uppercase;
color: #009DA5;
margin-bottom: 8px;
margin-bottom: 10px;
letter-spacing: 0.06em;
font-size: 13pt;
}
.portrait-matrix-legend ul {
list-style: none;
@ -248,18 +252,18 @@
.portrait-matrix-legend li {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
gap: 8px;
margin-bottom: 6px;
}
.portrait-matrix-legend .swatch {
width: 14px; height: 14px; border-radius: 2px; flex-shrink: 0;
width: 18px; height: 18px; border-radius: 2px; flex-shrink: 0;
}
.portrait-summary {
font-size: 14pt;
line-height: 1.55;
font-size: 15pt;
line-height: 1.5;
color: #333;
display: -webkit-box;
-webkit-line-clamp: 5;
-webkit-line-clamp: 6;
-webkit-box-orient: vertical;
overflow: hidden;
}
@ -267,9 +271,9 @@
/* ── Footer ── */
.footer {
position: absolute;
bottom: {% if is_og %}16px{% elif is_portrait %}24px{% else %}12px{% endif %};
left: {% if is_og %}48px{% elif is_portrait %}56px{% else %}36px{% endif %};
right: {% if is_og %}48px{% elif is_portrait %}56px{% else %}36px{% endif %};
bottom: {% if is_og %}12px{% elif is_portrait %}16px{% else %}10px{% endif %};
left: {% if is_og %}38px{% elif is_portrait %}38px{% else %}30px{% endif %};
right: {% if is_og %}38px{% elif is_portrait %}38px{% else %}30px{% endif %};
display: flex;
justify-content: space-between;
align-items: center;
@ -277,7 +281,7 @@
font-size: 9pt;
color: #888;
border-top: 1px solid rgba(0,0,0,0.1);
padding-top: 8px;
padding-top: 6px;
}
.footer .brand { color: #009DA5; font-weight: 700; letter-spacing: 0.06em; }
</style>