ci: switch to Gitea Container Registry for reliable deploys
Previous pipeline built images locally via Portainer Docker API, but Docker layer caching produced identical images. Now: - Build with nocache=1 - Push to Gitea registry (git.home.rm-warpstation.de) - Compose uses image: from registry instead of build: - Redeploy with pullImage: true forces fresh container Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+67
-16
@@ -4,6 +4,10 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: git.home.rm-warpstation.de
|
||||||
|
IMAGE: mwf975_git/tippspiel
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: self-hosted
|
runs-on: self-hosted
|
||||||
@@ -33,42 +37,89 @@ jobs:
|
|||||||
|
|
||||||
- name: Build Docker Image via Portainer
|
- name: Build Docker Image via Portainer
|
||||||
run: |
|
run: |
|
||||||
echo "Starting Docker build on NAS..."
|
IMAGE_TAG="${{ env.REGISTRY }}/${{ env.IMAGE }}:latest"
|
||||||
|
echo "Building image: $IMAGE_TAG"
|
||||||
curl -s -k -X POST \
|
curl -s -k -X POST \
|
||||||
"https://192.168.1.60:9444/api/endpoints/2/docker/build?t=wm2026-tippspiel:latest&dockerfile=./Dockerfile" \
|
"https://192.168.1.60:9444/api/endpoints/2/docker/build?t=${IMAGE_TAG}&dockerfile=./Dockerfile&nocache=1" \
|
||||||
-H "X-API-Key: ${{ secrets.PORTAINER_TOKEN }}" \
|
-H "X-API-Key: ${{ secrets.PORTAINER_TOKEN }}" \
|
||||||
-H "Content-Type: application/x-tar" \
|
-H "Content-Type: application/x-tar" \
|
||||||
--data-binary @/tmp/tippspiel-ci.tar \
|
--data-binary @/tmp/tippspiel-ci.tar \
|
||||||
--max-time 600 \
|
--max-time 600 \
|
||||||
| grep -E '(Successfully built|Successfully tagged|error|Error)' || true
|
| grep -E '(Successfully|error|Error)' || true
|
||||||
echo "Build completed."
|
echo "Build completed."
|
||||||
|
|
||||||
|
- name: Push to Gitea Registry
|
||||||
|
run: |
|
||||||
|
IMAGE_TAG="${{ env.REGISTRY }}/${{ env.IMAGE }}:latest"
|
||||||
|
|
||||||
|
# Login to Gitea registry via Portainer Docker API
|
||||||
|
LOGIN_PAYLOAD=$(python3 -c "import json; print(json.dumps({'username': 'mwf975_git', 'password': '${{ secrets.DEPLOY_TOKEN }}', 'serveraddress': 'https://${{ env.REGISTRY }}'}))")
|
||||||
|
curl -s -k -X POST \
|
||||||
|
"https://192.168.1.60:9444/api/endpoints/2/docker/auth" \
|
||||||
|
-H "X-API-Key: ${{ secrets.PORTAINER_TOKEN }}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$LOGIN_PAYLOAD" || true
|
||||||
|
|
||||||
|
# Push image to registry
|
||||||
|
echo "Pushing $IMAGE_TAG..."
|
||||||
|
AUTH_HEADER=$(python3 -c "import base64,json; print(base64.urlsafe_b64encode(json.dumps({'username':'mwf975_git','password':'${{ secrets.DEPLOY_TOKEN }}'}).encode()).decode())")
|
||||||
|
curl -s -k -X POST \
|
||||||
|
"https://192.168.1.60:9444/api/endpoints/2/docker/images/${IMAGE_TAG}/push" \
|
||||||
|
-H "X-API-Key: ${{ secrets.PORTAINER_TOKEN }}" \
|
||||||
|
-H "X-Registry-Auth: $AUTH_HEADER" \
|
||||||
|
--max-time 300 || true
|
||||||
|
echo "Push completed."
|
||||||
|
|
||||||
- name: Redeploy Stack via Portainer
|
- name: Redeploy Stack via Portainer
|
||||||
run: |
|
run: |
|
||||||
echo "Fetching current stack config from Portainer..."
|
echo "Fetching current stack config from Portainer..."
|
||||||
|
|
||||||
# Aktuelles Compose-File aus Portainer lesen
|
# Aktuelle Env-Vars aus Portainer lesen
|
||||||
STACK_FILE=$(curl -s -k \
|
|
||||||
"https://192.168.1.60:9444/api/stacks/115/file" \
|
|
||||||
-H "X-API-Key: ${{ secrets.PORTAINER_TOKEN }}" \
|
|
||||||
| python3 -c "import sys,json; print(json.load(sys.stdin).get('StackFileContent',''))")
|
|
||||||
|
|
||||||
# Aktuelle Env-Vars aus Portainer lesen (enthält die echten Werte)
|
|
||||||
ENV_VARS=$(curl -s -k \
|
ENV_VARS=$(curl -s -k \
|
||||||
"https://192.168.1.60:9444/api/stacks/115" \
|
"https://192.168.1.60:9444/api/stacks/115" \
|
||||||
-H "X-API-Key: ${{ secrets.PORTAINER_TOKEN }}" \
|
-H "X-API-Key: ${{ secrets.PORTAINER_TOKEN }}" \
|
||||||
| python3 -c "import sys,json; print(json.dumps(json.load(sys.stdin).get('Env', [])))")
|
| python3 -c "import sys,json; print(json.dumps(json.load(sys.stdin).get('Env', [])))")
|
||||||
|
|
||||||
# Stack mit bestehender Konfiguration neu deployen (keine Credentials im Workflow)
|
# Stack mit Image-Pull neu deployen
|
||||||
PAYLOAD=$(python3 -c "
|
PAYLOAD=$(python3 -c "
|
||||||
import json, sys
|
import json
|
||||||
stack_file = '''$STACK_FILE'''
|
|
||||||
env_vars = $ENV_VARS
|
env_vars = $ENV_VARS
|
||||||
print(json.dumps({
|
print(json.dumps({
|
||||||
'stackFileContent': stack_file,
|
'stackFileContent': '''services:
|
||||||
|
tippspiel:
|
||||||
|
image: ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest
|
||||||
|
container_name: wm2026-tippspiel
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- \"3301:3001\"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=\${NODE_ENV}
|
||||||
|
- PORT=\${PORT}
|
||||||
|
- DATABASE_URL=\${DATABASE_URL}
|
||||||
|
- SUPABASE_URL=\${SUPABASE_URL}
|
||||||
|
- SUPABASE_SERVICE_ROLE_KEY=\${SUPABASE_SERVICE_ROLE_KEY}
|
||||||
|
- ANTHROPIC_API_KEY=\${ANTHROPIC_API_KEY}
|
||||||
|
- FOOTBALL_API_KEY=\${FOOTBALL_API_KEY}
|
||||||
|
- FOOTBALL_API_BASE_URL=\${FOOTBALL_API_BASE_URL}
|
||||||
|
- ELEVENLABS_API_KEY=\${ELEVENLABS_API_KEY}
|
||||||
|
- CORS_ORIGIN=\${CORS_ORIGIN}
|
||||||
|
- STAFFBASE_PUBLIC_KEY=\${STAFFBASE_PUBLIC_KEY:-}
|
||||||
|
- STAFFBASE_PLUGIN_ID=\${STAFFBASE_PLUGIN_ID:-}
|
||||||
|
healthcheck:
|
||||||
|
test: [\"CMD\", \"wget\", \"-qO-\", \"http://localhost:3001/health\"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
start_period: 10s
|
||||||
|
retries: 3
|
||||||
|
networks:
|
||||||
|
- main-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
main-network:
|
||||||
|
external: true''',
|
||||||
'env': env_vars,
|
'env': env_vars,
|
||||||
'prune': True,
|
'prune': True,
|
||||||
'pullImage': False
|
'pullImage': True
|
||||||
}))
|
}))
|
||||||
")
|
")
|
||||||
|
|
||||||
@@ -84,7 +135,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Verify deployment
|
- name: Verify deployment
|
||||||
run: |
|
run: |
|
||||||
sleep 15
|
sleep 20
|
||||||
STATUS=$(curl -s http://192.168.1.60:3301/health | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('status'))" 2>/dev/null || echo "unreachable")
|
STATUS=$(curl -s http://192.168.1.60:3301/health | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('status'))" 2>/dev/null || echo "unreachable")
|
||||||
echo "Health check: $STATUS"
|
echo "Health check: $STATUS"
|
||||||
if [ "$STATUS" = "ok" ]; then
|
if [ "$STATUS" = "ok" ]; then
|
||||||
|
|||||||
+1
-3
@@ -1,8 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
tippspiel:
|
tippspiel:
|
||||||
build:
|
image: git.home.rm-warpstation.de/mwf975_git/tippspiel:latest
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: wm2026-tippspiel
|
container_name: wm2026-tippspiel
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -14,5 +14,5 @@ export default function ConfettiReveal({ match, onDismiss }) {
|
|||||||
}, [points]);
|
}, [points]);
|
||||||
const resultLabel = points === 3 ? 'EXAKT! 🎉' : points === 1 ? 'Richtige Tendenz! 👏' : 'Knapp daneben... 😅';
|
const resultLabel = points === 3 ? 'EXAKT! 🎉' : points === 1 ? 'Richtige Tendenz! 👏' : 'Knapp daneben... 😅';
|
||||||
const badgeClass = points === 3 ? styles.badgeExact : points === 1 ? styles.badgeTendency : styles.badgeWrong;
|
const badgeClass = points === 3 ? styles.badgeExact : points === 1 ? styles.badgeTendency : styles.badgeWrong;
|
||||||
return (_jsx("div", { className: styles.overlay, onClick: onDismiss, children: _jsxs("div", { className: styles.card, onClick: e => e.stopPropagation(), children: [_jsxs("div", { className: styles.result, children: [match.homeTeam.shortName, " ", match.score.home, ":", match.score.away, " ", match.awayTeam.shortName] }), _jsxs("div", { className: styles.tipLine, children: ["Dein Tipp: ", tip.home, ":", tip.away] }), _jsxs("div", { className: `${styles.badge} ${badgeClass}`, children: [points, " ", points === 1 ? 'Punkt' : 'Punkte'] }), _jsx("div", { className: styles.label, children: resultLabel }), _jsx("button", { className: styles.dismissBtn, onClick: onDismiss, children: "Weiter" })] }) }));
|
return (_jsx("div", { className: styles.overlay, onClick: onDismiss, children: _jsxs("div", { className: `card ${styles.card}`, onClick: e => e.stopPropagation(), children: [_jsxs("div", { className: styles.result, children: [match.homeTeam.shortName, " ", match.score.home, ":", match.score.away, " ", match.awayTeam.shortName] }), _jsxs("div", { className: styles.tipLine, children: ["Dein Tipp: ", tip.home, ":", tip.away] }), _jsxs("div", { className: `${styles.badge} ${badgeClass}`, children: [points, " ", points === 1 ? 'Punkt' : 'Punkte'] }), _jsx("div", { className: styles.label, children: resultLabel }), _jsx("button", { className: styles.dismissBtn, onClick: onDismiss, children: "Weiter" })] }) }));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ import styles from './StatsRing.module.css';
|
|||||||
export default function StatsRing({ exact, tendency, wrong, total }) {
|
export default function StatsRing({ exact, tendency, wrong, total }) {
|
||||||
const radius = 55;
|
const radius = 55;
|
||||||
const circumference = 2 * Math.PI * radius;
|
const circumference = 2 * Math.PI * radius;
|
||||||
const all = exact + tendency + wrong || 1;
|
const all = exact + tendency + wrong;
|
||||||
const segments = [
|
const hasData = all > 0;
|
||||||
{ value: exact / all, color: 'var(--gold)', label: 'Exakt' },
|
const segments = hasData ? [
|
||||||
{ value: tendency / all, color: 'var(--success)', label: 'Tendenz' },
|
{ value: exact / all, color: 'var(--gold)', label: 'Exakt', count: exact },
|
||||||
{ value: wrong / all, color: 'var(--error)', label: 'Falsch' },
|
{ value: tendency / all, color: 'var(--success)', label: 'Tendenz', count: tendency },
|
||||||
];
|
{ value: wrong / all, color: 'var(--error)', label: 'Falsch', count: wrong },
|
||||||
|
] : [];
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
return (_jsxs("div", { className: styles.ring, children: [_jsxs("svg", { viewBox: "0 0 140 140", className: styles.svg, children: [_jsx("circle", { cx: "70", cy: "70", r: radius, fill: "none", stroke: "var(--surface-high)", strokeWidth: "12" }), segments.map((seg, i) => {
|
return (_jsxs("div", { className: styles.ring, children: [_jsxs("svg", { viewBox: "0 0 140 140", className: styles.svg, children: [_jsx("circle", { cx: "70", cy: "70", r: radius, fill: "none", stroke: "var(--surface-high)", strokeWidth: "12" }), segments.map((seg, i) => {
|
||||||
if (seg.value === 0)
|
if (seg.value === 0)
|
||||||
@@ -17,5 +18,5 @@ export default function StatsRing({ exact, tendency, wrong, total }) {
|
|||||||
const rotation = offset * 360 - 90;
|
const rotation = offset * 360 - 90;
|
||||||
offset += seg.value;
|
offset += seg.value;
|
||||||
return (_jsx("circle", { cx: "70", cy: "70", r: radius, fill: "none", stroke: seg.color, strokeWidth: "12", strokeDasharray: dashArray, transform: `rotate(${rotation} 70 70)`, strokeLinecap: "round" }, i));
|
return (_jsx("circle", { cx: "70", cy: "70", r: radius, fill: "none", stroke: seg.color, strokeWidth: "12", strokeDasharray: dashArray, transform: `rotate(${rotation} 70 70)`, strokeLinecap: "round" }, i));
|
||||||
}), _jsx("text", { x: "70", y: "65", textAnchor: "middle", dominantBaseline: "central", fill: "var(--text-primary)", fontSize: "28", fontWeight: "700", children: total }), _jsx("text", { x: "70", y: "85", textAnchor: "middle", fill: "var(--text-secondary)", fontSize: "11", children: "Punkte" })] }), _jsx("div", { className: styles.legend, children: segments.map((seg, i) => (_jsxs("span", { className: styles.legendItem, children: [_jsx("span", { className: styles.dot, style: { background: seg.color } }), seg.label, ": ", Math.round(seg.value * all)] }, i))) })] }));
|
}), _jsx("text", { x: "70", y: "65", textAnchor: "middle", dominantBaseline: "central", fill: "var(--text-primary)", fontSize: "28", fontWeight: "700", children: total }), _jsx("text", { x: "70", y: "85", textAnchor: "middle", fill: "var(--text-secondary)", fontSize: "11", children: hasData ? 'Punkte' : 'Keine Tipps' })] }), hasData && (_jsx("div", { className: styles.legend, children: segments.map((seg, i) => (_jsxs("span", { className: styles.legendItem, children: [_jsx("span", { className: styles.dot, style: { background: seg.color } }), seg.label, ": ", seg.count] }, i))) }))] }));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { jsx as _jsx } from "react/jsx-runtime";
|
import { jsx as _jsx } from "react/jsx-runtime";
|
||||||
import { useEffect } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import styles from './Toast.module.css';
|
import styles from './Toast.module.css';
|
||||||
export default function Toast({ message, onDismiss, duration = 5000 }) {
|
export default function Toast({ message, onDismiss, duration = 5000 }) {
|
||||||
|
const onDismissRef = useRef(onDismiss);
|
||||||
|
onDismissRef.current = onDismiss;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(onDismiss, duration);
|
const timer = setTimeout(() => onDismissRef.current(), duration);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [onDismiss, duration]);
|
}, [duration]);
|
||||||
return (_jsx("div", { className: styles.toast, onClick: onDismiss, children: message }));
|
return (_jsx("div", { className: `card ${styles.toast}`, onClick: onDismiss, children: message }));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { api } from '../api/client';
|
import { api } from '../api/client';
|
||||||
const RANK_KEY = 'tippspiel_last_rank';
|
const RANK_KEY = 'tippspiel_last_rank';
|
||||||
|
const SHOWN_KEY = 'tippspiel_rank_toast_shown';
|
||||||
export function useRankChange() {
|
export function useRankChange() {
|
||||||
const [message, setMessage] = useState(null);
|
const [message, setMessage] = useState(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Only show once per session
|
||||||
|
if (sessionStorage.getItem(SHOWN_KEY))
|
||||||
|
return;
|
||||||
api.getMyStats().then(stats => {
|
api.getMyStats().then(stats => {
|
||||||
if (!stats.rank)
|
if (!stats.rank)
|
||||||
return;
|
return;
|
||||||
@@ -15,6 +19,7 @@ export function useRankChange() {
|
|||||||
else {
|
else {
|
||||||
setMessage(`⬇️ Du bist auf Platz ${stats.rank} gerutscht — hol dir die Punkte zurück!`);
|
setMessage(`⬇️ Du bist auf Platz ${stats.rank} gerutscht — hol dir die Punkte zurück!`);
|
||||||
}
|
}
|
||||||
|
sessionStorage.setItem(SHOWN_KEY, '1');
|
||||||
}
|
}
|
||||||
localStorage.setItem(RANK_KEY, String(stats.rank));
|
localStorage.setItem(RANK_KEY, String(stats.rank));
|
||||||
}).catch(() => { });
|
}).catch(() => { });
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export default function DashboardPage(_props) {
|
|||||||
if (error || !data)
|
if (error || !data)
|
||||||
return _jsx("div", { className: styles.error, children: "Dashboard konnte nicht geladen werden." });
|
return _jsx("div", { className: styles.error, children: "Dashboard konnte nicht geladen werden." });
|
||||||
const { hero, stats, nudges } = data;
|
const { hero, stats, nudges } = data;
|
||||||
return (_jsxs("div", { className: styles.dashboard, children: [_jsxs("div", { className: styles.hero, onClick: () => navigate('/spiele'), children: [_jsxs("div", { className: styles.heroLabel, children: [_jsx("span", { children: "N\u00E4chstes Spiel" }), hero && (_jsx("span", { className: styles.heroCountdown, children: formatCountdown(hero.match.minutesUntilKickoff) }))] }), hero ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.heroTeams, children: [_jsxs("div", { className: styles.heroTeam, children: [hero.match.homeTeam.crest ? (_jsx("img", { src: hero.match.homeTeam.crest, alt: hero.match.homeTeam.name, className: styles.heroCrest })) : (_jsx("div", { className: styles.heroCrest })), _jsx("span", { children: hero.match.homeTeam.shortName })] }), _jsx("span", { className: styles.heroVs, children: "vs" }), _jsxs("div", { className: styles.heroTeam, children: [hero.match.awayTeam.crest ? (_jsx("img", { src: hero.match.awayTeam.crest, alt: hero.match.awayTeam.name, className: styles.heroCrest })) : (_jsx("div", { className: styles.heroCrest })), _jsx("span", { children: hero.match.awayTeam.shortName })] })] }), hero.userTip ? (_jsxs("div", { className: styles.heroTip, children: ["Dein Tipp: ", hero.userTip.home, ":", hero.userTip.away, " \u2713"] })) : hero.tippable ? (_jsx("button", { className: styles.heroTipBtn, onClick: e => { e.stopPropagation(); navigate('/spiele'); }, children: "Jetzt tippen" })) : null] })) : (_jsx("p", { style: { textAlign: 'center', color: 'var(--text-muted)', margin: '16px 0' }, children: "Keine anstehenden Spiele" }))] }), _jsxs("div", { className: styles.statsRow, children: [_jsxs("div", { className: styles.statTile, children: [_jsx("span", { className: styles.statValue, children: stats.rank !== null ? stats.rank : '—' }), _jsx("span", { className: styles.statLabel, children: "Dein Rang" })] }), _jsxs("div", { className: styles.statTile, children: [_jsx("span", { className: styles.statValue, children: stats.totalPoints }), _jsx("span", { className: styles.statLabel, children: "Punkte" })] }), _jsxs("div", { className: styles.statTile, children: [_jsx("span", { className: styles.statValue, children: formatStreak(stats.streak) }), _jsx("span", { className: styles.statLabel, children: "Streak" })] })] }), nudges.length > 0 && (_jsx("div", { className: styles.nudges, children: nudges.map((nudge, i) => (_jsx("div", { className: styles.nudge, onClick: () => {
|
return (_jsxs("div", { className: styles.dashboard, children: [_jsxs("div", { className: `card ${styles.hero}`, onClick: () => navigate('/spiele'), children: [_jsxs("div", { className: styles.heroLabel, children: [_jsx("span", { children: "N\u00E4chstes Spiel" }), hero && (_jsx("span", { className: styles.heroCountdown, children: formatCountdown(hero.match.minutesUntilKickoff) }))] }), hero ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.heroTeams, children: [_jsxs("div", { className: styles.heroTeam, children: [hero.match.homeTeam.crest ? (_jsx("img", { src: hero.match.homeTeam.crest, alt: hero.match.homeTeam.name, className: styles.heroCrest })) : (_jsx("div", { className: styles.heroCrest })), _jsx("span", { children: hero.match.homeTeam.shortName })] }), _jsx("span", { className: styles.heroVs, children: "vs" }), _jsxs("div", { className: styles.heroTeam, children: [hero.match.awayTeam.crest ? (_jsx("img", { src: hero.match.awayTeam.crest, alt: hero.match.awayTeam.name, className: styles.heroCrest })) : (_jsx("div", { className: styles.heroCrest })), _jsx("span", { children: hero.match.awayTeam.shortName })] })] }), hero.userTip ? (_jsxs("div", { className: styles.heroTip, children: ["Dein Tipp: ", hero.userTip.home, ":", hero.userTip.away, " \u2713"] })) : hero.tippable ? (_jsx("button", { className: styles.heroTipBtn, onClick: e => { e.stopPropagation(); navigate('/spiele'); }, children: "Jetzt tippen" })) : null] })) : (_jsx("p", { style: { textAlign: 'center', color: 'var(--text-muted)', margin: '16px 0' }, children: "Keine anstehenden Spiele" }))] }), _jsxs("div", { className: styles.statsRow, children: [_jsxs("div", { className: `card ${styles.statTile}`, children: [_jsx("span", { className: styles.statValue, children: stats.rank !== null ? stats.rank : '—' }), _jsx("span", { className: styles.statLabel, children: "Dein Rang" })] }), _jsxs("div", { className: `card ${styles.statTile}`, children: [_jsx("span", { className: styles.statValue, children: stats.totalPoints }), _jsx("span", { className: styles.statLabel, children: "Punkte" })] }), _jsxs("div", { className: `card ${styles.statTile}`, children: [_jsx("span", { className: styles.statValue, children: formatStreak(stats.streak) }), _jsx("span", { className: styles.statLabel, children: "Streak" })] })] }), nudges.length > 0 && (_jsx("div", { className: styles.nudges, children: nudges.map((nudge, i) => (_jsx("div", { className: `card ${styles.nudge}`, onClick: () => {
|
||||||
if (nudge.type === 'untipped')
|
if (nudge.type === 'untipped')
|
||||||
navigate('/spiele');
|
navigate('/spiele');
|
||||||
else if (nudge.type === 'leader')
|
else if (nudge.type === 'leader')
|
||||||
|
|||||||
Reference in New Issue
Block a user