Compare commits

..
11 Commits
Author SHA1 Message Date
lukas 275120aa71 wip 2026-08-27 11:40:07 +02:00
lukas 44b66ac8bf wip
Build and Push Container / build (push) Failing after 37s
2026-08-27 11:39:13 +02:00
lukas ce5e97a7a6 wip
Build and Push Container / build (push) Failing after 35s
2026-08-27 11:31:11 +02:00
lukas 04764d10e4 wip 2026-08-27 11:30:34 +02:00
lukas 0ad4e26e0c wip 2026-08-27 11:29:33 +02:00
lukas fa425beef7 wip
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 33s
2026-08-27 11:04:15 +02:00
lukas d6dfbe2971 wip
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 37s
2026-08-27 11:02:16 +02:00
lukas 98544ce370 wip
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 31s
2026-08-27 11:00:49 +02:00
lukas a5806385cf wip
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 25s
2026-08-27 10:51:56 +02:00
lukas 57f2ad2f3d wip
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 27s
2026-08-27 10:46:26 +02:00
lukas c64889c3b1 wip
CI / build-and-test (pull_request) Failing after 49s
2026-08-27 10:04:59 +02:00
14 changed files with 22 additions and 813 deletions
-20
View File
@@ -1,20 +0,0 @@
node_modules
dist
dist-ssr
.git
.gitea
.github
.idea
.vscode
.env
.env.*
!.env.example
*.local
.nitro
.tanstack
.wrangler
.output
.vinxi
__unconfig*
todos.json
README.md
+4 -150
View File
@@ -1,160 +1,14 @@
name: CI name: Build and Push Container
on: on:
push: push:
branches:
- main
- proof
workflow_dispatch:
env:
REGISTRY: git.lukas-harat.com
IMAGE_NAME: ${{ github.repository }}
permissions:
contents: read
packages: write
jobs: jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Typecheck
run: npx tsc --noEmit
build: build:
needs: test
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v7
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to registry
uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} - username: lukas
username: ${{ secrets.USERNAME }}
password: ${{ secrets.TOKEN }}
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Deploy to TrueNAS
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }}
BETTER_AUTH_URL: ${{ secrets.BETTER_AUTH_URL }}
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
STAFF_SEED_EMAIL: ${{ secrets.STAFF_SEED_EMAIL }}
STAFF_SEED_PASSWORD: ${{ secrets.STAFF_SEED_PASSWORD }}
RESERVATIONS_FROM_EMAIL: ${{ secrets.RESERVATIONS_FROM_EMAIL }}
IMAGE_TAG: ${{ github.sha }}
run: |
set -euo pipefail
mkdir -p ~/.ssh
printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan \
-p "${DEPLOY_PORT:-22}" \
-H "$DEPLOY_HOST" \
>> ~/.ssh/known_hosts
echo "Creating deployment directory if needed..."
ssh \
-i ~/.ssh/deploy_key \
-p "${DEPLOY_PORT:-22}" \
"$DEPLOY_USER@$DEPLOY_HOST" \
"mkdir -p '$DEPLOY_PATH'"
echo "Uploading compose file..."
scp \
-i ~/.ssh/deploy_key \
-P "${DEPLOY_PORT:-22}" \
docker-compose.prod.yml \
"$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/docker-compose.yml"
echo "Deploying..."
ssh \
-i ~/.ssh/deploy_key \
-p "${DEPLOY_PORT:-22}" \
"$DEPLOY_USER@$DEPLOY_HOST" \
"POSTGRES_PASSWORD=$(printf '%q' "$POSTGRES_PASSWORD") \
BETTER_AUTH_SECRET=$(printf '%q' "$BETTER_AUTH_SECRET") \
BETTER_AUTH_URL=$(printf '%q' "$BETTER_AUTH_URL") \
RESEND_API_KEY=$(printf '%q' "$RESEND_API_KEY") \
RESERVATIONS_FROM_EMAIL=$(printf '%q' "$RESERVATIONS_FROM_EMAIL") \
STAFF_SEED_EMAIL=$(printf '%q' "$STAFF_SEED_EMAIL") \
STAFF_SEED_PASSWORD=$(printf '%q' "$STAFF_SEED_PASSWORD") \
IMAGE_TAG=$(printf '%q' "$IMAGE_TAG") \
bash -c '
set -euo pipefail
cd \"$DEPLOY_PATH\"
echo \"Pulling images...\"
docker compose pull
echo \"Starting PostgreSQL...\"
docker compose up -d postgres
echo "Running database migrations..."
docker compose run --rm app npx drizzle-kit migrate
echo "Seeding database..."
docker compose run --rm app npm run db:seed
echo "Starting application..."
docker compose up -d
echo \"Cleaning unused images...\"
docker image prune -f
echo \"Deployment complete.\"
'"
-31
View File
@@ -1,31 +0,0 @@
# syntax=docker/dockerfile:1
FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
# Dummy values so the build-time env checks in src/db/index.ts and
# src/lib/auth-instance.ts don't throw while bundling. The real values are
# supplied at container runtime.
ENV DATABASE_URL=postgres://build:build@localhost:5432/build \
BETTER_AUTH_SECRET=build-time-placeholder \
BETTER_AUTH_URL=http://localhost:3000
RUN npm run build
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# This build has no standalone Node server entrypoint — dist/server/server.js
# only exports a fetch handler. `vite preview` (via the tanstackStart plugin)
# is the supported way to serve it, and it re-resolves the app's router entry
# from src/ at startup, so the full source tree (not just dist/) has to ship.
COPY --from=builder /app ./
EXPOSE 3000
CMD ["npm", "run", "preview", "--", "--host", "0.0.0.0", "--port", "3000"]
-35
View File
@@ -1,35 +0,0 @@
services:
app:
image: git.lukas-harat.com/lukas/promo_steakhouse:${IMAGE_TAG:-latest}
restart: unless-stopped
ports:
- '3003:3000'
environment:
DATABASE_URL: postgres://steakhouse:${POSTGRES_PASSWORD}@postgres:5432/steakhouse
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
BETTER_AUTH_URL: ${BETTER_AUTH_URL}
STAFF_SEED_EMAIL: ${STAFF_SEED_EMAIL}
STAFF_SEED_PASSWORD: ${STAFF_SEED_PASSWORD}
RESEND_API_KEY: ${RESEND_API_KEY}
RESERVATIONS_FROM_EMAIL: ${RESERVATIONS_FROM_EMAIL}
depends_on:
postgres:
condition: service_healthy
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: steakhouse
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: steakhouse
volumes:
- steakhouse-postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U steakhouse -d steakhouse"]
interval: 5s
timeout: 5s
retries: 10
volumes:
steakhouse-postgres-data:
-278
View File
@@ -10,7 +10,6 @@
"@tanstack/react-start": "latest", "@tanstack/react-start": "latest",
"better-auth": "^1.6.29", "better-auth": "^1.6.29",
"drizzle-orm": "^0.45.2", "drizzle-orm": "^0.45.2",
"pdfjs-dist": "^6.2.108",
"postgres": "^3.4.9", "postgres": "^3.4.9",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
@@ -1311,271 +1310,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@napi-rs/canvas": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-1.0.8.tgz",
"integrity": "sha512-/SaLcvlqGWdm0HSCWMgHu7cjJiQXfP8/mOY+6dUyV9flQz7sPBBZ+ed2zYtoukojPmxOaL7bm+d/G4GeWWoN7g==",
"license": "MIT",
"optional": true,
"workspaces": [
"e2e/*"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
},
"optionalDependencies": {
"@napi-rs/canvas-android-arm64": "1.0.8",
"@napi-rs/canvas-darwin-arm64": "1.0.8",
"@napi-rs/canvas-darwin-x64": "1.0.8",
"@napi-rs/canvas-linux-arm-gnueabihf": "1.0.8",
"@napi-rs/canvas-linux-arm64-gnu": "1.0.8",
"@napi-rs/canvas-linux-arm64-musl": "1.0.8",
"@napi-rs/canvas-linux-riscv64-gnu": "1.0.8",
"@napi-rs/canvas-linux-x64-gnu": "1.0.8",
"@napi-rs/canvas-linux-x64-musl": "1.0.8",
"@napi-rs/canvas-win32-arm64-msvc": "1.0.8",
"@napi-rs/canvas-win32-x64-msvc": "1.0.8"
}
},
"node_modules/@napi-rs/canvas-android-arm64": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-1.0.8.tgz",
"integrity": "sha512-5+nkh8i3gt6lqS/d2jTZ1xAn6tdgtB4Lf1mW6T0Qm5/rXNwBuV1sAEyLEWan5o9gJPU/GuvHR3rvSeZ+FaGrbw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-darwin-arm64": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-1.0.8.tgz",
"integrity": "sha512-7jQ47gi+fZ7KJmfc/5rNyy1CYw/cu4kZ0KPIYbo9UUgSdW0bKQJpt+WihEor6s4Lyp7+xc3a+3HeyXmAEbbnPg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-darwin-x64": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-1.0.8.tgz",
"integrity": "sha512-rRjDMZs9pIRKGxgijwezplKc1RnJsqUokrA9h88bbTkqQ+7ePj0ZN4ZnZDy8Vu0tXs7KRlI2tQLaK4mx9QlxHg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-1.0.8.tgz",
"integrity": "sha512-jGcCd+8ra6Q61xKqZeiItujTpp9a9eRLcQ0jW6qYNku+WpupqOPFPY0SrsuSnXFviJwkpKYT9p7QrB4lsf3LNQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-1.0.8.tgz",
"integrity": "sha512-od6I2Y7kU7i1SwZYG2EKW8rWz6JiedtPpko4WEe1DDsiikrfaotVBCRaUTM5/yeZKaZ92EatoAS+5xG+6uJlYA==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-1.0.8.tgz",
"integrity": "sha512-yYkPbJDJiWj6N0gASA3CAvRypZmVpJnxU0DQg3aBhneLDQde9TPLKADsQkobNoJUtTT/lj46aWpzT48PDb3Qcg==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-1.0.8.tgz",
"integrity": "sha512-PB00MSKAp4VwK/xwe6duKxRKmH8UH4GIl1pqHSbxng0jnU9Dr7FwaDypDiqwNFZ774N+8G7mJLGuLtg9NTcQsg==",
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-1.0.8.tgz",
"integrity": "sha512-TWM2XWJoitLiIPCvgJh7SriC+L/T9qkYCVzC66AidsZy0QP1hkKzBzVwshCdcA3q6fIn3yE0ISbq4lMJSy8jFw==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-x64-musl": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-1.0.8.tgz",
"integrity": "sha512-hb20MxKXXb5IB7AAwN8UHz9WRsa2HmdZfjsDCzjElwJoeV1aotVEwFU4FrFQcYQVzsJQLeaCc/2Qdt/0Q72mMg==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-win32-arm64-msvc": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-1.0.8.tgz",
"integrity": "sha512-WwPN08IXE4SkL+FhJyPz/iFnycMAUkbphFIT4cmKLlvbSU0Zfn1R7BGJ3Hqky1S89QUYc0Q4IOScXb/42Re9wQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-1.0.8.tgz",
"integrity": "sha512-XkrVqKb+pxyba7kjy2LJvABFVBTE0DNpEl7MrG4OYUmaWarrXH+t54z/Czj2YxCKtizYTV4mg6phNm3x24qjhQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@noble/ciphers": { "node_modules/@noble/ciphers": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.3.0.tgz", "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.3.0.tgz",
@@ -3998,18 +3732,6 @@
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/pdfjs-dist": {
"version": "6.2.108",
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-6.2.108.tgz",
"integrity": "sha512-YxFb+SQcodN2rnX9Tn3dHYlqfb7NjlzzfONPpJd+AKoKtUjEdevTfbC07d5TcczzOK6261auRkP/M8OBHs9vFQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=22.13.0 || >=24"
},
"optionalDependencies": {
"@napi-rs/canvas": "^1.0.0"
}
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
-1
View File
@@ -21,7 +21,6 @@
"@tanstack/react-start": "latest", "@tanstack/react-start": "latest",
"better-auth": "^1.6.29", "better-auth": "^1.6.29",
"drizzle-orm": "^0.45.2", "drizzle-orm": "^0.45.2",
"pdfjs-dist": "^6.2.108",
"postgres": "^3.4.9", "postgres": "^3.4.9",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
-5
View File
@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="14" fill="#17120f"/>
<rect x="2" y="2" width="60" height="60" rx="12" fill="none" stroke="#c9a15a" stroke-width="2"/>
<text x="32" y="44" font-family="Georgia, 'Times New Roman', serif" font-size="34" font-weight="600" text-anchor="middle" fill="#e0bb78">M</text>
</svg>

