✨ Update design
This commit is contained in:
@@ -0,0 +1,150 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
Guidance for Claude Code when working in this repository.
|
||||||
|
|
||||||
|
## Project overview
|
||||||
|
|
||||||
|
[1-2 sentences: what this app does, who it's for]
|
||||||
|
|
||||||
|
**Stack:**
|
||||||
|
- TanStack Start (React, file-based routing via TanStack Router)
|
||||||
|
- TypeScript (strict mode)
|
||||||
|
- Vite
|
||||||
|
- SCSS Modules
|
||||||
|
- Drizzle ORM + PostgreSQL
|
||||||
|
- [Auth: Better Auth / Clerk / custom / none yet]
|
||||||
|
- [Package manager: pnpm / npm / bun]
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev # start dev server (http://localhost:3000)
|
||||||
|
pnpm build # production build
|
||||||
|
pnpm typecheck # tsc --noEmit
|
||||||
|
pnpm lint # eslint
|
||||||
|
pnpm test # [test runner, e.g. vitest]
|
||||||
|
```
|
||||||
|
|
||||||
|
Always run `typecheck` and `lint` before considering a task done. If `build`
|
||||||
|
fails with TypeScript errors, run `typecheck` first to isolate them.
|
||||||
|
|
||||||
|
## Project structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── routes/ # file-based routes (TanStack Router)
|
||||||
|
│ ├── __root.tsx # root layout, wraps every page
|
||||||
|
│ └── index.tsx # home page
|
||||||
|
├── routeTree.gen.ts # AUTO-GENERATED — never edit by hand
|
||||||
|
├── router.tsx # router instance/config
|
||||||
|
├── components/
|
||||||
|
│ ├── ui/ # base primitives (buttons, inputs, etc.)
|
||||||
|
│ └── <domain>/ # feature-specific components, e.g. posts/, auth/
|
||||||
|
├── lib/ # shared utilities, API clients, helpers
|
||||||
|
├── db/
|
||||||
|
│ ├── schema.ts # Drizzle schema definitions
|
||||||
|
│ ├── index.ts # Drizzle client instance
|
||||||
|
│ └── migrations/ # generated SQL migrations — never hand-edit
|
||||||
|
├── styles/
|
||||||
|
│ ├── globals.scss # global styles, resets
|
||||||
|
│ └── _variables.scss # shared SCSS variables/mixins (partial)
|
||||||
|
public/ # static assets
|
||||||
|
drizzle.config.ts # Drizzle Kit config (schema path, out dir, dialect)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Routing conventions
|
||||||
|
- Routes live under `src/routes/` and map to URLs by file name/path.
|
||||||
|
- `__root.tsx` is the root layout every route renders inside.
|
||||||
|
- `routeTree.gen.ts` is generated by TanStack Router — **never edit it
|
||||||
|
directly**; it regenerates on `dev`/`build` from the files in `routes/`.
|
||||||
|
- Use route `loader`s for data fetching tied to a route, not `useEffect`.
|
||||||
|
- Keep server-only code (secrets, DB access) out of files that ship to the
|
||||||
|
client; be careful with `verbatimModuleSyntax` so server code doesn't leak
|
||||||
|
into client bundles.
|
||||||
|
|
||||||
|
## Styling (SCSS Modules)
|
||||||
|
|
||||||
|
- Every component that needs styles gets a co-located `Component.module.scss`
|
||||||
|
next to `Component.tsx` — not a shared stylesheet.
|
||||||
|
- Import as `import styles from './Component.module.scss'` and apply classes
|
||||||
|
via `className={styles.foo}`; never reference a module class by string
|
||||||
|
outside that import.
|
||||||
|
- Class names in `.module.scss` files are camelCase (e.g. `.cardHeader`, not
|
||||||
|
`.card-header`) so they read naturally as `styles.cardHeader`.
|
||||||
|
- Shared tokens (colors, spacing, breakpoints) live in `src/styles/_variables.scss`
|
||||||
|
and are pulled in with `@use '~/styles/variables' as *;` — don't hardcode
|
||||||
|
colors/spacing that already exist as a variable.
|
||||||
|
- `src/styles/globals.scss` is for resets and truly global rules only
|
||||||
|
(imported once in `__root.tsx`); component-specific styles always go in a
|
||||||
|
module, never in globals.
|
||||||
|
- Avoid deep nesting (max ~3 levels) and avoid `!important`.
|
||||||
|
|
||||||
|
## Code style
|
||||||
|
|
||||||
|
- TypeScript strict mode; avoid `any`. Prefer inferred types everywhere
|
||||||
|
possible — let TypeScript infer return types and variable types rather
|
||||||
|
than annotating them. Only add explicit types where inference can't reach:
|
||||||
|
function parameters (always type these), exported values whose inferred
|
||||||
|
type would be unclear to a caller, or when inference produces something
|
||||||
|
wider than intended (e.g. use `as const` or a literal type instead of
|
||||||
|
annotating).
|
||||||
|
- Functional React components only, no class components.
|
||||||
|
- Use arrow functions everywhere (components, handlers, utilities) instead of
|
||||||
|
the `function` keyword — e.g. `const getUser = async (id: string) => {...}`
|
||||||
|
and `export const Button = (props: ButtonProps) => {...}`.
|
||||||
|
- [Naming conventions: PascalCase components, camelCase functions/vars, etc.]
|
||||||
|
- [Import order / path aliases, e.g. `~/` or `@/` for `src/`]
|
||||||
|
- Co-locate a component's test file next to it unless it's a shared
|
||||||
|
primitive in `components/ui/` (see "Styling" for `.module.scss` placement).
|
||||||
|
- No comments unless they're unavoidable for understanding — e.g. explaining
|
||||||
|
*why* a non-obvious workaround exists, documenting a constraint imposed by
|
||||||
|
an external system, or flagging a case that genuinely can't be made
|
||||||
|
self-evident through naming/structure. Never comment what the code already
|
||||||
|
says; prefer clearer names and smaller functions over a comment explaining
|
||||||
|
an unclear one.
|
||||||
|
|
||||||
|
## Data layer (Drizzle + Postgres)
|
||||||
|
|
||||||
|
- Schema lives in `src/db/schema.ts`; the Drizzle client is created once in
|
||||||
|
`src/db/index.ts` and imported from there — don't instantiate new clients
|
||||||
|
elsewhere.
|
||||||
|
- Only import `db/*` from server-only code (route `loader`s/`action`s,
|
||||||
|
server functions) — never from client components, to avoid leaking DB
|
||||||
|
credentials into the client bundle.
|
||||||
|
- Query with Drizzle's query builder (`db.select()...`, `db.query.table...`)
|
||||||
|
rather than raw SQL, unless a query genuinely needs it.
|
||||||
|
- After changing `schema.ts`, generate a migration rather than editing the
|
||||||
|
database by hand:
|
||||||
|
```bash
|
||||||
|
pnpm drizzle-kit generate # create a migration from schema changes
|
||||||
|
pnpm drizzle-kit migrate # apply pending migrations
|
||||||
|
```
|
||||||
|
- Never hand-edit files under `db/migrations/` — they're generated and
|
||||||
|
should be treated as a history, not touched retroactively.
|
||||||
|
- [Connection details: env var name for `DATABASE_URL`, whether using a
|
||||||
|
connection pool like `postgres.js` or `node-postgres`, local dev DB setup
|
||||||
|
e.g. Docker Compose]
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- [Test runner and conventions, e.g. Vitest + Testing Library]
|
||||||
|
- [Where tests live: colocated `*.test.ts(x)` vs a `__tests__` dir]
|
||||||
|
- Run the relevant test file after changes, not just the full suite, unless
|
||||||
|
asked to verify broadly.
|
||||||
|
|
||||||
|
## Things to avoid
|
||||||
|
|
||||||
|
- Don't hand-edit `routeTree.gen.ts` or files under `db/migrations/`.
|
||||||
|
- Don't run destructive Drizzle commands (`drizzle-kit push` against a
|
||||||
|
populated DB, or anything that drops columns/tables) without confirming
|
||||||
|
first — prefer `generate` + `migrate` so changes are reviewable.
|
||||||
|
- Don't introduce a new state-management library without asking — prefer
|
||||||
|
route loaders / TanStack Query cache / local state first.
|
||||||
|
- Don't add new dependencies without checking `package.json` for an
|
||||||
|
existing one that already covers the need.
|
||||||
|
- [Any other project-specific guardrails]
|
||||||
|
|
||||||
|
## Git / PR conventions
|
||||||
|
|
||||||
|
- [Commit message style, branch naming, whether Claude should commit
|
||||||
|
automatically or leave changes staged for review]
|
||||||
+5
-83
@@ -13,7 +13,6 @@ env:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
@@ -69,92 +68,15 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Deploy to TrueNAS
|
- name: Deploy to TrueNAS
|
||||||
env:
|
env:
|
||||||
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
TRUENAS_URL: ${{ secrets.TRUENAS_URL }}
|
||||||
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
|
TRUENAS_API_KEY: ${{ secrets.TRUENAS_API_KEY }}
|
||||||
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 }}
|
IMAGE_TAG: ${{ github.sha }}
|
||||||
|
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
mkdir -p ~/.ssh
|
echo "Deploying image:"
|
||||||
|
echo "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${IMAGE_TAG}"
|
||||||
|
|
||||||
printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/deploy_key
|
echo "TrueNAS deployment configuration required."
|
||||||
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.\"
|
|
||||||
'"
|
|
||||||
+5
-8
@@ -8,9 +8,6 @@ RUN npm ci
|
|||||||
|
|
||||||
COPY . .
|
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 \
|
ENV DATABASE_URL=postgres://build:build@localhost:5432/build \
|
||||||
BETTER_AUTH_SECRET=build-time-placeholder \
|
BETTER_AUTH_SECRET=build-time-placeholder \
|
||||||
BETTER_AUTH_URL=http://localhost:3000
|
BETTER_AUTH_URL=http://localhost:3000
|
||||||
@@ -21,11 +18,11 @@ FROM node:22-alpine AS runner
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV NODE_ENV=production
|
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 ./
|
COPY --from=builder /app ./
|
||||||
|
|
||||||
|
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||||
|
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["npm", "run", "preview", "--", "--host", "0.0.0.0", "--port", "3000"]
|
|
||||||
|
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||||
+20
-10
@@ -1,35 +1,45 @@
|
|||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
image: git.lukas-harat.com/lukas/promo_steakhouse:${IMAGE_TAG:-latest}
|
image: git.lukas-harat.com/lukas/promo_rathaus:${IMAGE_TAG:-latest}
|
||||||
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- '3003:3000'
|
- "3003:3000"
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: postgres://steakhouse:${POSTGRES_PASSWORD}@postgres:5432/steakhouse
|
DATABASE_URL: postgres://promo_rathaus:${POSTGRES_PASSWORD}@postgres:5432/promo_rathaus
|
||||||
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
|
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
|
||||||
BETTER_AUTH_URL: ${BETTER_AUTH_URL}
|
BETTER_AUTH_URL: ${BETTER_AUTH_URL}
|
||||||
STAFF_SEED_EMAIL: ${STAFF_SEED_EMAIL}
|
STAFF_SEED_EMAIL: ${STAFF_SEED_EMAIL}
|
||||||
STAFF_SEED_PASSWORD: ${STAFF_SEED_PASSWORD}
|
STAFF_SEED_PASSWORD: ${STAFF_SEED_PASSWORD}
|
||||||
RESEND_API_KEY: ${RESEND_API_KEY}
|
RESEND_API_KEY: ${RESEND_API_KEY}
|
||||||
RESERVATIONS_FROM_EMAIL: ${RESERVATIONS_FROM_EMAIL}
|
RESERVATIONS_FROM_EMAIL: ${RESERVATIONS_FROM_EMAIL}
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: steakhouse
|
POSTGRES_USER: promo_rathaus
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
POSTGRES_DB: steakhouse
|
POSTGRES_DB: promo_rathaus
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- steakhouse-postgres-data:/var/lib/postgresql/data
|
- /mnt/YOUR_POOL/apps/promo-rathaus/postgres:/var/lib/postgresql/data
|
||||||
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U steakhouse -d steakhouse"]
|
test:
|
||||||
|
[
|
||||||
|
"CMD-SHELL",
|
||||||
|
"pg_isready -U promo_rathaus -d promo_rathaus"
|
||||||
|
]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 10
|
retries: 10
|
||||||
|
start_period: 10s
|
||||||
volumes:
|
|
||||||
steakhouse-postgres-data:
|
|
||||||
+5
-5
@@ -3,13 +3,13 @@ services:
|
|||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: steakhouse
|
POSTGRES_USER: promo_rathaus
|
||||||
POSTGRES_PASSWORD: steakhouse
|
POSTGRES_PASSWORD: promo_rathaus
|
||||||
POSTGRES_DB: steakhouse
|
POSTGRES_DB: promo_rathaus
|
||||||
ports:
|
ports:
|
||||||
- '5432:5432'
|
- '5432:5432'
|
||||||
volumes:
|
volumes:
|
||||||
- steakhouse-postgres-data:/var/lib/postgresql/data
|
- promo_rathaus-postgres-data:/var/lib/postgresql/data
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
steakhouse-postgres-data:
|
promo_rathaus-postgres-data:
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
echo "==> Running database migrations..."
|
||||||
|
npx drizzle-kit migrate
|
||||||
|
|
||||||
|
echo "==> Seeding database..."
|
||||||
|
npm run db:seed
|
||||||
|
|
||||||
|
echo "==> Starting application..."
|
||||||
|
exec npm run preview -- --host 0.0.0.0 --port 3000
|
||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||||
<rect width="64" height="64" rx="14" fill="#17120f"/>
|
<rect width="64" height="64" rx="14" fill="#241417"/>
|
||||||
<rect x="2" y="2" width="60" height="60" rx="12" fill="none" stroke="#c9a15a" stroke-width="2"/>
|
<rect x="2" y="2" width="60" height="60" rx="12" fill="none" stroke="#a3182f" 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>
|
<text x="32" y="44" font-family="Georgia, 'Times New Roman', serif" font-size="34" font-weight="600" text-anchor="middle" fill="#fbf4ef">R</text>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 371 B After Width: | Height: | Size: 371 B |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 228 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 187 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
@@ -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 |
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -54,7 +54,7 @@ export function AppSidebar({ staffUser, sectionLinks }: Props) {
|
|||||||
<nav className="app-sidebar" aria-label="Hauptnavigation" ref={navRef}>
|
<nav className="app-sidebar" aria-label="Hauptnavigation" ref={navRef}>
|
||||||
<div className="app-sidebar-bar">
|
<div className="app-sidebar-bar">
|
||||||
<Link className="wordmark app-sidebar-brand" to="/" onClick={() => setIsOpen(false)}>
|
<Link className="wordmark app-sidebar-brand" to="/" onClick={() => setIsOpen(false)}>
|
||||||
Steakhaus <span>bei Mirko</span>
|
Historisches Rathaus <span>Resto-Bar</span>
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ import type { PDFDocumentProxy, RenderTask } from 'pdfjs-dist'
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
src: string
|
src: string
|
||||||
|
background?: string
|
||||||
|
spinnerTrackColor?: string
|
||||||
|
spinnerAccentColor?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// pdfjs-dist touches the DOM/Canvas at import time, so it's loaded lazily inside
|
// 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.
|
// the effect below instead of at module scope — this file also runs during SSR.
|
||||||
export function PdfMenu({ src }: Props) {
|
export function PdfMenu({ src, background, spinnerTrackColor, spinnerAccentColor }: Props) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||||
const pdfRef = useRef<PDFDocumentProxy | null>(null)
|
const pdfRef = useRef<PDFDocumentProxy | null>(null)
|
||||||
@@ -137,7 +140,7 @@ export function PdfMenu({ src }: Props) {
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<p className="field-hint">
|
<p className="field-hint">
|
||||||
Die Speisekarte konnte nicht geladen werden.{' '}
|
Die Karte konnte nicht geladen werden.{' '}
|
||||||
<a href={src} target="_blank" rel="noopener noreferrer">
|
<a href={src} target="_blank" rel="noopener noreferrer">
|
||||||
Als PDF öffnen
|
Als PDF öffnen
|
||||||
</a>
|
</a>
|
||||||
@@ -148,12 +151,20 @@ export function PdfMenu({ src }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pdf-menu" ref={containerRef}>
|
<div className="pdf-menu" ref={containerRef}>
|
||||||
<div className="pdf-menu-viewport">
|
<div className="pdf-menu-viewport" style={background ? { background } : undefined}>
|
||||||
<canvas className="pdf-menu-page" ref={canvasRef} />
|
<canvas className="pdf-menu-page" ref={canvasRef} />
|
||||||
{numPages === 0 && (
|
{numPages === 0 && (
|
||||||
<div className="pdf-menu-loading" role="status">
|
<div className="pdf-menu-loading" role="status">
|
||||||
<div className="pdf-menu-spinner" aria-hidden="true" />
|
<div
|
||||||
<p>Die Speisekarte wird aufgetischt …</p>
|
className="pdf-menu-spinner"
|
||||||
|
aria-hidden="true"
|
||||||
|
style={
|
||||||
|
spinnerTrackColor || spinnerAccentColor
|
||||||
|
? { borderColor: spinnerTrackColor, borderTopColor: spinnerAccentColor }
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<p>Die Karte wird aufgetischt …</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -181,7 +192,7 @@ export function PdfMenu({ src }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<p className="field-hint">
|
<p className="field-hint">
|
||||||
Speisekarte nicht geladen?{' '}
|
Karte nicht geladen?{' '}
|
||||||
<a href={src} target="_blank" rel="noopener noreferrer">
|
<a href={src} target="_blank" rel="noopener noreferrer">
|
||||||
Klick hier
|
Klick hier
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -12,14 +12,12 @@ type Props = {
|
|||||||
onClose: () => void
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function FlameBadgeIcon() {
|
function SparkleBadgeIcon() {
|
||||||
return (
|
return (
|
||||||
<svg className="reservation-confirmed-flame" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
<svg className="reservation-confirmed-sparkle" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
<path
|
<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"
|
d="M12 2c0.6 3.2 1.2 5.8 2.7 7.3S18.8 11.4 22 12c-3.2 0.6-5.8 1.2-7.3 2.7S12.6 18.8 12 22c-0.6-3.2-1.2-5.8-2.7-7.3S5.2 12.6 2 12c3.2-0.6 5.8-1.2 7.3-2.7S11.4 5.2 12 2Z"
|
||||||
stroke="currentColor"
|
fill="currentColor"
|
||||||
strokeWidth="1.4"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
@@ -62,13 +60,13 @@ export function ReservationConfirmedModal({
|
|||||||
aria-labelledby="reservation-confirmed-title"
|
aria-labelledby="reservation-confirmed-title"
|
||||||
onClick={(event) => event.stopPropagation()}
|
onClick={(event) => event.stopPropagation()}
|
||||||
>
|
>
|
||||||
<FlameBadgeIcon />
|
<SparkleBadgeIcon />
|
||||||
|
|
||||||
<h2 id="reservation-confirmed-title" className="reservation-confirmed-title">
|
<h2 id="reservation-confirmed-title" className="reservation-confirmed-title">
|
||||||
Tisch reserviert!
|
Tisch reserviert!
|
||||||
</h2>
|
</h2>
|
||||||
<p className="reservation-confirmed-message">
|
<p className="reservation-confirmed-message">
|
||||||
Bis bald, {customerName}! Wir halten Ihnen den Platz warm und legen schon mal Kohle nach.
|
Bis bald, {customerName}! Wir halten Ihnen den Platz warm und freuen uns auf Ihren Besuch.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<dl className="detail-list">
|
<dl className="detail-list">
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ export function SiteFooter() {
|
|||||||
return (
|
return (
|
||||||
<footer className="site-footer">
|
<footer className="site-footer">
|
||||||
<span className="wordmark">
|
<span className="wordmark">
|
||||||
Steakhaus <span>bei Mirko</span>
|
Historisches Rathaus <span>Resto-Bar</span>
|
||||||
</span>
|
</span>
|
||||||
Meppener Straße 8 · 49716 Meppen · 05931 - 12750
|
Markt 49 · 49716 Meppen · 05931 - 848 0161
|
||||||
<nav className="footer-nav">
|
<nav className="footer-nav">
|
||||||
<Link to="/">Startseite</Link>
|
<Link to="/">Startseite</Link>
|
||||||
<Link to="/impressum">Impressum</Link>
|
<Link to="/impressum">Impressum</Link>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export function SiteHeader() {
|
|||||||
<header className="site-header">
|
<header className="site-header">
|
||||||
<div className="site-header-inner">
|
<div className="site-header-inner">
|
||||||
<Link className="wordmark" to="/">
|
<Link className="wordmark" to="/">
|
||||||
Steakhaus <span>bei Mirko</span>
|
Historisches Rathaus <span>Resto-Bar</span>
|
||||||
</Link>
|
</Link>
|
||||||
<Link className="staff-login-link" to="/staff/login">
|
<Link className="staff-login-link" to="/staff/login">
|
||||||
Mitarbeiter-Login
|
Mitarbeiter-Login
|
||||||
|
|||||||
+14
-14
@@ -8,13 +8,13 @@ const dateTimeFormatter = new Intl.DateTimeFormat('de-DE', {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const COLORS = {
|
const COLORS = {
|
||||||
bg: '#17120f',
|
bg: '#fbf4ef',
|
||||||
bgRaised: '#1e1712',
|
bgRaised: '#ffffff',
|
||||||
line: '#3a2f26',
|
line: '#e6d9cf',
|
||||||
text: '#f3ead9',
|
text: '#241417',
|
||||||
textMuted: '#c9baa3',
|
textMuted: '#6b5f5c',
|
||||||
gold: '#c9a15a',
|
gold: '#6b0f22',
|
||||||
goldBright: '#e0bb78',
|
goldBright: '#a3182f',
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(value: string) {
|
function escapeHtml(value: string) {
|
||||||
@@ -49,7 +49,7 @@ type ReservationEmailInfo = {
|
|||||||
table: Pick<RestaurantTable, 'name'>
|
table: Pick<RestaurantTable, 'name'>
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONTACT_EMAIL = 'info@steakhaus-meppen.de'
|
const CONTACT_EMAIL = 'info@restobar-meppen.de'
|
||||||
|
|
||||||
function getSiteUrl() {
|
function getSiteUrl() {
|
||||||
return process.env.BETTER_AUTH_URL ?? 'http://localhost:3000'
|
return process.env.BETTER_AUTH_URL ?? 'http://localhost:3000'
|
||||||
@@ -96,7 +96,7 @@ function renderReservationEmailHtml(
|
|||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="padding-bottom:28px;">
|
<td align="center" style="padding-bottom:28px;">
|
||||||
<span style="font:600 20px/1.3 Georgia,'Times New Roman',serif;letter-spacing:0.03em;color:${COLORS.text};">
|
<span style="font:600 20px/1.3 Georgia,'Times New Roman',serif;letter-spacing:0.03em;color:${COLORS.text};">
|
||||||
Steakhaus <span style="color:${COLORS.goldBright};">bei Mirko</span>
|
Historisches Rathaus <span style="color:${COLORS.goldBright};">Resto-Bar</span>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -173,7 +173,7 @@ function renderReservationEmailHtml(
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="padding-top:16px;color:${COLORS.textMuted};font-size:12px;">
|
<td align="center" style="padding-top:16px;color:${COLORS.textMuted};font-size:12px;">
|
||||||
Meppener Straße 8 · 49716 Meppen · 05931 - 12750
|
Markt 49 · 49716 Meppen · 05931 - 848 0161
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -193,7 +193,7 @@ function renderReservationEmailText(
|
|||||||
const formattedTime = dateTimeFormatter.format(reservation.reservationTime)
|
const formattedTime = dateTimeFormatter.format(reservation.reservationTime)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'Steakhaus bei Mirko',
|
'Historisches Rathaus Resto-Bar',
|
||||||
'',
|
'',
|
||||||
heading,
|
heading,
|
||||||
'',
|
'',
|
||||||
@@ -214,8 +214,8 @@ function renderReservationEmailText(
|
|||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
'Wir freuen uns auf Sie!',
|
'Wir freuen uns auf Sie!',
|
||||||
'Steakhaus bei Mirko',
|
'Historisches Rathaus Resto-Bar',
|
||||||
'Meppener Straße 8 · 49716 Meppen · 05931 - 12750',
|
'Markt 49 · 49716 Meppen · 05931 - 848 0161',
|
||||||
].join('\n')
|
].join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,7 +232,7 @@ async function sendReservationEmail(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const fromAddress = process.env.RESERVATIONS_FROM_EMAIL ?? 'Steakhaus bei Mirko <reservierungen@steakhaus-meppen.de>'
|
const fromAddress = process.env.RESERVATIONS_FROM_EMAIL ?? 'Historisches Rathaus Resto-Bar <reservierungen@restobar-meppen.de>'
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.emails.send({
|
await client.emails.send({
|
||||||
|
|||||||
@@ -7,16 +7,13 @@ export type OpeningRange = { start: string; end: string }
|
|||||||
|
|
||||||
// Keyed by the JS `Date#getDay()` convention: 0 = Sunday … 6 = Saturday.
|
// Keyed by the JS `Date#getDay()` convention: 0 = Sunday … 6 = Saturday.
|
||||||
const OPENING_HOURS_BY_WEEKDAY: Record<number, OpeningRange[]> = {
|
const OPENING_HOURS_BY_WEEKDAY: Record<number, OpeningRange[]> = {
|
||||||
0: [
|
0: [{ start: '11:00', end: '20:00' }],
|
||||||
{ start: '11:30', end: '13:30' },
|
|
||||||
{ start: '17:00', end: '22:00' },
|
|
||||||
],
|
|
||||||
1: [],
|
1: [],
|
||||||
2: [{ start: '17:00', end: '21:30' }],
|
2: [{ start: '11:00', end: '20:00' }],
|
||||||
3: [{ start: '17:00', end: '21:30' }],
|
3: [{ start: '11:00', end: '20:00' }],
|
||||||
4: [{ start: '17:00', end: '21:30' }],
|
4: [{ start: '11:00', end: '20:00' }],
|
||||||
5: [{ start: '17:00', end: '22:00' }],
|
5: [{ start: '11:00', end: '20:00' }],
|
||||||
6: [{ start: '17:00', end: '22:00' }],
|
6: [{ start: '11:00', end: '20:00' }],
|
||||||
}
|
}
|
||||||
|
|
||||||
const WEEKDAY_LABELS_MONDAY_FIRST: Array<{ day: string; weekday: number }> = [
|
const WEEKDAY_LABELS_MONDAY_FIRST: Array<{ day: string; weekday: number }> = [
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ const createReservationSchema = z.object({
|
|||||||
.max(500, 'Message must be at most 500 characters')
|
.max(500, 'Message must be at most 500 characters')
|
||||||
.optional()
|
.optional()
|
||||||
.transform((value) => value || null),
|
.transform((value) => value || null),
|
||||||
partySize: z.number().int().min(1, 'At least 1 guest').max(20),
|
partySize: z.number().int().min(1, 'At least 1 guest').max(30),
|
||||||
reservationTime: z.string().refine((value) => !Number.isNaN(Date.parse(value)), {
|
reservationTime: z.string().refine((value) => !Number.isNaN(Date.parse(value)), {
|
||||||
message: 'Enter a valid date and time',
|
message: 'Enter a valid date and time',
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ export const Route = createRootRoute({
|
|||||||
content: 'width=device-width, initial-scale=1',
|
content: 'width=device-width, initial-scale=1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Steakhaus bei Mirko | Meppen',
|
title: 'Historisches Rathaus Resto-Bar | Meppen',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'description',
|
name: 'description',
|
||||||
content:
|
content:
|
||||||
'Steakhaus bei Mirko in Meppen – Steaks vom Grill, gemütliche Atmosphäre. Reservieren Sie Ihren Tisch online.',
|
'Historisches Rathaus Resto-Bar in Meppen – Deine Gastronomie im Herzen der Stadt. Reservieren Sie Ihren Tisch online.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
links: [
|
links: [
|
||||||
|
|||||||
@@ -17,18 +17,20 @@ function Impressum() {
|
|||||||
|
|
||||||
<h2>Angaben gemäß § 5 TMG</h2>
|
<h2>Angaben gemäß § 5 TMG</h2>
|
||||||
<p>
|
<p>
|
||||||
Drenica & Mousa GbR
|
Ilie Cosmin Ivan
|
||||||
<br />
|
<br />
|
||||||
Meppener Str. 8
|
Historisches Rathaus Resto-Bar
|
||||||
|
<br />
|
||||||
|
Markt 49
|
||||||
<br />
|
<br />
|
||||||
49716 Meppen
|
49716 Meppen
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Kontakt</h2>
|
<h2>Kontakt</h2>
|
||||||
<p>
|
<p>
|
||||||
Telefon: <a href="tel:+49593112750">+49 (5931) 12750</a>
|
Telefon: <a href="tel:+4959318480161">+49 5931 84 80161</a>
|
||||||
<br />
|
<br />
|
||||||
E-Mail: <a href="mailto:info@steakhaus-meppen.de">info@steakhaus-meppen.de</a>
|
E-Mail: <a href="mailto:info@restobar-meppen.de">info@restobar-meppen.de</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Haftungsausschluss</h2>
|
<h2>Haftungsausschluss</h2>
|
||||||
@@ -59,8 +61,8 @@ function Impressum() {
|
|||||||
|
|
||||||
<h2 id="datenschutz">Datenschutz</h2>
|
<h2 id="datenschutz">Datenschutz</h2>
|
||||||
<p>
|
<p>
|
||||||
Verantwortlich für die Verarbeitung personenbezogener Daten auf dieser Website ist die
|
Verantwortlich für die Verarbeitung personenbezogener Daten auf dieser Website ist der
|
||||||
oben genannte Drenica & Mousa GbR.
|
oben genannte Ilie Cosmin Ivan.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Wenn Sie über das Reservierungsformular einen Tisch buchen, speichern wir die von Ihnen
|
Wenn Sie über das Reservierungsformular einen Tisch buchen, speichern wir die von Ihnen
|
||||||
|
|||||||
+33
-10
@@ -108,7 +108,7 @@ function Home() {
|
|||||||
EMAIL_PATTERN.test(customerEmail) &&
|
EMAIL_PATTERN.test(customerEmail) &&
|
||||||
PHONE_PATTERN.test(customerPhone) &&
|
PHONE_PATTERN.test(customerPhone) &&
|
||||||
partySize >= 1 &&
|
partySize >= 1 &&
|
||||||
partySize <= 20 &&
|
partySize <= 30 &&
|
||||||
Boolean(reservationTime.split('T')[1]) &&
|
Boolean(reservationTime.split('T')[1]) &&
|
||||||
acceptsTerms
|
acceptsTerms
|
||||||
|
|
||||||
@@ -175,13 +175,14 @@ 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" />
|
<img className="hero-logo" src="/hero.jpg" alt="Historisches Rathaus Resto-Bar" />
|
||||||
<h1>
|
<h1>
|
||||||
Vom Feuer verwöhnt, <span>mit Liebe serviert</span>
|
Deine Gastronomie <span>im Herzen der Stadt</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p>
|
<p>
|
||||||
Dicke Steaks, knisterndes Holzkohlefeuer und Gelächter am Nachbartisch – so schmeckt ein
|
Mitten auf dem Markt, im historischen Rathaus von Meppen – gute Küche, entspannte
|
||||||
guter Abend mitten in Meppen. Reservieren Sie Ihren Tisch, wir legen schon auf.
|
Atmosphäre und Gelächter am Nachbartisch. Reservieren Sie Ihren Tisch, wir freuen uns
|
||||||
|
auf Sie.
|
||||||
</p>
|
</p>
|
||||||
<div className="hero-actions">
|
<div className="hero-actions">
|
||||||
<a className="btn btn-primary" href="#reservieren">
|
<a className="btn btn-primary" href="#reservieren">
|
||||||
@@ -200,6 +201,7 @@ function Home() {
|
|||||||
<span className="section-kicker">Wann wir für Sie da sind</span>
|
<span className="section-kicker">Wann wir für Sie da sind</span>
|
||||||
<h2 className="section-heading">Öffnungszeiten</h2>
|
<h2 className="section-heading">Öffnungszeiten</h2>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
<ul className="hours-list">
|
<ul className="hours-list">
|
||||||
{OPENING_HOURS.map(({ day, hours }) => (
|
{OPENING_HOURS.map(({ day, hours }) => (
|
||||||
<li key={day} className={day === todayName ? 'today' : undefined}>
|
<li key={day} className={day === todayName ? 'today' : undefined}>
|
||||||
@@ -212,6 +214,8 @@ function Home() {
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
<p className="field-hint hours-note">Unsere Küche ist von 12:00 – 20:00 Uhr für Sie geöffnet.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -221,7 +225,26 @@ function Home() {
|
|||||||
<span className="section-kicker">Was uns schmeckt</span>
|
<span className="section-kicker">Was uns schmeckt</span>
|
||||||
<h2 className="section-heading">Speisekarte</h2>
|
<h2 className="section-heading">Speisekarte</h2>
|
||||||
</div>
|
</div>
|
||||||
<PdfMenu src="/menu.pdf" />
|
<div className="menu-stack">
|
||||||
|
<div>
|
||||||
|
<h3 className="menu-stack-heading">Speisen</h3>
|
||||||
|
<PdfMenu
|
||||||
|
src="/speisekarte.pdf"
|
||||||
|
background="#fef8de"
|
||||||
|
spinnerTrackColor="rgba(97, 16, 30, 0.2)"
|
||||||
|
spinnerAccentColor="#61101e"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="menu-stack-heading">Getränke</h3>
|
||||||
|
<PdfMenu
|
||||||
|
src="/getraenkekarte.pdf"
|
||||||
|
background="#b19e76"
|
||||||
|
spinnerTrackColor="rgba(87, 23, 23, 0.3)"
|
||||||
|
spinnerAccentColor="#571717"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -319,7 +342,7 @@ function Home() {
|
|||||||
type="number"
|
type="number"
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
min={1}
|
min={1}
|
||||||
max={20}
|
max={30}
|
||||||
value={partySize}
|
value={partySize}
|
||||||
onChange={(e) => setPartySize(Number(e.target.value))}
|
onChange={(e) => setPartySize(Number(e.target.value))}
|
||||||
required
|
required
|
||||||
@@ -391,18 +414,18 @@ function Home() {
|
|||||||
<li>
|
<li>
|
||||||
<span className="label">Adresse</span>
|
<span className="label">Adresse</span>
|
||||||
<span>
|
<span>
|
||||||
Meppener Straße 8
|
Markt 49
|
||||||
<br />
|
<br />
|
||||||
49716 Meppen
|
49716 Meppen
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span className="label">Telefon</span>
|
<span className="label">Telefon</span>
|
||||||
<a href="tel:+49593112750">05931 - 12750</a>
|
<a href="tel:+4959318480161">05931 - 848 0161</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span className="label">E-Mail</span>
|
<span className="label">E-Mail</span>
|
||||||
<a href="mailto:info@steakhaus-meppen.de">info@steakhaus-meppen.de</a>
|
<a href="mailto:info@restobar-meppen.de">info@restobar-meppen.de</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ function StaffDashboard() {
|
|||||||
<div className="staff-nav">
|
<div className="staff-nav">
|
||||||
<header className="staff-topbar">
|
<header className="staff-topbar">
|
||||||
<Link className="wordmark" to="/">
|
<Link className="wordmark" to="/">
|
||||||
Steakhaus <span>bei Mirko</span>
|
Historisches Rathaus <span>Resto-Bar</span>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="staff-topbar-right">
|
<div className="staff-topbar-right">
|
||||||
<span>Angemeldet als {staffUser.name}</span>
|
<span>Angemeldet als {staffUser.name}</span>
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ function StaffSettings() {
|
|||||||
<div className="staff-nav">
|
<div className="staff-nav">
|
||||||
<header className="staff-topbar">
|
<header className="staff-topbar">
|
||||||
<Link className="wordmark" to="/">
|
<Link className="wordmark" to="/">
|
||||||
Steakhaus <span>bei Mirko</span>
|
Historisches Rathaus <span>Resto-Bar</span>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="staff-topbar-right">
|
<div className="staff-topbar-right">
|
||||||
<span>Angemeldet als {staffUser.name}</span>
|
<span>Angemeldet als {staffUser.name}</span>
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ function StaffTables() {
|
|||||||
<div className="staff-nav">
|
<div className="staff-nav">
|
||||||
<header className="staff-topbar">
|
<header className="staff-topbar">
|
||||||
<Link className="wordmark" to="/">
|
<Link className="wordmark" to="/">
|
||||||
Steakhaus <span>bei Mirko</span>
|
Historisches Rathaus <span>Resto-Bar</span>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="staff-topbar-right">
|
<div className="staff-topbar-right">
|
||||||
<span>Angemeldet als {staffUser.name}</span>
|
<span>Angemeldet als {staffUser.name}</span>
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ function StaffTeam() {
|
|||||||
<div className="staff-nav">
|
<div className="staff-nav">
|
||||||
<header className="staff-topbar">
|
<header className="staff-topbar">
|
||||||
<Link className="wordmark" to="/">
|
<Link className="wordmark" to="/">
|
||||||
Steakhaus <span>bei Mirko</span>
|
Historisches Rathaus <span>Resto-Bar</span>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="staff-topbar-right">
|
<div className="staff-topbar-right">
|
||||||
<span>Angemeldet als {staffUser.name}</span>
|
<span>Angemeldet als {staffUser.name}</span>
|
||||||
|
|||||||
+81
-53
@@ -1,14 +1,14 @@
|
|||||||
@import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600;9..144,700&family=Inter:wght@400;500;600&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600;9..144,700&family=Inter:wght@400;500;600&display=swap');
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg: #17120f;
|
--bg: #fbf4ef;
|
||||||
--bg-raised: #1e1712;
|
--bg-raised: #ffffff;
|
||||||
--line: #3a2f26;
|
--line: #e6d9cf;
|
||||||
--text: #f3ead9;
|
--text: #241417;
|
||||||
--text-muted: #c9baa3;
|
--text-muted: #6b5f5c;
|
||||||
--gold: #c9a15a;
|
--gold: #6b0f22;
|
||||||
--gold-bright: #e0bb78;
|
--gold-bright: #a3182f;
|
||||||
--ember: #a8402c;
|
--ember: #cf2e2e;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -131,9 +131,9 @@ a {
|
|||||||
padding: 4.5rem 1.5rem 5rem;
|
padding: 4.5rem 1.5rem 5rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background:
|
background:
|
||||||
radial-gradient(ellipse at top, rgba(200, 90, 50, 0.2), transparent 60%),
|
radial-gradient(ellipse at top, rgba(107, 15, 34, 0.25), transparent 60%),
|
||||||
linear-gradient(rgba(23, 18, 15, 0.55), rgba(23, 18, 15, 0.93)),
|
linear-gradient(rgba(23, 18, 15, 0.55), rgba(23, 18, 15, 0.93)),
|
||||||
url('/hero-interior.jpg');
|
url('/hero-banner.jpeg');
|
||||||
background-size: auto, auto, cover;
|
background-size: auto, auto, cover;
|
||||||
background-position: center, center, center 55%;
|
background-position: center, center, center 55%;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
@@ -141,29 +141,43 @@ a {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-flame {
|
.hero-logo {
|
||||||
display: block;
|
display: block;
|
||||||
width: 7rem;
|
width: 9rem;
|
||||||
height: 3.5rem;
|
height: 9rem;
|
||||||
|
object-fit: contain;
|
||||||
margin: 0 auto 1.5rem;
|
margin: 0 auto 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero h1 {
|
.hero h1 {
|
||||||
font-size: clamp(2.25rem, 5vw, 3.5rem);
|
font-size: clamp(2.25rem, 5vw, 3.5rem);
|
||||||
color: var(--text);
|
color: #fbf4ef;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero h1 span {
|
.hero h1 span {
|
||||||
color: var(--text);
|
color: #fbf4ef;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero p {
|
.hero p {
|
||||||
max-width: 34rem;
|
max-width: 34rem;
|
||||||
margin: 1rem auto 0;
|
margin: 1rem auto 0;
|
||||||
color: var(--text-muted);
|
color: #d9c9c2;
|
||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The hero sits on a photo with a dark scrim regardless of the site's light
|
||||||
|
theme, so `.btn-secondary` needs its own light-on-dark treatment here —
|
||||||
|
the global version assumes it's sitting on the page's (light) background. */
|
||||||
|
.hero .btn-secondary {
|
||||||
|
border-color: rgba(251, 244, 239, 0.4);
|
||||||
|
color: #fbf4ef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero .btn-secondary:hover {
|
||||||
|
border-color: rgba(251, 244, 239, 0.7);
|
||||||
|
color: #fbf4ef;
|
||||||
|
}
|
||||||
|
|
||||||
.hero-actions {
|
.hero-actions {
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -190,7 +204,7 @@ a {
|
|||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: var(--gold);
|
background: var(--gold);
|
||||||
color: #201709;
|
color: #fbf4ef;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
.btn-primary:hover {
|
||||||
@@ -216,7 +230,7 @@ a {
|
|||||||
|
|
||||||
.btn-ghost:hover {
|
.btn-ghost:hover {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
background: rgba(243, 234, 217, 0.06);
|
background: rgba(36, 20, 23, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sections */
|
/* Sections */
|
||||||
@@ -260,6 +274,17 @@ a {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-stack-heading {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* Opening hours */
|
/* Opening hours */
|
||||||
|
|
||||||
.hours-list {
|
.hours-list {
|
||||||
@@ -600,7 +625,7 @@ a {
|
|||||||
.reservation-form textarea:focus {
|
.reservation-form textarea:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--gold);
|
border-color: var(--gold);
|
||||||
box-shadow: 0 0 0 2px rgba(201, 161, 90, 0.25);
|
box-shadow: 0 0 0 2px rgba(107, 15, 34, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Overrides the generic `.reservation-form input` box styling above — that rule
|
/* Overrides the generic `.reservation-form input` box styling above — that rule
|
||||||
@@ -671,7 +696,7 @@ a {
|
|||||||
border: none;
|
border: none;
|
||||||
border-radius: 0.3rem;
|
border-radius: 0.3rem;
|
||||||
background: var(--gold);
|
background: var(--gold);
|
||||||
color: #201709;
|
color: #fbf4ef;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -699,6 +724,10 @@ a {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hours-note {
|
||||||
|
margin-top: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0.65rem 0.9rem;
|
padding: 0.65rem 0.9rem;
|
||||||
@@ -707,15 +736,15 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message-error {
|
.message-error {
|
||||||
background: #3a1414;
|
background: #fbe4e4;
|
||||||
color: #f3a9a9;
|
color: #8a2020;
|
||||||
border: 1px solid #5c1f1f;
|
border: 1px solid #eeb3b3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-success {
|
.message-success {
|
||||||
background: #17301c;
|
background: #e5f3e8;
|
||||||
color: #a9d9b4;
|
color: #1f6b34;
|
||||||
border: 1px solid #23502c;
|
border: 1px solid #b8ddc2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.staff-page-messages {
|
.staff-page-messages {
|
||||||
@@ -882,7 +911,7 @@ a {
|
|||||||
animation: none;
|
animation: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reservation-confirmed-flame {
|
.reservation-confirmed-sparkle {
|
||||||
animation: none;
|
animation: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1164,7 +1193,7 @@ a {
|
|||||||
.table-filters select:focus {
|
.table-filters select:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--gold);
|
border-color: var(--gold);
|
||||||
box-shadow: 0 0 0 2px rgba(201, 161, 90, 0.25);
|
box-shadow: 0 0 0 2px rgba(107, 15, 34, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-search {
|
.table-search {
|
||||||
@@ -1209,11 +1238,11 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.reservations-table tr.is-editing {
|
.reservations-table tr.is-editing {
|
||||||
background: rgba(243, 234, 217, 0.06);
|
background: rgba(36, 20, 23, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.reservations-table-clickable tbody tr:hover {
|
.reservations-table-clickable tbody tr:hover {
|
||||||
background: rgba(243, 234, 217, 0.05);
|
background: rgba(36, 20, 23, 0.04);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1307,21 +1336,21 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.status-badge.status-confirmed {
|
.status-badge.status-confirmed {
|
||||||
background: rgba(201, 161, 90, 0.15);
|
background: rgba(107, 15, 34, 0.12);
|
||||||
color: var(--gold-bright);
|
color: var(--gold-bright);
|
||||||
border: 1px solid rgba(201, 161, 90, 0.35);
|
border: 1px solid rgba(107, 15, 34, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-badge.status-no_show {
|
.status-badge.status-no_show {
|
||||||
background: #3a1414;
|
background: #fbe4e4;
|
||||||
color: #f3a9a9;
|
color: #8a2020;
|
||||||
border: 1px solid #5c1f1f;
|
border: 1px solid #eeb3b3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-badge.status-completed {
|
.status-badge.status-completed {
|
||||||
background: #17301c;
|
background: #e5f3e8;
|
||||||
color: #a9d9b4;
|
color: #1f6b34;
|
||||||
border: 1px solid #23502c;
|
border: 1px solid #b8ddc2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* A single grouped 3-way toggle track, not three loose floating pills — the
|
/* A single grouped 3-way toggle track, not three loose floating pills — the
|
||||||
@@ -1359,12 +1388,12 @@ a {
|
|||||||
|
|
||||||
.status-option:hover:not(:disabled):not(.is-active) {
|
.status-option:hover:not(:disabled):not(.is-active) {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
background: rgba(243, 234, 217, 0.06);
|
background: rgba(36, 20, 23, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-option.is-active.status-confirmed {
|
.status-option.is-active.status-confirmed {
|
||||||
background: var(--gold);
|
background: var(--gold);
|
||||||
color: #201709;
|
color: #fbf4ef;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-option.is-active.status-no_show {
|
.status-option.is-active.status-no_show {
|
||||||
@@ -1545,7 +1574,7 @@ a {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
font-family: 'Fraunces', Georgia, serif;
|
font-family: 'Fraunces', Georgia, serif;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: #6b5f4d;
|
color: #6b5f5c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pdf-menu-spinner {
|
.pdf-menu-spinner {
|
||||||
@@ -1623,17 +1652,17 @@ a {
|
|||||||
|
|
||||||
.table-marker.is-selected .table-marker-label,
|
.table-marker.is-selected .table-marker-label,
|
||||||
.table-marker.is-active .table-marker-label {
|
.table-marker.is-active .table-marker-label {
|
||||||
color: #201709;
|
color: #fbf4ef;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-color: #5c1f1f;
|
border-color: #eeb3b3;
|
||||||
color: #f3a9a9;
|
color: #a3182f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger:hover:not(:disabled) {
|
.btn-danger:hover:not(:disabled) {
|
||||||
background: #3a1414;
|
background: #fbe4e4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger:disabled {
|
.btn-danger:disabled {
|
||||||
@@ -1719,16 +1748,16 @@ a {
|
|||||||
gap: 0.6rem;
|
gap: 0.6rem;
|
||||||
margin: 0 0 1.5rem;
|
margin: 0 0 1.5rem;
|
||||||
padding: 0.75rem 0.9rem;
|
padding: 0.75rem 0.9rem;
|
||||||
border: 1px solid #5c1f1f;
|
border: 1px solid #eeb3b3;
|
||||||
border-radius: 0.4rem;
|
border-radius: 0.4rem;
|
||||||
background: rgba(168, 64, 44, 0.14);
|
background: rgba(207, 46, 46, 0.08);
|
||||||
color: #f3a9a9;
|
color: #8a2020;
|
||||||
}
|
}
|
||||||
|
|
||||||
.confirm-dialog-warning svg {
|
.confirm-dialog-warning svg {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-top: 0.1rem;
|
margin-top: 0.1rem;
|
||||||
color: #f3a9a9;
|
color: #8a2020;
|
||||||
}
|
}
|
||||||
|
|
||||||
.confirm-dialog-warning span {
|
.confirm-dialog-warning span {
|
||||||
@@ -1767,15 +1796,15 @@ a {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reservation-confirmed-flame {
|
.reservation-confirmed-sparkle {
|
||||||
width: 3.25rem;
|
width: 3.25rem;
|
||||||
height: 3.25rem;
|
height: 3.25rem;
|
||||||
margin: 0 auto 1rem;
|
margin: 0 auto 1rem;
|
||||||
color: var(--ember);
|
color: var(--ember);
|
||||||
animation: reservation-confirmed-flicker 2.2s ease-in-out infinite;
|
animation: reservation-confirmed-twinkle 2.2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes reservation-confirmed-flicker {
|
@keyframes reservation-confirmed-twinkle {
|
||||||
0%,
|
0%,
|
||||||
100% {
|
100% {
|
||||||
transform: rotate(0deg) scale(1);
|
transform: rotate(0deg) scale(1);
|
||||||
@@ -1988,5 +2017,4 @@ a {
|
|||||||
|
|
||||||
.legal-page p {
|
.legal-page p {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
max-width: 42rem;
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ const config = defineConfig({
|
|||||||
resolve: { tsconfigPaths: true },
|
resolve: { tsconfigPaths: true },
|
||||||
plugins: [tanstackStart(), viteReact()],
|
plugins: [tanstackStart(), viteReact()],
|
||||||
preview: {
|
preview: {
|
||||||
allowedHosts: ['localhost', '127.0.0.1', 'promo-steakhouse.lukas-harat.com'],
|
allowedHosts: ['localhost', '127.0.0.1', 'promo-rathaus.lukas-harat.com'],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user