From 381c39a93c3859ffd159416fbbef28443aea61b8 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sat, 25 Jul 2026 18:51:57 +0200 Subject: [PATCH] =?UTF-8?q?Add=20Schlangajass=20UI,=20Weis-Hilfe=20und=20D?= =?UTF-8?q?eploy=20f=C3=BCr=20jassen.local/pauker.at.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- README.md | 84 +++++++- deploy/apache-jassen.local.conf | 15 ++ deploy/apache-jassen.pauker.at.conf | 11 + deploy/nginx-jassen.pauker.at.conf | 21 ++ deploy/server-setup.sh | 23 ++ deploy/setup-local.ps1 | 34 +++ public/.htaccess | 6 + src/App.css | 224 ++++++++++++++----- src/App.tsx | 323 ++++++++++++++++++++++------ src/game.ts | 8 + vite.config.ts | 12 ++ 11 files changed, 633 insertions(+), 128 deletions(-) create mode 100644 deploy/apache-jassen.local.conf create mode 100644 deploy/apache-jassen.pauker.at.conf create mode 100644 deploy/nginx-jassen.pauker.at.conf create mode 100644 deploy/server-setup.sh create mode 100644 deploy/setup-local.ps1 create mode 100644 public/.htaccess diff --git a/README.md b/README.md index cbabe05..09c3245 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,90 @@ Mobile-first Web-App zum Punktezählen beim Kreuzjassen (Schlangajass). -## Start +## URLs + +| Umgebung | Adresse | +|----------|---------| +| Lokal (Apache / Build) | http://jassen.local/ | +| Lokal (Vite Dev) | http://jassen.local:5173/ | +| Server | https://jassen.pauker.at | + +## Lokal einrichten + +1. Einmalig als Administrator: `deploy\setup-local.ps1` + (trägt `jassen.local` in die hosts-Datei ein und startet WAMP-Apache) +2. Build: `npm run build` +3. Öffnen: http://jassen.local/ + +Apache-VHost liegt in `httpd-vhosts.conf` (DocumentRoot → `dist/`). Vorlage: `deploy/apache-jassen.local.conf`. + +## Entwicklung ```bash npm install npm run dev ``` -Standard-Zielpunktzahl: **1000**. Partie wird im Browser gespeichert (`localStorage`). +Dann http://jassen.local:5173/ (hosts-Eintrag nötig). + +## Server (jassen.pauker.at → `/web/jassen`) + +### Einmalig auf dem Debian-Server + +```bash +# 1) Repo klonen + bauen +sudo mkdir -p /web +sudo git clone https://git.pauker.at/Stefan/jassen.git /web/jassen +sudo chown -R "$USER:$USER" /web/jassen +cd /web/jassen +# falls privat: SSH-URL nutzen, z.B. git@git.pauker.at:Stefan/jassen.git +npm ci +npm run build +``` + +Oder das Skript: `bash deploy/server-setup.sh` + +### Webserver + +**Nginx** (Beispiel): + +```bash +sudo cp /web/jassen/deploy/nginx-jassen.pauker.at.conf /etc/nginx/sites-available/jassen.pauker.at +sudo ln -sf /etc/nginx/sites-available/jassen.pauker.at /etc/nginx/sites-enabled/ +sudo nginx -t && sudo systemctl reload nginx +``` + +**Apache**: `deploy/apache-jassen.pauker.at.conf` — DocumentRoot = `/web/jassen/dist` + +### DNS + +`jassen.pauker.at` A/AAAA → Server-IP. Optional HTTPS mit Certbot: + +```bash +sudo certbot --nginx -d jassen.pauker.at +``` + +### Updates später + +```bash +cd /web/jassen +git pull +npm ci +npm run build +``` + +### Alternative: nur `dist/` von Windows pushen + +```powershell +npm run build +scp -r dist/* USER@SERVER:/web/jassen/dist/ +``` + +(VHost muss dann trotzdem auf `/web/jassen/dist` zeigen.) ## Nutzung -1. Teamnamen und Ziel festlegen -2. Pro Runde: Team mit weniger Punkten wählen und Summe eingeben (Rest = 157 − x) -3. Optional Weis und Matsch (257) -4. Stand mit Schlangen-Strichen (100 / 50 / 20) +- Standard-Ziel: **1000** Punkte +- Pro Runde: Team, das gezählt hat, und Summe (Rest = 157 − x) +- Weis und Matsch möglich +- Stand als Schlangajass (100 / 50 / 20) diff --git a/deploy/apache-jassen.local.conf b/deploy/apache-jassen.local.conf new file mode 100644 index 0000000..38d8982 --- /dev/null +++ b/deploy/apache-jassen.local.conf @@ -0,0 +1,15 @@ +# +# Lokal: http://jassen.local/ +# DocumentRoot zeigt auf den Vite-Build (dist). +# Nach Änderungen: npm run build +# + + ServerName jassen.local + DocumentRoot "c:/users/stefan/nextcloud/web/jassen/dist" + + Options +Indexes +Includes +FollowSymLinks +MultiViews + AllowOverride All + Require local + FallbackResource /index.html + + diff --git a/deploy/apache-jassen.pauker.at.conf b/deploy/apache-jassen.pauker.at.conf new file mode 100644 index 0000000..2b654fe --- /dev/null +++ b/deploy/apache-jassen.pauker.at.conf @@ -0,0 +1,11 @@ +# Apache auf Debian für jassen.pauker.at + + ServerName jassen.pauker.at + DocumentRoot /web/jassen/dist + + Options -Indexes +FollowSymLinks + AllowOverride All + Require all granted + FallbackResource /index.html + + diff --git a/deploy/nginx-jassen.pauker.at.conf b/deploy/nginx-jassen.pauker.at.conf new file mode 100644 index 0000000..11178a6 --- /dev/null +++ b/deploy/nginx-jassen.pauker.at.conf @@ -0,0 +1,21 @@ +# Server: https://jassen.pauker.at +# DocumentRoot: /web/jassen/dist (Vite-Build) + +server { + listen 80; + listen [::]:80; + server_name jassen.pauker.at; + + root /web/jassen/dist; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location ~* \.(js|css|png|svg|ico|woff2?)$ { + expires 7d; + add_header Cache-Control "public"; + try_files $uri =404; + } +} diff --git a/deploy/server-setup.sh b/deploy/server-setup.sh new file mode 100644 index 0000000..966924b --- /dev/null +++ b/deploy/server-setup.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# Auf dem Debian-Server ausführen (einmalig oder nach Updates). +# Voraussetzung: Node.js 20+, Git, Zugriff auf git.pauker.at +set -euo pipefail + +APP=/web/jassen +REPO=https://git.pauker.at/Stefan/jassen.git + +if [ ! -d "$APP/.git" ]; then + sudo mkdir -p /web + sudo git clone "$REPO" "$APP" + sudo chown -R "$USER:$USER" "$APP" +else + cd "$APP" + git pull --ff-only +fi + +cd "$APP" +npm ci +npm run build + +echo "Build fertig: $APP/dist" +echo "Danach VHost aktivieren (siehe deploy/) und DNS auf jassen.pauker.at setzen." diff --git a/deploy/setup-local.ps1 b/deploy/setup-local.ps1 new file mode 100644 index 0000000..8dc2ba5 --- /dev/null +++ b/deploy/setup-local.ps1 @@ -0,0 +1,34 @@ +# Als Administrator ausführen: +# Rechtsklick PowerShell → „Als Administrator ausführen“, dann: +# Set-ExecutionPolicy -Scope Process Bypass; .\deploy\setup-local.ps1 + +$ErrorActionPreference = 'Stop' + +$hostsPath = 'C:\Windows\System32\drivers\etc\hosts' +$hostsLine4 = '127.0.0.1 jassen.local' +$hostsLine6 = '::1 jassen.local' +$content = Get-Content $hostsPath -Raw +if ($content -notmatch 'jassen\.local') { + Add-Content -Path $hostsPath -Value "`r`n$hostsLine4`r`n$hostsLine6`r`n" + Write-Host "hosts: jassen.local eingetragen" +} else { + Write-Host "hosts: jassen.local war schon da" +} + +$apache = Get-Service wampapache64 -ErrorAction SilentlyContinue +if ($apache) { + if ($apache.Status -ne 'Running') { + Start-Service wampapache64 + Write-Host "Apache gestartet" + } else { + Restart-Service wampapache64 + Write-Host "Apache neu gestartet" + } +} else { + Write-Host "Hinweis: Dienst wampapache64 nicht gefunden — WAMP manuell starten." +} + +Write-Host "" +Write-Host "Lokal: http://jassen.local/" +Write-Host "Dev: npm run dev → http://jassen.local:5173/" +Write-Host "Server: https://jassen.pauker.at (siehe deploy/)" diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..c431d92 --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,6 @@ +RewriteEngine On +RewriteBase / +RewriteRule ^index\.html$ - [L] +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule . /index.html [L] diff --git a/src/App.css b/src/App.css index 32d6266..b6eb187 100644 --- a/src/App.css +++ b/src/App.css @@ -237,6 +237,123 @@ input { gap: 0.75rem; } +.weis-block .hint { + margin: 0 0 0.65rem; + font-size: 0.8rem; + font-weight: 500; + color: var(--ink-soft); + line-height: 1.35; +} + +.weis-team { + display: flex; + flex-direction: column; + gap: 0.45rem; + padding: 0.55rem 0.45rem 0.6rem; + background: color-mix(in srgb, var(--felt) 6%, var(--paper)); + border-radius: 8px; +} + +.weis-team.off { + opacity: 0.45; +} + +.weis-head { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 0.35rem; + font-size: 0.8rem; + font-weight: 600; + color: var(--ink-soft); +} + +.weis-head strong { + font-family: var(--font-display); + font-size: 1.35rem; + color: var(--ink); + font-variant-numeric: tabular-nums; +} + +.weis-chips { + display: flex; + flex-wrap: wrap; + gap: 0.3rem; +} + +.weis-chips button { + border: 1px solid color-mix(in srgb, var(--ink) 18%, transparent); + background: var(--paper); + color: var(--ink); + font-size: 0.75rem; + font-weight: 700; + padding: 0.35rem 0.45rem; + border-radius: 6px; + cursor: pointer; +} + +.weis-chips button:disabled { + cursor: not-allowed; +} + +.weis-chips button:not(:disabled):active { + background: var(--felt); + color: var(--chalk); + border-color: var(--felt); +} + +.weis-edit { + display: grid; + grid-template-columns: 1fr auto; + gap: 0.3rem; + align-items: center; +} + +.weis-edit input { + width: 100%; + border: none; + border-bottom: 2px solid color-mix(in srgb, var(--ink) 18%, transparent); + background: transparent; + padding: 0.35rem 0.1rem; + color: var(--ink); + font-size: 1rem; +} + +.weis-edit input:focus { + outline: none; + border-bottom-color: var(--felt); +} + +.weis-clear { + border: none; + background: color-mix(in srgb, var(--ink) 8%, transparent); + color: var(--ink-soft); + width: 2rem; + height: 2rem; + border-radius: 6px; + cursor: pointer; + font-size: 1.1rem; + line-height: 1; +} + +.weis-clear:disabled { + opacity: 0.35; + cursor: not-allowed; +} + +.preview small { + font-size: 0.7rem; + font-weight: 600; + color: var(--felt); +} + +.weis-tag { + font-style: normal; + font-size: 0.75rem; + font-weight: 600; + color: var(--felt); +} + .preview { display: grid; grid-template-columns: 1fr 1fr; @@ -314,60 +431,54 @@ input { .sheet { flex: 1; - background: var(--paper); + background: + radial-gradient(ellipse at 20% 10%, rgba(255, 255, 255, 0.35), transparent 45%), + linear-gradient(180deg, #f4ead6 0%, var(--paper) 40%, #e8dcc4 100%); color: var(--ink); border-radius: 3px 14px 10px 4px; - padding: 1.15rem 0.9rem 1rem; + padding: 1rem 0.75rem 1rem; box-shadow: 0 1px 0 var(--paper-edge), 0 22px 48px var(--shadow); display: flex; flex-direction: column; - gap: 1rem; + gap: 0.85rem; min-height: 0; animation: sheet-in 0.45s ease-out both; } -.sheet-cols { +.snake-board { + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.snake-heads { display: grid; - grid-template-columns: 1fr 1px 1fr; + grid-template-columns: 1fr 1fr; gap: 0.5rem; - align-items: start; + padding: 0 0.5rem; } -.sheet-gutter { - background: linear-gradient( - to bottom, - transparent, - color-mix(in srgb, var(--ink) 25%, transparent) 8%, - color-mix(in srgb, var(--ink) 25%, transparent) 92%, - transparent - ); - width: 1px; - align-self: stretch; - min-height: 8rem; -} - -.col { +.snake-head { text-align: center; - padding: 0.25rem 0.35rem; transition: opacity 0.2s; } -.col:not(.lead) { - opacity: 0.72; +.snake-head:not(.lead) { + opacity: 0.62; } -.col h2 { +.snake-head h2 { font-family: var(--font-display); font-size: 1.05rem; font-weight: 600; - margin: 0 0 0.2rem; + margin: 0 0 0.15rem; } .total { font-family: var(--font-display); - font-size: clamp(2.4rem, 12vw, 3rem); + font-size: clamp(2.35rem, 11vw, 2.9rem); font-weight: 650; font-variant-numeric: tabular-nums; margin: 0; @@ -387,40 +498,47 @@ input { } } -.snake { - margin-top: 0.85rem; - display: flex; - flex-direction: column; - gap: 0.25rem; - min-height: 3.5rem; +/* Schrieba nach jassa.at: alles in einem SVG → Striche treffen die Linien */ +.schrieba { + display: block; + width: 100%; + height: auto; + margin: 0.1rem 0 0; + overflow: visible; } -.snake-row { - display: flex; - align-items: baseline; - justify-content: center; - gap: 0.35rem; - font-size: 0.8rem; +.schrieba-mid { + stroke: color-mix(in srgb, var(--ink) 28%, transparent); + stroke-width: 1.3; } -.snake-label { - color: var(--ink-soft); - font-weight: 600; - width: 1.6rem; - text-align: right; +.schrieba-snake { + fill: none; + stroke: #2a5f9e; + stroke-width: 3; + stroke-linecap: round; + stroke-linejoin: round; } -.ticks { - letter-spacing: -0.08em; - font-weight: 500; - color: var(--felt); +.schrieba-label { + fill: color-mix(in srgb, var(--ink) 45%, transparent); + font-family: var(--font-ui); + font-size: 9px; + font-weight: 700; + letter-spacing: 0.04em; } -.rest { - margin: 0.15rem 0 0; - font-size: 0.85rem; - font-weight: 600; - color: var(--ink-soft); +.svg-stroke { + stroke: #14110e; + stroke-width: 2.6; + stroke-linecap: round; +} + +.schrieba-rest-svg { + fill: var(--ink); + font-family: var(--font-display); + font-size: 16px; + font-weight: 650; } .history { @@ -431,7 +549,7 @@ input { display: flex; flex-direction: column; gap: 0.35rem; - max-height: 40vh; + max-height: 32vh; overflow: auto; font-variant-numeric: tabular-nums; } diff --git a/src/App.tsx b/src/App.tsx index 32db954..0cfd925 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,7 @@ import { ROUND_TOTAL, snakeMarks, totals, + WEIS_OPTIONS, winner, } from './game' import { clearGame, loadGame, saveGame } from './storage' @@ -182,11 +183,14 @@ function PlayScreen({ )}
-
- = score.b} /> - +
    {game.rounds.length === 0 && ( @@ -201,10 +205,16 @@ function PlayScreen({ {r.pointsA} {round.matsch && round.team === 'a' ? ' M' : ''} + {round.weisA > 0 && !(round.matsch && round.team !== 'a') ? ( + W{round.weisA} + ) : null} {r.pointsB} {round.matsch && round.team === 'b' ? ' M' : ''} + {round.weisB > 0 && !(round.matsch && round.team !== 'b') ? ( + W{round.weisB} + ) : null} ) @@ -227,38 +237,160 @@ function PlayScreen({ ) } -function TeamColumn({ - name, - total, - marks, - leading, +function SnakeBoard({ + teamA, + teamB, + scoreA, + scoreB, + marksA, + marksB, }: { - name: string - total: number - marks: ReturnType - leading: boolean + teamA: string + teamB: string + scoreA: number + scoreB: number + marksA: ReturnType + marksB: ReturnType }) { + // Einheitlicher Wendungsradius: Linienabstand = 2R + const R = 28 + const Y100 = 46 + const Y50 = Y100 + 2 * R + const Y20 = Y50 + 2 * R + const left = 72 + const right = 248 + return ( -
    -

    {name}

    -

    {total}

    -