Before

Width:  |  Height:  |  Size: 371 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 500"><path d="M 79.47,45.64 C 71.12,45.78 60.80,48.79 55.89,52.64 C 50.98,56.49 48.85,62.75 50.00,68.74 C 51.15,74.74 56.86,81.99 62.81,88.62 C 68.75,95.24 77.19,102.34 85.70,108.49 C 94.20,114.64 104.30,120.74 113.83,125.51 C 123.36,130.27 133.30,134.13 142.89,137.07 C 152.48,140.00 161.90,141.73 171.36,143.12 C 180.83,144.51 190.19,144.97 199.69,145.40 C 209.20,145.83 218.68,145.63 228.38,145.70 C 238.08,145.76 247.88,145.49 257.92,145.78 C 267.95,146.06 278.42,146.34 288.57,147.41 C 298.73,148.47 309.21,149.81 318.83,152.17 C 328.44,154.53 337.98,157.42 346.24,161.57 C 354.50,165.73 362.29,170.66 368.39,177.11 C 374.49,183.56 379.31,191.38 382.83,200.28 C 386.35,209.19 386.77,220.92 389.51,230.55 C 392.24,240.17 395.02,249.65 399.25,258.04 C 403.47,266.43 409.89,273.06 414.85,280.88 C 419.80,288.70 425.82,296.22 428.98,304.97 C 432.14,313.71 433.03,323.66 433.82,333.38 C 434.62,343.09 433.47,353.42 433.75,363.25 C 434.04,373.07 433.68,382.96 435.52,392.31 C 437.35,401.67 440.01,411.11 444.78,419.37 C 449.55,427.63 456.73,436.04 464.14,441.88 C 471.56,447.71 480.75,452.58 489.28,454.36 C 497.81,456.14 507.05,455.10 515.32,452.56 C 523.60,450.02 531.95,444.80 538.93,439.13 C 545.90,433.45 552.25,426.02 557.18,418.50 C 562.11,410.98 565.65,402.67 568.51,394.01 C 571.36,385.34 572.93,376.03 574.30,366.50 C 575.67,356.97 575.77,346.70 576.73,336.83 C 577.68,326.96 577.97,316.66 580.02,307.28 C 582.06,297.89 584.88,288.90 588.99,280.55 C 593.09,272.19 599.37,265.01 604.64,257.15 C 609.90,249.28 616.07,241.81 620.59,233.35 C 625.11,224.89 628.08,215.26 631.74,206.39 C 635.41,197.51 637.83,187.81 642.59,180.10 C 647.35,172.40 652.72,165.26 660.30,160.18 C 667.89,155.10 678.09,152.07 688.10,149.61 C 698.10,147.15 709.79,146.33 720.34,145.43 C 730.89,144.53 741.33,144.43 751.41,144.20 C 761.48,143.97 771.14,144.17 780.77,144.04 C 790.40,143.91 799.75,143.96 809.20,143.44 C 818.65,142.91 827.94,142.31 837.47,140.89 C 846.99,139.47 856.56,137.70 866.35,134.89 C 876.14,132.08 886.65,128.54 896.23,124.04 C 905.80,119.54 915.88,114.14 923.78,107.87 C 931.68,101.61 939.26,94.04 943.63,86.46 C 948.00,78.89 951.03,69.06 950.00,62.42 C 948.97,55.78 943.45,49.15 937.43,46.64 C 931.42,44.12 922.45,45.15 913.92,47.34 C 905.39,49.53 895.66,55.24 886.23,59.77 C 876.79,64.29 866.85,70.36 857.32,74.49 C 847.79,78.63 838.46,82.23 829.04,84.56 C 819.61,86.89 810.31,88.20 800.77,88.46 C 791.24,88.71 781.58,87.55 771.84,86.11 C 762.10,84.67 752.23,82.10 742.35,79.81 C 732.47,77.52 722.50,74.57 712.58,72.36 C 702.65,70.15 692.68,67.75 682.79,66.54 C 672.91,65.34 663.05,64.73 653.27,65.12 C 643.50,65.52 633.80,67.12 624.15,68.91 C 614.49,70.71 604.90,73.41 595.33,75.88 C 585.76,78.36 576.24,81.36 566.73,83.76 C 557.21,86.17 547.73,88.72 538.24,90.29 C 528.75,91.86 519.27,93.10 509.77,93.19 C 500.26,93.28 490.75,92.26 481.22,90.82 C 471.68,89.39 462.13,86.92 452.56,84.58 C 442.98,82.23 433.39,79.25 423.76,76.77 C 414.14,74.29 404.50,71.56 394.82,69.72 C 385.14,67.88 375.44,66.28 365.70,65.75 C 355.96,65.22 346.18,65.68 336.39,66.54 C 326.60,67.41 316.78,69.20 306.95,70.92 C 297.12,72.64 287.26,74.96 277.41,76.86 C 267.56,78.77 257.70,80.94 247.85,82.35 C 238.00,83.77 228.13,85.07 218.32,85.38 C 208.51,85.68 198.66,85.47 188.99,84.19 C 179.32,82.91 169.69,80.84 160.31,77.69 C 150.93,74.54 141.76,69.61 132.71,65.30 C 123.65,60.99 114.85,55.10 105.97,51.83 C 97.10,48.55 87.82,45.51 79.47,45.64 Z" fill="#7B2D2F"/></svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

