diff --git a/app/main.py b/app/main.py index 2b86e84..bcaee73 100644 --- a/app/main.py +++ b/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). diff --git a/app/templates/v2/screens/antrag_detail.html b/app/templates/v2/screens/antrag_detail.html index 1c70003..961111f 100644 --- a/app/templates/v2/screens/antrag_detail.html +++ b/app/templates/v2/screens/antrag_detail.html @@ -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.'); diff --git a/app/templates/v2/screens/scorecard.html b/app/templates/v2/screens/scorecard.html index 673d7cc..8fbbd92 100644 --- a/app/templates/v2/screens/scorecard.html +++ b/app/templates/v2/screens/scorecard.html @@ -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; }