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:
parent
8c8dfbe625
commit
0e5b2180ab
10
app/main.py
10
app/main.py
@ -3476,13 +3476,13 @@ async def og_template(request: Request, drucksache: str = ""):
|
|||||||
@app.get("/v2/scorecard")
|
@app.get("/v2/scorecard")
|
||||||
async def scorecard_template(
|
async def scorecard_template(
|
||||||
request: Request, drucksache: str, bundesland: str = "NRW",
|
request: Request, drucksache: str, bundesland: str = "NRW",
|
||||||
format: str = "og",
|
format: str = "portrait",
|
||||||
):
|
):
|
||||||
"""Internes Render-Template für Scorecards (#179).
|
"""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=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
|
from .config import settings as _settings
|
||||||
drucksache = validate_drucksache(drucksache)
|
drucksache = validate_drucksache(drucksache)
|
||||||
@ -3587,7 +3587,7 @@ async def _render_scorecard_pdf(
|
|||||||
|
|
||||||
@app.get("/api/assessment/scorecard.png")
|
@app.get("/api/assessment/scorecard.png")
|
||||||
async def api_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.
|
"""Liefert die Scorecard als PNG via WeasyPrint→PyMuPDF.
|
||||||
|
|
||||||
@ -3621,7 +3621,7 @@ async def api_scorecard_png(
|
|||||||
|
|
||||||
@app.get("/api/assessment/scorecard.pdf")
|
@app.get("/api/assessment/scorecard.pdf")
|
||||||
async def api_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).
|
"""Liefert die Scorecard als PDF via WeasyPrint (#179).
|
||||||
|
|
||||||
|
|||||||
@ -942,22 +942,41 @@ window.v2ShowMatrixFieldInfo = function(field) {
|
|||||||
window.open(url, '_blank', 'noopener');
|
window.open(url, '_blank', 'noopener');
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Instagram-Sharing: oeffnet das Hochkant-PNG (1080×1350, 4:5 — das von
|
/* Instagram-Sharing: bevorzugt Web-Share-API mit Datei-Blob — auf
|
||||||
Instagram empfohlene Feed-Format) in neuem Tab, legt den Begleittext
|
Mobile-Browsern (Safari iOS, Chrome Android) oeffnet das den nativen
|
||||||
in die Zwischenablage. Instagram hat keinen Web-Share-Endpoint —
|
Share-Sheet, in dem Instagram als Ziel auftaucht und den User direkt
|
||||||
User muss das Bild lokal speichern und in der Instagram-App posten,
|
in die Instagram-App pusht. Auf Desktop (kein File-Sharing) oder
|
||||||
der Text liegt dann zum Einfuegen bereit. */
|
unsupported Browsern: PNG im neuen Tab + Text in Zwischenablage. */
|
||||||
window.v2DetailShareInstagram = function() {
|
window.v2DetailShareInstagram = async function() {
|
||||||
var url = '/api/assessment/scorecard.png?drucksache=' + encodeURIComponent(DRS)
|
var url = '/api/assessment/scorecard.png?drucksache=' + encodeURIComponent(DRS)
|
||||||
+ '&bundesland=' + encodeURIComponent(BL || 'NRW')
|
+ '&bundesland=' + encodeURIComponent(BL || 'NRW')
|
||||||
+ '&format=portrait&scale=2';
|
+ '&format=portrait&scale=2';
|
||||||
var win = window.open(url, '_blank', 'noopener');
|
|
||||||
var body = buildLongShareText();
|
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) {
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
navigator.clipboard.writeText(body).then(function() {
|
navigator.clipboard.writeText(body).then(function() {
|
||||||
v2ShareToast('Hochkant-Bild öffnet — Bild speichern, in Instagram posten. Text liegt in der Zwischenablage.');
|
v2ShareToast('Bild öffnet — speichern und in Instagram posten. Text liegt in der Zwischenablage.');
|
||||||
}, function() {
|
|
||||||
v2ShareToast('Hochkant-Bild öffnet. Text-Kopieren manuell.');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!win) v2ShareToast('Bitte Pop-up-Blocker prüfen.');
|
if (!win) v2ShareToast('Bitte Pop-up-Blocker prüfen.');
|
||||||
|
|||||||
@ -30,7 +30,7 @@
|
|||||||
.card {
|
.card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -150,23 +150,23 @@
|
|||||||
aspect-ratio: 1 / 1;
|
aspect-ratio: 1 / 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── portrait Layout ── */
|
/* ── portrait Layout — kompakt, weniger Rand ── */
|
||||||
.portrait-body {
|
.portrait-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 22px;
|
gap: 16px;
|
||||||
margin-top: 26px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
.portrait-title {
|
.portrait-title {
|
||||||
font-size: 32pt;
|
font-size: 36pt;
|
||||||
line-height: 1.15;
|
line-height: 1.1;
|
||||||
color: #1f1f1f;
|
color: #1f1f1f;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
.portrait-fraktionen {
|
.portrait-fraktionen {
|
||||||
font-family: 'Source Code Pro', monospace;
|
font-family: 'Source Code Pro', monospace;
|
||||||
font-size: 13pt;
|
font-size: 14pt;
|
||||||
color: #555;
|
color: #555;
|
||||||
}
|
}
|
||||||
.portrait-fraktionen .pill {
|
.portrait-fraktionen .pill {
|
||||||
@ -181,19 +181,19 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 28px;
|
gap: 28px;
|
||||||
padding: 16px 0;
|
padding: 12px 0;
|
||||||
border-top: 1px solid rgba(0,0,0,0.08);
|
border-top: 1px solid rgba(0,0,0,0.08);
|
||||||
border-bottom: 1px solid rgba(0,0,0,0.08);
|
border-bottom: 1px solid rgba(0,0,0,0.08);
|
||||||
}
|
}
|
||||||
.portrait-score-num {
|
.portrait-score-num {
|
||||||
font-family: 'Source Code Pro', monospace;
|
font-family: 'Source Code Pro', monospace;
|
||||||
font-size: 110pt;
|
font-size: 130pt;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 0.95;
|
line-height: 0.92;
|
||||||
color: {{ score_color }};
|
color: {{ score_color }};
|
||||||
flex-shrink: 0;
|
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 {
|
.portrait-score-side {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -201,44 +201,48 @@
|
|||||||
}
|
}
|
||||||
.portrait-score-label {
|
.portrait-score-label {
|
||||||
font-family: 'Source Code Pro', monospace;
|
font-family: 'Source Code Pro', monospace;
|
||||||
font-size: 12pt;
|
font-size: 13pt;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.12em;
|
letter-spacing: 0.12em;
|
||||||
color: #555;
|
color: #555;
|
||||||
}
|
}
|
||||||
.portrait-verdict {
|
.portrait-verdict {
|
||||||
font-size: 24pt;
|
font-size: 28pt;
|
||||||
color: {{ score_color }};
|
color: {{ score_color }};
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1.15;
|
line-height: 1.12;
|
||||||
}
|
}
|
||||||
.portrait-matrix-block {
|
.portrait-matrix-block {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: stretch;
|
||||||
gap: 24px;
|
gap: 22px;
|
||||||
}
|
}
|
||||||
.portrait-matrix {
|
.portrait-matrix {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(5, 1fr);
|
grid-template-columns: repeat(5, 1fr);
|
||||||
grid-template-rows: repeat(5, 1fr);
|
grid-template-rows: repeat(5, 1fr);
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
width: 380px;
|
width: 460px;
|
||||||
height: 380px;
|
height: 460px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.portrait-matrix-legend {
|
.portrait-matrix-legend {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-family: 'Source Code Pro', monospace;
|
font-family: 'Source Code Pro', monospace;
|
||||||
font-size: 11pt;
|
font-size: 12pt;
|
||||||
color: #555;
|
color: #555;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.portrait-matrix-legend .l-title {
|
.portrait-matrix-legend .l-title {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: #009DA5;
|
color: #009DA5;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 10px;
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.06em;
|
||||||
|
font-size: 13pt;
|
||||||
}
|
}
|
||||||
.portrait-matrix-legend ul {
|
.portrait-matrix-legend ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
@ -248,18 +252,18 @@
|
|||||||
.portrait-matrix-legend li {
|
.portrait-matrix-legend li {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 8px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
.portrait-matrix-legend .swatch {
|
.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 {
|
.portrait-summary {
|
||||||
font-size: 14pt;
|
font-size: 15pt;
|
||||||
line-height: 1.55;
|
line-height: 1.5;
|
||||||
color: #333;
|
color: #333;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 5;
|
-webkit-line-clamp: 6;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@ -267,9 +271,9 @@
|
|||||||
/* ── Footer ── */
|
/* ── Footer ── */
|
||||||
.footer {
|
.footer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: {% if is_og %}16px{% elif is_portrait %}24px{% else %}12px{% endif %};
|
bottom: {% if is_og %}12px{% elif is_portrait %}16px{% else %}10px{% endif %};
|
||||||
left: {% if is_og %}48px{% elif is_portrait %}56px{% else %}36px{% endif %};
|
left: {% if is_og %}38px{% elif is_portrait %}38px{% else %}30px{% endif %};
|
||||||
right: {% if is_og %}48px{% elif is_portrait %}56px{% else %}36px{% endif %};
|
right: {% if is_og %}38px{% elif is_portrait %}38px{% else %}30px{% endif %};
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -277,7 +281,7 @@
|
|||||||
font-size: 9pt;
|
font-size: 9pt;
|
||||||
color: #888;
|
color: #888;
|
||||||
border-top: 1px solid rgba(0,0,0,0.1);
|
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; }
|
.footer .brand { color: #009DA5; font-weight: 700; letter-spacing: 0.06em; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user