BIN
View File
Binary file not shown.
-191
View File
@@ -1,191 +0,0 @@
import { useEffect, useRef, useState } from 'react'
import type { PDFDocumentProxy, RenderTask } from 'pdfjs-dist'
type Props = {
src: string
}
// pdfjs-dist touches the DOM/Canvas at import time, so it's loaded lazily inside
// the effect below instead of at module scope — this file also runs during SSR.
export function PdfMenu({ src }: Props) {
const containerRef = useRef<HTMLDivElement>(null)
const canvasRef = useRef<HTMLCanvasElement>(null)
const pdfRef = useRef<PDFDocumentProxy | null>(null)
const directionRef = useRef(1)
const hasRenderedOnceRef = useRef(false)
const [error, setError] = useState(false)
const [numPages, setNumPages] = useState(0)
const [pageNumber, setPageNumber] = useState(1)
const [isBusy, setIsBusy] = useState(false)
function goToPage(next: number) {
directionRef.current = next > pageNumber ? 1 : -1
setPageNumber(next)
}
useEffect(() => {
let cancelled = false
setError(false)
setNumPages(0)
setPageNumber(1)
pdfRef.current = null
hasRenderedOnceRef.current = false
async function load() {
const pdfjsLib = await import('pdfjs-dist')
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url,
).href
try {
const pdf = await pdfjsLib.getDocument({ url: src }).promise
if (cancelled) return
pdfRef.current = pdf
setNumPages(pdf.numPages)
} catch (err) {
console.error('PdfMenu failed to load', err)
if (!cancelled) setError(true)
}
}
load()
return () => {
cancelled = true
}
}, [src])
useEffect(() => {
const pdf = pdfRef.current
const canvas = canvasRef.current
const container = containerRef.current
if (!pdf || !canvas || !container || numPages === 0) return
let cancelled = false
let renderTask: RenderTask | undefined
let exitTimeout: ReturnType<typeof setTimeout> | undefined
function wait(ms: number) {
return new Promise<void>((resolve) => {
exitTimeout = setTimeout(resolve, ms)
})
}
async function render(pdf: PDFDocumentProxy, canvas: HTMLCanvasElement, container: HTMLDivElement) {
const direction = directionRef.current
setIsBusy(true)
if (hasRenderedOnceRef.current) {
canvas.style.transition = 'opacity 0.18s ease, transform 0.18s ease'
canvas.style.opacity = '0'
canvas.style.transform = `translateX(${direction * -2}rem)`
await wait(180)
if (cancelled) return
}
try {
const page = await pdf.getPage(pageNumber)
if (cancelled) return
const unscaledViewport = page.getViewport({ scale: 1 })
const scale = container.clientWidth / unscaledViewport.width
const viewport = page.getViewport({ scale })
const pixelRatio = window.devicePixelRatio || 1
canvas.width = viewport.width * pixelRatio
canvas.height = viewport.height * pixelRatio
canvas.style.width = `${viewport.width}px`
canvas.style.height = `${viewport.height}px`
const context = canvas.getContext('2d')
if (!context) return
context.scale(pixelRatio, pixelRatio)
renderTask = page.render({ canvasContext: context, viewport, canvas })
await renderTask.promise
if (cancelled) return
// Jump to the entry offset with transitions off, then force the browser to
// commit that position before re-enabling them — otherwise the two style
// writes land in the same frame and the entry animation never paints.
canvas.style.transition = 'none'
canvas.style.transform = `translateX(${direction * 2}rem)`
canvas.offsetHeight
canvas.style.transition = 'opacity 0.22s ease, transform 0.22s ease'
canvas.style.opacity = '1'
canvas.style.transform = 'translateX(0)'
hasRenderedOnceRef.current = true
} catch (err) {
if (!cancelled) {
console.error('PdfMenu failed to render page', err)
setError(true)
}
} finally {
if (!cancelled) setIsBusy(false)
}
}
render(pdf, canvas, container)
return () => {
cancelled = true
clearTimeout(exitTimeout)
renderTask?.cancel()
}
}, [numPages, pageNumber])
if (error) {
return (
<p className="field-hint">
Die Speisekarte konnte nicht geladen werden.{' '}
<a href={src} target="_blank" rel="noopener noreferrer">
Als PDF öffnen
</a>
.
</p>
)
}
return (
<div className="pdf-menu" ref={containerRef}>
<div className="pdf-menu-viewport">
<canvas className="pdf-menu-page" ref={canvasRef} />
{numPages === 0 && (
<div className="pdf-menu-loading" role="status">
<div className="pdf-menu-spinner" aria-hidden="true" />
<p>Die Speisekarte wird aufgetischt …</p>
</div>
)}
</div>
{numPages > 1 && (
<div className="pdf-menu-pagination">
<button
type="button"
className="btn btn-secondary"
onClick={() => goToPage(pageNumber - 1)}
disabled={isBusy || pageNumber <= 1}
>
Zurück
</button>
<span>
Seite {pageNumber} von {numPages}
</span>
<button
type="button"
className="btn btn-secondary"
onClick={() => goToPage(pageNumber + 1)}
disabled={isBusy || pageNumber >= numPages}
>
Weiter
</button>
</div>
)}
<p className="field-hint">
Speisekarte nicht geladen?{' '}
<a href={src} target="_blank" rel="noopener noreferrer">
Klick hier
</a>
</p>
</div>
)
}
-5
View File
@@ -26,11 +26,6 @@ export const Route = createRootRoute({
rel: 'stylesheet', rel: 'stylesheet',
href: appCss, href: appCss,
}, },
{
rel: 'icon',
href: '/favicon.svg',
type: 'image/svg+xml',
},
], ],
}), }),
shellComponent: RootDocument, shellComponent: RootDocument,
+14 -12
View File
@@ -1,7 +1,6 @@
import { useState } from 'react' import { useState } from 'react'
import { createFileRoute, useRouter } from '@tanstack/react-router' import { createFileRoute, useRouter } from '@tanstack/react-router'
import { PdfMenu } from '#/components/pdf-menu'
import { ReservationConfirmedModal } from '#/components/reservation-confirmed-modal' import { ReservationConfirmedModal } from '#/components/reservation-confirmed-modal'
import { ReservationDateTimeField } from '#/components/reservation-datetime-field' import { ReservationDateTimeField } from '#/components/reservation-datetime-field'
import { SiteFooter } from '#/components/site-footer' import { SiteFooter } from '#/components/site-footer'
@@ -49,6 +48,19 @@ type ConfirmedReservation = {
partySize: number partySize: number
} }
function FlameIcon() {
return (
<svg className="hero-flame" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M12 2c1 3-3 4-3 7.5A3.5 3.5 0 0 0 12 13a3.5 3.5 0 0 0 3-5.3c1.5 1 2.5 3 2.5 5a5.5 5.5 0 1 1-11 0C6.5 8.5 9.5 6 12 2Z"
stroke="currentColor"
strokeWidth="1.4"
strokeLinejoin="round"
/>
</svg>
)
}
function Home() { function Home() {
const router = useRouter() const router = useRouter()
const { tables, settings, availability } = Route.useLoaderData() const { tables, settings, availability } = Route.useLoaderData()
@@ -175,7 +187,7 @@ function Home() {
<SiteHeader /> <SiteHeader />
<section className="hero" id="top"> <section className="hero" id="top">
<img className="hero-flame" src="/logo.svg" alt="" aria-hidden="true" /> <FlameIcon />
<h1> <h1>
Vom Feuer verwöhnt, <span>mit Liebe serviert</span> Vom Feuer verwöhnt, <span>mit Liebe serviert</span>
</h1> </h1>
@@ -215,16 +227,6 @@ function Home() {
</div> </div>
</section> </section>
<section className="section" id="speisekarte">
<div className="section-grid">
<div>
<span className="section-kicker">Was uns schmeckt</span>
<h2 className="section-heading">Speisekarte</h2>
</div>
<PdfMenu src="/menu.pdf" />
</div>
</section>
<section className="section" id="reservieren"> <section className="section" id="reservieren">
<div className="section-grid"> <div className="section-grid">
<div> <div>
+4 -81
View File
@@ -142,10 +142,10 @@ a {
} }
.hero-flame { .hero-flame {
display: block; width: 4rem;
width: 7rem; height: 4rem;
height: 3.5rem;
margin: 0 auto 1.5rem; margin: 0 auto 1.5rem;
color: var(--ember);
} }
.hero h1 { .hero h1 {
@@ -1508,83 +1508,6 @@ a {
pointer-events: none; pointer-events: none;
} }
.pdf-menu {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.pdf-menu-viewport {
position: relative;
width: 100%;
overflow: hidden;
border-radius: 0.4rem;
border: 1px solid var(--line);
background: #fff;
box-shadow: 0 0.5rem 1.5rem rgb(0 0 0 / 25%);
}
.pdf-menu-loading {
position: absolute;
top: 50%;
left: 50%;
width: 90%;
max-width: 20rem;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
opacity: 0;
animation: pdf-menu-spinner-fade-in 0.15s ease 0.15s forwards;
}
.pdf-menu-loading p {
margin: 0;
text-align: center;
font-family: 'Fraunces', Georgia, serif;
font-style: italic;
color: #6b5f4d;
}
.pdf-menu-spinner {
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
border: 3px solid var(--line);
border-top-color: var(--gold);
animation: pdf-menu-spin 0.8s linear infinite;
}
@keyframes pdf-menu-spin {
to {
transform: rotate(360deg);
}
}
@keyframes pdf-menu-spinner-fade-in {
to {
opacity: 1;
}
}
.pdf-menu-page {
display: block;
width: 100%;
max-width: 100%;
aspect-ratio: 210 / 297;
opacity: 0;
}
.pdf-menu-pagination {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
color: var(--text-muted);
}
/* A classic map-pin: a square with three rounded corners (50% 50% 50% 0 leaves /* A classic map-pin: a square with three rounded corners (50% 50% 50% 0 leaves
the bottom-left one sharp), rotated -45deg around that exact sharp corner so the bottom-left one sharp), rotated -45deg around that exact sharp corner so
the corner itself — not the box center — lands on the `left`/`top` coordinate the corner itself — not the box center — lands on the `left`/`top` coordinate
@@ -1967,7 +1890,7 @@ a {
/* Legal page */ /* Legal page */
.legal-page { .legal-page {
padding: 3.5rem 1rem 4rem; padding: 3.5rem 0 4rem;
} }
.legal-page h1 { .legal-page h1 {
-3
View File
@@ -7,9 +7,6 @@ import viteReact from '@vitejs/plugin-react'
const config = defineConfig({ const config = defineConfig({
resolve: { tsconfigPaths: true }, resolve: { tsconfigPaths: true },
plugins: [tanstackStart(), viteReact()], plugins: [tanstackStart(), viteReact()],
preview: {
allowedHosts: ['localhost', '127.0.0.1', 'promo-steakhouse.lukas-harat.com'],
}
}) })
export default config export default config