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")
|
||||
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).
|
||||
|
||||
|
||||
@ -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.');
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user