Init
44
.claude/settings.local.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(curl:*)",
|
||||||
|
"WebSearch",
|
||||||
|
"WebFetch(domain:medium.com)",
|
||||||
|
"WebFetch(domain:claudelog.com)",
|
||||||
|
"WebFetch(domain:github.com)",
|
||||||
|
"Bash(find:*)",
|
||||||
|
"WebFetch(domain:www.experimenta.science)",
|
||||||
|
"Bash(pnpm dlx:*)",
|
||||||
|
"Bash(test:*)",
|
||||||
|
"Bash(pnpm install:*)",
|
||||||
|
"Bash(pnpm view:*)",
|
||||||
|
"Bash(docker-compose:*)",
|
||||||
|
"Bash(tree:*)",
|
||||||
|
"Bash(timeout 30 pnpm dev:*)",
|
||||||
|
"Bash(ls:*)",
|
||||||
|
"Bash(pkill:*)",
|
||||||
|
"Bash(pnpm add:*)",
|
||||||
|
"Bash(npx shadcn-nuxt@latest init:*)",
|
||||||
|
"mcp__context7__resolve-library-id",
|
||||||
|
"Bash(pnpm format:*)",
|
||||||
|
"Bash(pnpm lint:*)",
|
||||||
|
"Bash(cat:*)",
|
||||||
|
"Bash(pnpm list:*)",
|
||||||
|
"Bash(npx nuxi:*)",
|
||||||
|
"Bash(pnpm typecheck:*)",
|
||||||
|
"Bash(pnpm exec nuxt typecheck:*)",
|
||||||
|
"Bash(docker ps:*)",
|
||||||
|
"Bash(pnpm dev)",
|
||||||
|
"Bash(lsof:*)",
|
||||||
|
"mcp__playwright__browser_navigate",
|
||||||
|
"mcp__playwright__browser_take_screenshot",
|
||||||
|
"mcp__playwright__browser_evaluate",
|
||||||
|
"mcp__playwright__browser_close",
|
||||||
|
"mcp__playwright__browser_wait_for",
|
||||||
|
"mcp__playwright__browser_snapshot",
|
||||||
|
"mcp__playwright__browser_hover"
|
||||||
|
],
|
||||||
|
"deny": [],
|
||||||
|
"ask": []
|
||||||
|
}
|
||||||
|
}
|
||||||
64
.dockerignore
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
pnpm-debug.log
|
||||||
|
.pnpm-store/
|
||||||
|
|
||||||
|
# Nuxt build outputs
|
||||||
|
.nuxt/
|
||||||
|
.output/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Development files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
||||||
|
|
||||||
|
# Documentation (not needed in container)
|
||||||
|
docs/
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
playwright-report/
|
||||||
|
test-results/
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
.cache/
|
||||||
|
.temp/
|
||||||
|
.tmp/
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.sqlite
|
||||||
|
*.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Drizzle
|
||||||
|
drizzle/
|
||||||
118
.env.example
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# ==============================================
|
||||||
|
# my.experimenta.science - Environment Variables
|
||||||
|
# ==============================================
|
||||||
|
# Copy this file to .env and fill in your values
|
||||||
|
# Never commit .env to git!
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# APPLICATION
|
||||||
|
# ==============================================
|
||||||
|
NODE_ENV=development
|
||||||
|
APP_URL=http://localhost:3000
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# DATABASE (PostgreSQL)
|
||||||
|
# ==============================================
|
||||||
|
# For local development with docker-compose.dev.yml:
|
||||||
|
DATABASE_URL=postgresql://dev:dev_password_change_me@localhost:5432/experimenta_dev
|
||||||
|
|
||||||
|
# For production, use separate values:
|
||||||
|
# DB_HOST=db
|
||||||
|
# DB_PORT=5432
|
||||||
|
# DB_NAME=experimenta
|
||||||
|
# DB_USER=experimenta_user
|
||||||
|
# DB_PASSWORD=xxx
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# REDIS (Sessions, Queues, Cache)
|
||||||
|
# ==============================================
|
||||||
|
REDIS_HOST=localhost
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASSWORD=
|
||||||
|
# For production: Set REDIS_PASSWORD
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# SESSION ENCRYPTION
|
||||||
|
# ==============================================
|
||||||
|
# Generate with: openssl rand -base64 32
|
||||||
|
NUXT_SESSION_PASSWORD=change-me-to-a-random-32-character-string-minimum
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# CIDAAS (OAuth2/OIDC Authentication)
|
||||||
|
# ==============================================
|
||||||
|
# Get these from Cidaas Admin Panel
|
||||||
|
CIDAAS_BASE_URL=https://experimenta.cidaas.de
|
||||||
|
CIDAAS_CLIENT_ID=your-client-id
|
||||||
|
CIDAAS_CLIENT_SECRET=your-client-secret
|
||||||
|
CIDAAS_REDIRECT_URI=http://localhost:3000/api/auth/callback
|
||||||
|
|
||||||
|
# Computed URLs (no need to change):
|
||||||
|
# CIDAAS_AUTHORIZE_URL=${CIDAAS_BASE_URL}/authz-srv/authz
|
||||||
|
# CIDAAS_TOKEN_URL=${CIDAAS_BASE_URL}/token-srv/token
|
||||||
|
# CIDAAS_USERINFO_URL=${CIDAAS_BASE_URL}/users-srv/userinfo
|
||||||
|
# CIDAAS_JWKS_URL=${CIDAAS_BASE_URL}/.well-known/jwks.json
|
||||||
|
# CIDAAS_ISSUER=${CIDAAS_BASE_URL}
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# PAYPAL (Payment Gateway)
|
||||||
|
# ==============================================
|
||||||
|
# Sandbox credentials for development
|
||||||
|
PAYPAL_CLIENT_ID=your-sandbox-client-id
|
||||||
|
PAYPAL_CLIENT_SECRET=your-sandbox-client-secret
|
||||||
|
PAYPAL_MODE=sandbox
|
||||||
|
# For production: Set PAYPAL_MODE=live and use live credentials
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# X-API (NAV ERP Integration)
|
||||||
|
# ==============================================
|
||||||
|
# HTTP Basic Authentication credentials
|
||||||
|
X_API_BASE_URL=https://x-api-dev.experimenta.science
|
||||||
|
X_API_USERNAME=shop_user_dev
|
||||||
|
X_API_PASSWORD=xxx
|
||||||
|
|
||||||
|
# Staging:
|
||||||
|
# X_API_BASE_URL=https://x-api-stage.experimenta.science
|
||||||
|
# X_API_USERNAME=shop_user_stage
|
||||||
|
# X_API_PASSWORD=xxx
|
||||||
|
|
||||||
|
# Production:
|
||||||
|
# X_API_BASE_URL=https://x-api.experimenta.science
|
||||||
|
# X_API_USERNAME=shop_user_prod
|
||||||
|
# X_API_PASSWORD=xxx
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# NAV ERP (Incoming Product Sync)
|
||||||
|
# ==============================================
|
||||||
|
# API Key for NAV ERP to push products to us
|
||||||
|
NAV_ERP_API_KEY=your-secure-api-key-for-nav-erp
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# EMAIL (Transactional Emails)
|
||||||
|
# ==============================================
|
||||||
|
# Option A: SMTP Server
|
||||||
|
SMTP_HOST=smtp.example.com
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_USER=your-smtp-username
|
||||||
|
SMTP_PASSWORD=your-smtp-password
|
||||||
|
SMTP_FROM=noreply@experimenta.science
|
||||||
|
|
||||||
|
# Option B: SendGrid
|
||||||
|
# SENDGRID_API_KEY=your-sendgrid-api-key
|
||||||
|
|
||||||
|
# Option C: Postmark
|
||||||
|
# POSTMARK_SERVER_TOKEN=your-postmark-token
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# MONITORING & LOGGING (Optional)
|
||||||
|
# ==============================================
|
||||||
|
# SENTRY_DSN=https://xxx@sentry.io/xxx
|
||||||
|
# SENTRY_ENVIRONMENT=development
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# DEVELOPMENT TOOLS
|
||||||
|
# ==============================================
|
||||||
|
# Enable Nuxt DevTools
|
||||||
|
NUXT_DEVTOOLS_ENABLED=true
|
||||||
|
|
||||||
|
# Enable verbose logging
|
||||||
|
# DEBUG=nuxt:*
|
||||||
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Nuxt dev/build outputs
|
||||||
|
.output
|
||||||
|
.data
|
||||||
|
.nuxt
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Node dependencies
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.fleet
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Local env files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
BIN
.playwright-mcp/button-hover-state.png
Normal file
|
After Width: | Height: | Size: 485 KiB |
BIN
.playwright-mcp/buttons-fixed.png
Normal file
|
After Width: | Height: | Size: 460 KiB |
BIN
.playwright-mcp/current-buttons.png
Normal file
|
After Width: | Height: | Size: 485 KiB |
BIN
.playwright-mcp/current-design-state.png
Normal file
|
After Width: | Height: | Size: 309 KiB |
BIN
.playwright-mcp/default-button-hover-fixed.png
Normal file
|
After Width: | Height: | Size: 457 KiB |
BIN
.playwright-mcp/default-button-hover.png
Normal file
|
After Width: | Height: | Size: 485 KiB |
BIN
.playwright-mcp/design-final-verification.png
Normal file
|
After Width: | Height: | Size: 365 KiB |
BIN
.playwright-mcp/design-verification-top.png
Normal file
|
After Width: | Height: | Size: 309 KiB |
BIN
.playwright-mcp/design-verification.png
Normal file
|
After Width: | Height: | Size: 309 KiB |
BIN
.playwright-mcp/experimenta-button-hover-comparison.png
Normal file
|
After Width: | Height: | Size: 453 KiB |
BIN
.playwright-mcp/footer-h3-improved.png
Normal file
|
After Width: | Height: | Size: 368 KiB |
BIN
.playwright-mcp/h3-orange-text-fixed.png
Normal file
|
After Width: | Height: | Size: 368 KiB |
BIN
.playwright-mcp/homepage-after-update.png
Normal file
|
After Width: | Height: | Size: 485 KiB |
BIN
.playwright-mcp/homepage-before.png
Normal file
|
After Width: | Height: | Size: 485 KiB |
38
.prettierignore
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
.pnpm-store
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
.nuxt
|
||||||
|
.output
|
||||||
|
dist
|
||||||
|
.vercel
|
||||||
|
.netlify
|
||||||
|
|
||||||
|
# Lock files
|
||||||
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# Cache and temp files
|
||||||
|
.cache
|
||||||
|
.temp
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Coverage reports
|
||||||
|
coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
7
.prettierrc.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
||||||
903
CLAUDE.md
Normal file
@@ -0,0 +1,903 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**my.experimenta.science** is an e-commerce platform for the experimenta Science Center. It allows visitors to purchase Makerspace annual passes (MVP) and extends their existing web shop.
|
||||||
|
|
||||||
|
**Domain:** `my.experimenta.science`
|
||||||
|
**Status:** In Planning / Initial Setup
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Task Management & Progress Tracking
|
||||||
|
|
||||||
|
**WICHTIG:** Dieses Projekt nutzt ein strukturiertes Task-System im `tasks/` Ordner.
|
||||||
|
|
||||||
|
### Workflow für alle Implementierungen:
|
||||||
|
|
||||||
|
**1. Vor Start einer Arbeitssession:**
|
||||||
|
|
||||||
|
- Lies `tasks/00-PROGRESS.md` → Identifiziere aktuelle Phase
|
||||||
|
- Öffne die relevante Phase-Datei (z.B. `tasks/03-authentication.md`)
|
||||||
|
- Prüfe Dependencies der Phase (sind alle abhängigen Phasen abgeschlossen?)
|
||||||
|
|
||||||
|
**2. Während der Implementierung:**
|
||||||
|
|
||||||
|
- Arbeite Tasks sequenziell ab (von oben nach unten in der Phase-Datei)
|
||||||
|
- Markiere abgeschlossene Tasks: `- [ ]` → `- [x]`
|
||||||
|
- Aktualisiere Progress-Zeile in Phase-Datei: `Progress: X/Y tasks (Z%)`
|
||||||
|
- Dokumentiere wichtige Entscheidungen im Notes-Bereich
|
||||||
|
- Bei Blocker: Status auf 🚫 setzen, Grund dokumentieren
|
||||||
|
|
||||||
|
**3. Nach jedem abgeschlossenen Task:**
|
||||||
|
|
||||||
|
- Update Phase-Datei (Task checkbox + Progress-Zeile)
|
||||||
|
- Update `tasks/00-PROGRESS.md`:
|
||||||
|
- Aktualisiere Progress in "Quick Status" Tabelle
|
||||||
|
- Aktualisiere "Current Work" Section (welche Phase, welcher Task, nächste Schritte)
|
||||||
|
- Falls Phase abgeschlossen: Status auf ✅, Started/Completed Datum eintragen
|
||||||
|
|
||||||
|
**4. Phase-Abschluss:**
|
||||||
|
|
||||||
|
- Prüfe: Alle Acceptance Criteria erfüllt?
|
||||||
|
- Setze Status in Phase-Datei auf ✅ Done
|
||||||
|
- Setze Status in `00-PROGRESS.md` auf ✅ Done
|
||||||
|
- Trage "Completed" Datum ein
|
||||||
|
- **⚠️ Frage den Benutzer explizit, ob mit der nächsten Phase fortgefahren werden soll**
|
||||||
|
- **Erst nach Bestätigung:** Identifiziere nächste Phase und starte mit Schritt 1
|
||||||
|
|
||||||
|
### Niemals vergessen:
|
||||||
|
|
||||||
|
- ⚠️ **Immer** `00-PROGRESS.md` aktualisieren nach jedem abgeschlossenen Task
|
||||||
|
- ⚠️ **Immer** Phase-Datei und PROGRESS.md synchron halten
|
||||||
|
- ⚠️ **Immer** bei Unterbrechung "Current Work" in PROGRESS.md dokumentieren
|
||||||
|
- ⚠️ **Niemals** mehrere Phasen gleichzeitig bearbeiten (sequenziell arbeiten)
|
||||||
|
|
||||||
|
**Siehe:** [`tasks/README.md`](./tasks/README.md) für vollständige Dokumentation des Task-Systems.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Style Requirements
|
||||||
|
|
||||||
|
- **All code comments MUST be written in English**
|
||||||
|
- User-facing content (UI text, emails) should be in German
|
||||||
|
- Documentation should be in German (PRD, README)
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Framework:** Nuxt 4 (Vue 3 Composition API + TypeScript)
|
||||||
|
- **UI Library:** shadcn-nuxt (<https://nuxt.com/modules/shadcn>)
|
||||||
|
- **Styling:** Tailwind CSS v4
|
||||||
|
- **Database:** PostgreSQL 16+
|
||||||
|
- **ORM:** Drizzle ORM (TypeScript-first, performant)
|
||||||
|
- **Queue System:** BullMQ (MIT License) - Async job processing
|
||||||
|
- **In-Memory Store:** Redis 7 - Queue storage, sessions, caching
|
||||||
|
- **Auth:** Cidaas (OIDC/OAuth2) - external platform
|
||||||
|
- **Payment:** PayPal (MVP), more providers later
|
||||||
|
- **i18n:** @nuxtjs/i18n - German (default) + English
|
||||||
|
- **Validation:** Zod + VeeValidate
|
||||||
|
- **State Management:** Pinia (minimal use, prefer composables)
|
||||||
|
- **Testing:** Vitest (unit/integration), Playwright (E2E)
|
||||||
|
- **Deployment:** Docker on Hetzner Proxmox
|
||||||
|
- **CI/CD:** GitLab
|
||||||
|
|
||||||
|
## Architecture Principles
|
||||||
|
|
||||||
|
### Authentication & User Management
|
||||||
|
|
||||||
|
**Critical:** Cidaas is **only** for authentication (login/registration). User profiles and roles are stored in the local PostgreSQL database.
|
||||||
|
|
||||||
|
- Cidaas provides: OAuth2 authentication, user identity
|
||||||
|
- Local DB stores: User profile, roles, preferences, purchase history
|
||||||
|
- Custom UI for login/registration (not Cidaas hosted pages)
|
||||||
|
- Link users via `experimenta_id` field
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
|
||||||
|
1. **Product Sync:** NAV ERP <20> Push to `/api/erp/products` <20> PostgreSQL
|
||||||
|
2. **Order Flow:** User checkout <20> PayPal payment <20> Order in DB <20> **X-API** <20> NAV ERP
|
||||||
|
3. **Auth Flow:** User login <20> Cidaas OAuth <20> Create/update local user profile <20> Session
|
||||||
|
|
||||||
|
### Key Integrations
|
||||||
|
|
||||||
|
- **NAV ERP:** Microsoft Dynamics NAV - Push products/stock to our API
|
||||||
|
- **X-API:** Order submission to NAV ERP via `/shopware/order` endpoint
|
||||||
|
- Environments: x-api-{dev|stage|live}.experimenta.science
|
||||||
|
- **Authentication:** HTTP Basic Auth (username + password)
|
||||||
|
- Credentials stored in environment variables (never hardcoded)
|
||||||
|
- Converts our JSON orders to SOAP for NAV ERP
|
||||||
|
- Critical: Prices must be in cents (Integer), dates in ISO 8601 UTC
|
||||||
|
- **Cidaas:** Auth platform by Widas (company requirement)
|
||||||
|
|
||||||
|
## MVP Scope (Phase 1)
|
||||||
|
|
||||||
|
**In Scope:**
|
||||||
|
|
||||||
|
- User registration and login (email-based via Cidaas)
|
||||||
|
- Display Makerspace annual passes ("Makerspace-Jahreskarten")
|
||||||
|
- Shopping cart functionality
|
||||||
|
- Checkout process
|
||||||
|
- PayPal payment integration
|
||||||
|
- NAV ERP product sync (receive products via push)
|
||||||
|
|
||||||
|
**Out of Scope (Post-MVP):**
|
||||||
|
|
||||||
|
- User roles (Educator, Company) - MVP only supports "Privatperson" implicitly
|
||||||
|
- Educator annual passes ("Pädagogische Jahreskarten")
|
||||||
|
- Approval workflows
|
||||||
|
- Experimenta tickets + Science Dome seat reservations
|
||||||
|
- Lab courses for schools
|
||||||
|
- Multi-payment providers
|
||||||
|
|
||||||
|
## Project Structure (Once Initialized)
|
||||||
|
|
||||||
|
```
|
||||||
|
/
|
||||||
|
├── app/
|
||||||
|
│ ├── components/ # Vue components (Nuxt 4 structure)
|
||||||
|
│ │ └── ui/ # shadcn-nuxt components
|
||||||
|
│ ├── layouts/ # Nuxt layouts
|
||||||
|
│ ├── lib/ # Client-side utilities
|
||||||
|
│ │ └── utils.ts # Shared utilities
|
||||||
|
│ └── pages/ # File-based routing (Nuxt 4)
|
||||||
|
├── server/
|
||||||
|
│ ├── api/ # API routes (Nitro)
|
||||||
|
│ │ ├── auth/ # Cidaas OAuth handlers
|
||||||
|
│ │ ├── products/ # Product endpoints
|
||||||
|
│ │ ├── cart/ # Shopping cart
|
||||||
|
│ │ ├── orders/ # Order management
|
||||||
|
│ │ ├── payment/ # PayPal integration
|
||||||
|
│ │ └── erp/ # NAV ERP endpoints (API key protected)
|
||||||
|
│ ├── database/
|
||||||
|
│ │ ├── schema.ts # Drizzle schema definitions
|
||||||
|
│ │ └── migrations/ # DB migrations
|
||||||
|
│ └── utils/ # Server-side utilities
|
||||||
|
├── components/ # Legacy components (to be migrated to app/)
|
||||||
|
├── composables/ # Vue composables
|
||||||
|
├── pages/ # Legacy pages (to be migrated to app/)
|
||||||
|
├── docs/ # Documentation
|
||||||
|
│ ├── PRD.md # Product Requirements Document
|
||||||
|
│ ├── TECH_STACK.md # Tech decisions & rationale
|
||||||
|
│ └── ARCHITECTURE.md # System architecture
|
||||||
|
└── docker-compose.yml # Docker setup
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
See `docs/ARCHITECTURE.md` for full schema. Key tables:
|
||||||
|
|
||||||
|
- `users` - User profiles (linked to Cidaas via `experimenta_id`)
|
||||||
|
- **Includes billing address fields:** `salutation`, `date_of_birth`, `street`, `post_code`, `city`, `country_code`
|
||||||
|
- Address fields are optional, filled during checkout or profile edit
|
||||||
|
- Pre-fills checkout form for returning customers
|
||||||
|
- `products` - Products synced from NAV ERP
|
||||||
|
- `carts` / `cart_items` - Shopping cart
|
||||||
|
- `orders` / `order_items` - Orders and line items
|
||||||
|
|
||||||
|
Use Drizzle ORM for all database operations. All tables use UUID primary keys.
|
||||||
|
|
||||||
|
## Development Commands (Once Set Up)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# Development server
|
||||||
|
pnpm dev
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# Run production build locally
|
||||||
|
pnpm preview
|
||||||
|
|
||||||
|
# Database migrations
|
||||||
|
pnpm db:generate # Generate migrations from schema changes
|
||||||
|
pnpm db:migrate # Apply migrations to database
|
||||||
|
pnpm db:studio # Open Drizzle Studio (DB GUI)
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
pnpm test # Run unit tests (Vitest)
|
||||||
|
pnpm test:e2e # Run E2E tests (Playwright)
|
||||||
|
pnpm test:coverage # Generate coverage report
|
||||||
|
|
||||||
|
# Code quality
|
||||||
|
pnpm lint # ESLint
|
||||||
|
pnpm format # Prettier
|
||||||
|
pnpm typecheck # TypeScript type checking
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Tools & Testing
|
||||||
|
|
||||||
|
### Playwright MCP Server
|
||||||
|
|
||||||
|
Ein Playwright MCP Server ist installiert und verfügbar für:
|
||||||
|
|
||||||
|
- **UI/UX Überprüfung:** Visuelle Validierung von Komponenten und Layouts
|
||||||
|
- **Funktionale Tests:** Interaktive Tests während der Entwicklung
|
||||||
|
- **Responsive Design Checks:** Überprüfung der Mobile-First Implementierung
|
||||||
|
- **User Flow Testing:** Validierung kompletter Benutzer-Workflows (z.B. Checkout-Prozess)
|
||||||
|
|
||||||
|
**Wann nutzen:**
|
||||||
|
|
||||||
|
- Nach Implementierung neuer UI-Komponenten
|
||||||
|
- Bei Layout-Anpassungen (besonders mobile vs. desktop)
|
||||||
|
- Zum Testen von Formularen und Interaktionen
|
||||||
|
- Zur Validierung des gesamten Checkout-Flows
|
||||||
|
- Bei Integration von shadcn-nuxt Komponenten
|
||||||
|
|
||||||
|
**Workflow:**
|
||||||
|
|
||||||
|
1. Starte den Dev-Server (`pnpm dev`)
|
||||||
|
2. Nutze Playwright MCP Tools um die Anwendung zu navigieren und zu testen
|
||||||
|
3. Validiere visuelle Darstellung und Funktionalität
|
||||||
|
4. Dokumentiere gefundene Issues oder bestätige erfolgreiche Implementierung
|
||||||
|
|
||||||
|
## Important Constraints
|
||||||
|
|
||||||
|
1. **Cidaas Custom UI:** We must implement custom login/registration forms in our app (not hosted by Cidaas). Use OIDC/OAuth2 flow with Cidaas as identity provider.
|
||||||
|
|
||||||
|
2. **Mobile-First:** All UI must be optimized for mobile devices first, then scale up to desktop.
|
||||||
|
|
||||||
|
3. **Corporate Design:** Use experimenta style guide for colors and fonts (configured in Tailwind).
|
||||||
|
|
||||||
|
4. **NAV ERP Integration:** Products are pushed to us (we don't pull). Implement robust endpoint with API key authentication and validation.
|
||||||
|
|
||||||
|
5. **No Roles in MVP:** Don't implement role selection or role-based features yet. All users are implicitly "Privatperson" (private individual).
|
||||||
|
|
||||||
|
6. **TypeScript Strict Mode:** Use strict TypeScript everywhere. All schemas defined with Zod.
|
||||||
|
|
||||||
|
7. **Bilingual Support:** App must support German (default) and English. Use @nuxtjs/i18n for all user-facing text. Routes: `/produkte` (de), `/en/products` (en).
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- Never commit secrets (`.env` is gitignored)
|
||||||
|
- All API endpoints must validate input (use Zod schemas)
|
||||||
|
- ERP endpoints must be protected with API key authentication
|
||||||
|
- Use session-based auth (HTTP-only cookies)
|
||||||
|
- Validate JWT tokens from Cidaas using `jose` library
|
||||||
|
- Implement rate limiting on public endpoints
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### API Route Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// server/api/products/[id].get.ts
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
// Validate path params with Zod
|
||||||
|
const { id } = await getValidatedRouterParams(
|
||||||
|
event,
|
||||||
|
z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
}).parse
|
||||||
|
)
|
||||||
|
|
||||||
|
// Query with Drizzle
|
||||||
|
const product = await db.query.products.findFirst({
|
||||||
|
where: eq(products.id, id),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 404,
|
||||||
|
message: 'Product not found',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return product
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Protected Route Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// middleware/auth.ts
|
||||||
|
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||||
|
const { user } = await useAuth()
|
||||||
|
|
||||||
|
if (!user.value) {
|
||||||
|
return navigateTo('/login')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Checkout with Saved Address Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// pages/checkout.vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { user } = await useAuth()
|
||||||
|
const { saveAddress } = useUserProfile()
|
||||||
|
|
||||||
|
// Pre-fill form if user has saved address
|
||||||
|
const form = reactive({
|
||||||
|
salutation: user.value?.salutation || '',
|
||||||
|
dateOfBirth: user.value?.dateOfBirth || '',
|
||||||
|
street: user.value?.street || '',
|
||||||
|
postCode: user.value?.postCode || '',
|
||||||
|
city: user.value?.city || '',
|
||||||
|
countryCode: user.value?.countryCode || 'DE',
|
||||||
|
// Pre-checked if user doesn't have address yet
|
||||||
|
saveForFuture: !user.value?.street
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleCheckout() {
|
||||||
|
// Validate form
|
||||||
|
const validated = await checkoutSchema.parseAsync(form)
|
||||||
|
|
||||||
|
// Save address to profile if checkbox is checked
|
||||||
|
if (form.saveForFuture) {
|
||||||
|
await saveAddress({
|
||||||
|
salutation: validated.salutation,
|
||||||
|
dateOfBirth: validated.dateOfBirth,
|
||||||
|
street: validated.street,
|
||||||
|
postCode: validated.postCode,
|
||||||
|
city: validated.city,
|
||||||
|
countryCode: validated.countryCode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue with payment
|
||||||
|
await processPayment(validated)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form @submit.prevent="handleCheckout">
|
||||||
|
<h2>{{ $t('checkout.billingAddress') }}</h2>
|
||||||
|
|
||||||
|
<!-- Address form fields -->
|
||||||
|
<Input v-model="form.street" :label="$t('checkout.street')" required />
|
||||||
|
<Input v-model="form.postCode" :label="$t('checkout.postCode')" required />
|
||||||
|
<Input v-model="form.city" :label="$t('checkout.city')" required />
|
||||||
|
|
||||||
|
<!-- Save address checkbox -->
|
||||||
|
<Checkbox
|
||||||
|
v-model="form.saveForFuture"
|
||||||
|
:label="$t('checkout.saveAddress')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit">{{ $t('checkout.continue') }}</Button>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### X-API Order Transformation Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// server/utils/xapi.ts
|
||||||
|
|
||||||
|
// Transform order from DB to X-API format
|
||||||
|
export function transformOrderToXAPI(order: Order, user: User) {
|
||||||
|
return {
|
||||||
|
shopPOSOrder: {
|
||||||
|
documentType: 'Order',
|
||||||
|
externalDocumentNo: order.orderNumber,
|
||||||
|
salesChannel: 'Shop',
|
||||||
|
shoppingCartCompletion: order.createdAt.toISOString(),
|
||||||
|
visitType: 'Private',
|
||||||
|
amountIncludingVAT: Math.round(order.totalAmount * 100), // EUR -> Cents!
|
||||||
|
language: 'DEU',
|
||||||
|
|
||||||
|
salesLine: order.items.map((item, index) => ({
|
||||||
|
type: 'Item',
|
||||||
|
lineNo: String((index + 1) * 10000), // 10000, 20000, 30000...
|
||||||
|
no: item.product.navProductId,
|
||||||
|
quantity: item.quantity,
|
||||||
|
unitPrice: Math.round(item.priceSnapshot * 100), // EUR -> Cents!
|
||||||
|
vatPct: 7,
|
||||||
|
visitorCategory: '120', // Annual pass holders
|
||||||
|
ticketCode: generateUniqueTicketCode(),
|
||||||
|
|
||||||
|
// Required for annual passes!
|
||||||
|
annualPass: {
|
||||||
|
salutationCode: mapSalutation(user.salutation), // 'HERR', 'FRAU', 'K_ANGABE'
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
streetAndHouseNumber: user.address.street,
|
||||||
|
postCode: user.address.postCode,
|
||||||
|
city: user.address.city,
|
||||||
|
countryCode: user.address.countryCode || 'DE',
|
||||||
|
dateOfBirth: user.dateOfBirth,
|
||||||
|
eMail: user.email,
|
||||||
|
validFrom: new Date().toISOString().split('T')[0], // YYYY-MM-DD
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
|
||||||
|
payment: [
|
||||||
|
{
|
||||||
|
paymentEntryNo: 10000,
|
||||||
|
amount: Math.round(order.totalAmount * 100), // EUR -> Cents!
|
||||||
|
paymentType: 'Shop PayPal',
|
||||||
|
createdOn: order.paymentCompletedAt.toISOString(),
|
||||||
|
reference: order.paymentId,
|
||||||
|
paymentId: order.paymentId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
personContact: {
|
||||||
|
experimentaAccountID: user.experimentaId, // Our experimenta_id!
|
||||||
|
eMail: user.email,
|
||||||
|
salutationCode: mapSalutation(user.salutation),
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
streetAndHouseNumber: order.billingAddress.street,
|
||||||
|
postCode: order.billingAddress.postCode,
|
||||||
|
city: order.billingAddress.city,
|
||||||
|
countryCode: order.billingAddress.countryCode || 'DE',
|
||||||
|
phoneNumber: user.phone || '',
|
||||||
|
guest: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit to X-API with retry logic and Basic Auth
|
||||||
|
export async function submitOrderToXAPI(payload: XAPIOrderPayload) {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const maxRetries = 3
|
||||||
|
const retryDelays = [1000, 3000, 9000] // Exponential backoff
|
||||||
|
|
||||||
|
// Prepare Basic Auth header
|
||||||
|
const authString = Buffer.from(`${config.xApiUsername}:${config.xApiPassword}`).toString('base64')
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${config.xApiBaseUrl}/shopware/order`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Basic ${authString}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text()
|
||||||
|
throw new Error(`X-API error ${response.status}: ${errorText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json()
|
||||||
|
} catch (error) {
|
||||||
|
if (attempt === maxRetries - 1) throw error
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, retryDelays[attempt]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Critical transformations:**
|
||||||
|
|
||||||
|
- Prices: EUR (Decimal) → Cents (Integer) using `Math.round(price * 100)`
|
||||||
|
- Dates: JavaScript Date → ISO 8601 UTC using `.toISOString()`
|
||||||
|
- Line numbers: Sequential multiples of 10000
|
||||||
|
- Salutation: 'male' → 'HERR', 'female' → 'FRAU', other → 'K_ANGABE'
|
||||||
|
|
||||||
|
## Authentication Patterns
|
||||||
|
|
||||||
|
See [`docs/CIDAAS_INTEGRATION.md`](./docs/CIDAAS_INTEGRATION.md) for complete Cidaas OAuth2 implementation guide.
|
||||||
|
|
||||||
|
### OAuth2 Login Flow Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// composables/useAuth.ts - Client-side auth composable
|
||||||
|
export function useAuth() {
|
||||||
|
const { loggedIn, user, clear, fetch } = useUserSession()
|
||||||
|
|
||||||
|
async function login(email: string) {
|
||||||
|
// Initiate OAuth2 Authorization Code Flow with PKCE
|
||||||
|
const { redirectUrl } = await $fetch('/api/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { email },
|
||||||
|
})
|
||||||
|
|
||||||
|
// Redirect to Cidaas login page
|
||||||
|
navigateTo(redirectUrl, { external: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
await $fetch('/api/auth/logout', { method: 'POST' })
|
||||||
|
await clear()
|
||||||
|
navigateTo('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
return { user, loggedIn, login, logout }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Server-side:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// server/api/auth/login.post.ts - Initiate OAuth2 flow
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const { email } = await readBody(event)
|
||||||
|
|
||||||
|
// 1. Generate PKCE challenge
|
||||||
|
const { verifier, challenge } = await generatePKCE()
|
||||||
|
|
||||||
|
// 2. Store PKCE verifier in cookie (5min TTL)
|
||||||
|
setCookie(event, 'pkce_verifier', verifier, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
sameSite: 'lax',
|
||||||
|
maxAge: 300,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 3. Generate state (CSRF protection)
|
||||||
|
const state = generateState(32)
|
||||||
|
setCookie(event, 'oauth_state', state, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
sameSite: 'lax',
|
||||||
|
maxAge: 300,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 4. Build Cidaas authorization URL
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const authUrl = new URL(config.cidaas.authorizeUrl)
|
||||||
|
authUrl.searchParams.set('client_id', config.cidaas.clientId)
|
||||||
|
authUrl.searchParams.set('redirect_uri', config.cidaas.redirectUri)
|
||||||
|
authUrl.searchParams.set('response_type', 'code')
|
||||||
|
authUrl.searchParams.set('scope', 'openid profile email')
|
||||||
|
authUrl.searchParams.set('state', state)
|
||||||
|
authUrl.searchParams.set('code_challenge', challenge)
|
||||||
|
authUrl.searchParams.set('code_challenge_method', 'S256')
|
||||||
|
authUrl.searchParams.set('login_hint', email)
|
||||||
|
|
||||||
|
return { redirectUrl: authUrl.toString() }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### OAuth2 Callback Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// server/api/auth/callback.get.ts - Handle OAuth2 callback
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const { code, state } = getQuery(event)
|
||||||
|
|
||||||
|
// 1. Validate state (CSRF protection)
|
||||||
|
const storedState = getCookie(event, 'oauth_state')
|
||||||
|
if (!storedState || state !== storedState) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: 'Invalid state parameter',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Retrieve PKCE verifier
|
||||||
|
const verifier = getCookie(event, 'pkce_verifier')
|
||||||
|
if (!verifier) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: 'PKCE verifier not found',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 3. Exchange code for tokens
|
||||||
|
const tokens = await exchangeCodeForToken(code as string, verifier)
|
||||||
|
|
||||||
|
// 4. Validate ID token
|
||||||
|
const idTokenPayload = await verifyIdToken(tokens.id_token)
|
||||||
|
|
||||||
|
// 5. Fetch user info from Cidaas
|
||||||
|
const cidaasUser = await fetchUserInfo(tokens.access_token)
|
||||||
|
|
||||||
|
// 6. Create/update user in local DB
|
||||||
|
const db = useDatabase()
|
||||||
|
let user = await db.query.users.findFirst({
|
||||||
|
where: eq(users.experimentaId, cidaasUser.sub),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
// First time login - create user
|
||||||
|
const [newUser] = await db
|
||||||
|
.insert(users)
|
||||||
|
.values({
|
||||||
|
experimentaId: cidaasUser.sub,
|
||||||
|
email: cidaasUser.email,
|
||||||
|
firstName: cidaasUser.given_name,
|
||||||
|
lastName: cidaasUser.family_name,
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
user = newUser
|
||||||
|
} else {
|
||||||
|
// Update last login
|
||||||
|
await db.update(users).set({ updatedAt: new Date() }).where(eq(users.id, user.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Create encrypted session
|
||||||
|
await setUserSession(event, {
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 8. Clean up temporary cookies
|
||||||
|
deleteCookie(event, 'oauth_state')
|
||||||
|
deleteCookie(event, 'pkce_verifier')
|
||||||
|
|
||||||
|
// 9. Redirect to homepage
|
||||||
|
return sendRedirect(event, '/')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('OAuth callback error:', error)
|
||||||
|
deleteCookie(event, 'oauth_state')
|
||||||
|
deleteCookie(event, 'pkce_verifier')
|
||||||
|
return sendRedirect(event, '/auth?error=login_failed')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Protected Route Middleware Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// middleware/auth.ts - Require authentication
|
||||||
|
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||||
|
const { loggedIn } = useUserSession()
|
||||||
|
|
||||||
|
if (!loggedIn.value) {
|
||||||
|
// Store intended destination for post-login redirect
|
||||||
|
useCookie('redirect_after_login', {
|
||||||
|
maxAge: 600, // 10 minutes
|
||||||
|
path: '/',
|
||||||
|
}).value = to.fullPath
|
||||||
|
|
||||||
|
return navigateTo('/auth')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage in pages:**
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- pages/profile.vue -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'auth', // Require authentication
|
||||||
|
})
|
||||||
|
|
||||||
|
const { user } = useAuth()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Welcome, {{ user.firstName }}!</h1>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Protected API Endpoint Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// server/api/orders/index.get.ts - Protected API route
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
// Require authentication (throws 401 if not logged in)
|
||||||
|
const { user } = await requireUserSession(event)
|
||||||
|
|
||||||
|
// Fetch user's orders
|
||||||
|
const db = useDatabase()
|
||||||
|
const orders = await db.query.orders.findMany({
|
||||||
|
where: eq(orders.userId, user.id),
|
||||||
|
orderBy: desc(orders.createdAt),
|
||||||
|
})
|
||||||
|
|
||||||
|
return orders
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### User Registration Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// server/api/auth/register.post.ts - Register via Cidaas
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
const registerSchema = z.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
password: z.string().min(8).regex(/[A-Z]/).regex(/[a-z]/).regex(/[0-9]/),
|
||||||
|
firstName: z.string().min(2),
|
||||||
|
lastName: z.string().min(2),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
// Validate input
|
||||||
|
const body = await readBody(event)
|
||||||
|
const validated = registerSchema.parse(body)
|
||||||
|
|
||||||
|
// Register user via Cidaas API
|
||||||
|
const result = await registerUser({
|
||||||
|
email: validated.email,
|
||||||
|
password: validated.password,
|
||||||
|
given_name: validated.firstName,
|
||||||
|
family_name: validated.lastName,
|
||||||
|
locale: 'de',
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Registration successful. Please verify your email.',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Session Management Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Server-side: Create session
|
||||||
|
await setUserSession(event, {
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
},
|
||||||
|
loggedInAt: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Server-side: Check session
|
||||||
|
const { user } = await getUserSession(event)
|
||||||
|
if (!user) {
|
||||||
|
throw createError({ statusCode: 401, message: 'Not authenticated' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server-side: Require session
|
||||||
|
const { user } = await requireUserSession(event) // Throws 401 if not logged in
|
||||||
|
|
||||||
|
// Server-side: Clear session
|
||||||
|
await clearUserSession(event)
|
||||||
|
```
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- Client-side: Use session in components -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { user, loggedIn } = useUserSession()
|
||||||
|
|
||||||
|
// user.value: { id, email, firstName, ... } or null
|
||||||
|
// loggedIn.value: boolean
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="loggedIn">
|
||||||
|
<p>Welcome {{ user.firstName }}!</p>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<NuxtLink to="/auth">Login</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### JWT Validation Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// server/utils/jwt.ts - Validate Cidaas ID tokens
|
||||||
|
import { jwtVerify, createRemoteJWKSet } from 'jose'
|
||||||
|
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const JWKS = createRemoteJWKSet(new URL(config.cidaas.jwksUrl))
|
||||||
|
|
||||||
|
export async function verifyIdToken(idToken: string) {
|
||||||
|
try {
|
||||||
|
const { payload } = await jwtVerify(idToken, JWKS, {
|
||||||
|
issuer: config.cidaas.issuer,
|
||||||
|
audience: config.cidaas.clientId,
|
||||||
|
})
|
||||||
|
return payload
|
||||||
|
} catch (error) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 401,
|
||||||
|
statusMessage: 'Invalid ID token',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rate Limiting Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// server/middleware/rate-limit.ts - Rate limit auth endpoints
|
||||||
|
interface RateLimitEntry {
|
||||||
|
count: number
|
||||||
|
resetAt: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const rateLimitStore = new Map<string, RateLimitEntry>()
|
||||||
|
|
||||||
|
export default defineEventHandler((event) => {
|
||||||
|
const path = event.path
|
||||||
|
if (!path.startsWith('/api/auth/')) return
|
||||||
|
|
||||||
|
const ip = getRequestIP(event) || 'unknown'
|
||||||
|
const limits = {
|
||||||
|
'/api/auth/login': { maxAttempts: 5, windowMs: 15 * 60 * 1000 },
|
||||||
|
'/api/auth/register': { maxAttempts: 3, windowMs: 60 * 60 * 1000 },
|
||||||
|
}
|
||||||
|
|
||||||
|
const limit = limits[path]
|
||||||
|
if (!limit) return
|
||||||
|
|
||||||
|
const key = `${ip}:${path}`
|
||||||
|
const now = Date.now()
|
||||||
|
const entry = rateLimitStore.get(key)
|
||||||
|
|
||||||
|
if (!entry || entry.resetAt < now) {
|
||||||
|
rateLimitStore.set(key, { count: 1, resetAt: now + limit.windowMs })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.count++
|
||||||
|
if (entry.count > limit.maxAttempts) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 429,
|
||||||
|
statusMessage: 'Too many requests',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Security Practices
|
||||||
|
|
||||||
|
1. **PKCE for OAuth2:**
|
||||||
|
- Always use PKCE (Proof Key for Code Exchange)
|
||||||
|
- Prevents authorization code interception attacks
|
||||||
|
- Generate random verifier, compute SHA-256 challenge
|
||||||
|
- Store verifier in temporary cookie (5min TTL)
|
||||||
|
|
||||||
|
2. **State Parameter:**
|
||||||
|
- Generate random state for CSRF protection
|
||||||
|
- Validate state on callback
|
||||||
|
- Store in temporary cookie (5min TTL)
|
||||||
|
|
||||||
|
3. **Encrypted Sessions:**
|
||||||
|
- Use `nuxt-auth-utils` for session management
|
||||||
|
- Sessions encrypted with AES-256-GCM
|
||||||
|
- HTTP-only, Secure, SameSite=Lax cookies
|
||||||
|
- 30-day expiration (configurable)
|
||||||
|
|
||||||
|
4. **Rate Limiting:**
|
||||||
|
- Login: 5 attempts / 15min per IP
|
||||||
|
- Register: 3 attempts / hour per IP
|
||||||
|
- Implement in middleware, not individual endpoints
|
||||||
|
|
||||||
|
5. **Input Validation:**
|
||||||
|
- Always validate with Zod schemas
|
||||||
|
- Validate on both client and server
|
||||||
|
- Never trust client-side validation alone
|
||||||
|
|
||||||
|
**Implementation Details:**
|
||||||
|
|
||||||
|
For complete implementation with all utilities (PKCE generator, Cidaas API client, full endpoint code), see:
|
||||||
|
|
||||||
|
- [`docs/CIDAAS_INTEGRATION.md`](./docs/CIDAAS_INTEGRATION.md) - Complete implementation guide
|
||||||
|
- [`docs/ARCHITECTURE.md#3.6`](./docs/ARCHITECTURE.md#36-authentication--authorization-cidaas-oauth2oidc) - Architecture diagrams and flows
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- **PRD:** See `docs/PRD.md` for complete requirements
|
||||||
|
- **Tech Stack:** See `docs/TECH_STACK.md` for technology decisions
|
||||||
|
- **Architecture:** See `docs/ARCHITECTURE.md` for system design and data flows
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
- **Platform:** Docker containers on Hetzner Proxmox
|
||||||
|
- **CI/CD:** GitLab pipelines (build <20> test <20> deploy)
|
||||||
|
- **Environments:** Staging (auto-deploy) + Production (manual approval)
|
||||||
|
- **Database Backups:** Daily automated backups with 7-day retention
|
||||||
|
|
||||||
|
## Future Phases
|
||||||
|
|
||||||
|
After MVP, we'll add:
|
||||||
|
|
||||||
|
- **Phase 2:** Educator roles + approval workflow + educator annual passes
|
||||||
|
- **Phase 3:** Experimenta tickets + Science Dome seat reservations
|
||||||
|
- **Phase 4:** Lab courses for schools
|
||||||
|
|
||||||
|
Keep the codebase modular to accommodate these features without major refactoring.
|
||||||
|
|
||||||
|
- Always use context7 when I need code generation, setup or configuration steps, or
|
||||||
|
library/API documentation. This means you should automatically use the Context7 MCP
|
||||||
|
tools to resolve library id and get library docs without me having to explicitly ask.
|
||||||
75
README.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# Nuxt Minimal Starter
|
||||||
|
|
||||||
|
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Make sure to install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Server
|
||||||
|
|
||||||
|
Start the development server on `http://localhost:3000`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm dev
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn dev
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production
|
||||||
|
|
||||||
|
Build the application for production:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn build
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Locally preview production build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm run preview
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm preview
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn preview
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
||||||
5
app/app.vue
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<NuxtPage />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
152
app/components/CommonFooter.vue
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const currentYear = new Date().getFullYear()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="footer-content">
|
||||||
|
<div class="footer-main">
|
||||||
|
<!-- Logo & Description Column -->
|
||||||
|
<div class="footer-section">
|
||||||
|
<div class="footer-logo">
|
||||||
|
<NuxtLink to="/">
|
||||||
|
<img src="/img/experimenta-logo-white.svg" alt="experimenta Logo" class="footer-logo-svg" />
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
experimenta ist Deutschlands größtes Science Center. Mit über 275 Mitmachstationen, vier
|
||||||
|
Kreativstudios, neun Laboren und einer Sternwarte.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Links Column -->
|
||||||
|
<div class="footer-section">
|
||||||
|
<h3>Rechtliches</h3>
|
||||||
|
<ul class="footer-links">
|
||||||
|
<li><a href="#">Impressum</a></li>
|
||||||
|
<li><a href="#">Datenschutz</a></li>
|
||||||
|
<li><a href="#">AGB</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info Column -->
|
||||||
|
<div class="footer-section">
|
||||||
|
<h3>Über uns</h3>
|
||||||
|
<ul class="footer-links">
|
||||||
|
<li><a href="#">experimenta Shop</a></li>
|
||||||
|
<li><a href="#">Kontakt</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contact Column -->
|
||||||
|
<div class="footer-section">
|
||||||
|
<h3>Kontakt</h3>
|
||||||
|
<p>experimenta gGmbH<br />Kranenstraße 14<br />74072 Heilbronn</p>
|
||||||
|
<p>
|
||||||
|
<a href="mailto:info@experimenta.science" class="footer-link">info@experimenta.science</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer Bottom -->
|
||||||
|
<div class="footer-bottom">
|
||||||
|
<p>© {{ currentYear }} experimenta gGmbH. Alle Rechte vorbehalten.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.footer {
|
||||||
|
background: linear-gradient(135deg, #1a0a3a 0%, #0f051d 100%);
|
||||||
|
margin-top: 80px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 60px 20px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-main {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2.5fr 1fr 1fr 1.5fr;
|
||||||
|
gap: 50px;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section h3 {
|
||||||
|
color: #f59d24;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section p {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links li {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links a,
|
||||||
|
.footer-link {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links a:hover,
|
||||||
|
.footer-link:hover {
|
||||||
|
color: #ff4081;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-logo-svg {
|
||||||
|
width: 200px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom {
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
padding-top: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom p {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.footer-main {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-logo-svg {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
63
app/components/CommonHeader.vue
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// experimenta header with branding
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<header class="header-wrapper">
|
||||||
|
<div class="header-content">
|
||||||
|
<NuxtLink to="/" class="logo">
|
||||||
|
<img src="/img/experimenta-logo-white.svg" alt="experimenta Logo" class="logo-svg" />
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.header-wrapper {
|
||||||
|
background: rgba(46, 16, 101, 0.95);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
position: relative;
|
||||||
|
z-index: 100;
|
||||||
|
padding: 30px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: white;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-svg {
|
||||||
|
width: 300px;
|
||||||
|
height: auto;
|
||||||
|
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.logo-svg {
|
||||||
|
width: 250px;
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.logo-svg {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
28
app/components/ui/button/Button.vue
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PrimitiveProps } from 'reka-ui'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import type { ButtonVariants } from '.'
|
||||||
|
import { Primitive } from 'reka-ui'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
import { buttonVariants } from '.'
|
||||||
|
|
||||||
|
interface Props extends PrimitiveProps {
|
||||||
|
variant?: ButtonVariants['variant']
|
||||||
|
size?: ButtonVariants['size']
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
as: 'button',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Primitive
|
||||||
|
:as="as"
|
||||||
|
:as-child="asChild"
|
||||||
|
:class="cn(buttonVariants({ variant, size }), props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Primitive>
|
||||||
|
</template>
|
||||||
36
app/components/ui/button/index.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import type { VariantProps } from 'class-variance-authority'
|
||||||
|
import { cva } from 'class-variance-authority'
|
||||||
|
|
||||||
|
export { default as Button } from './Button.vue'
|
||||||
|
|
||||||
|
export const buttonVariants = cva(
|
||||||
|
'inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: 'btn-experimenta',
|
||||||
|
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90 rounded-md',
|
||||||
|
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground rounded-md',
|
||||||
|
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 rounded-md',
|
||||||
|
ghost: 'hover:bg-accent hover:text-accent-foreground rounded-md',
|
||||||
|
link: 'text-primary underline-offset-4 hover:underline',
|
||||||
|
experimenta: 'btn-experimenta',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: 'px-[30px] py-[10px] text-lg leading-[1.7em]',
|
||||||
|
sm: 'h-9 rounded-md px-3',
|
||||||
|
lg: 'h-11 rounded-md px-8',
|
||||||
|
icon: 'h-10 w-10',
|
||||||
|
'icon-sm': 'size-9',
|
||||||
|
'icon-lg': 'size-11',
|
||||||
|
experimenta: 'px-[30px] py-[10px] text-lg leading-[1.7em]',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default',
|
||||||
|
size: 'default',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export type ButtonVariants = VariantProps<typeof buttonVariants>
|
||||||
18
app/layouts/default.vue
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// Default layout with Header and Footer components
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="min-h-screen flex flex-col">
|
||||||
|
<!-- Header -->
|
||||||
|
<CommonHeader />
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<main class="flex-1">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<CommonFooter />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
10
app/lib/utils.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { type ClassValue, clsx } from 'clsx'
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to merge Tailwind CSS classes
|
||||||
|
* Combines clsx for conditional classes and twMerge for Tailwind-specific merging
|
||||||
|
*/
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
68
app/pages/index.vue
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// Home page - MVP placeholder with shadcn-nuxt test
|
||||||
|
// Sample button click handler
|
||||||
|
const handleClick = () => {
|
||||||
|
console.log('shadcn-nuxt Button clicked!')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NuxtLayout name="default">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<h1 class="text-4xl font-bold mb-4">Welcome to experimenta Shop</h1>
|
||||||
|
<p class="text-lg mb-8 text-white/90">Your gateway to Makerspace annual passes and more.</p>
|
||||||
|
|
||||||
|
<div class="card-info mb-8">
|
||||||
|
<h2 class="text-xl font-semibold mb-2">MVP Development in Progress</h2>
|
||||||
|
<p>
|
||||||
|
This is a placeholder page. The full e-commerce functionality will be implemented in
|
||||||
|
upcoming phases.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- experimenta Button Showcase -->
|
||||||
|
<div class="card-glass mb-8">
|
||||||
|
<h3 class="text-xl font-semibold mb-4 text-experimenta-accent">experimenta Button</h3>
|
||||||
|
<p class="mb-6 text-white/90">
|
||||||
|
Der offizielle experimenta-Button mit animiertem Gradient-Effekt beim Hovern:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-4 items-center">
|
||||||
|
<UiButton variant="experimenta" size="experimenta" as="a" href="https://www.experimenta.science/">
|
||||||
|
Zur experimenta Startseite
|
||||||
|
</UiButton>
|
||||||
|
|
||||||
|
<UiButton variant="experimenta" size="experimenta" @click="handleClick">
|
||||||
|
Mit Click Handler
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-6 text-sm text-white/70">
|
||||||
|
Hinweis: Der Button hat einen animierten Gradient-Effekt von Pink zu Rot beim Hovern.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- shadcn-nuxt Button Component Test -->
|
||||||
|
<div class="card-glass">
|
||||||
|
<h3 class="text-xl font-semibold mb-4 text-experimenta-accent">shadcn-nuxt Components Test</h3>
|
||||||
|
<p class="mb-6 text-white/90">Testing shadcn-nuxt Button component integration:</p>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-4">
|
||||||
|
<UiButton variant="default" @click="handleClick"> Default Button </UiButton>
|
||||||
|
|
||||||
|
<UiButton variant="destructive" @click="handleClick"> Destructive </UiButton>
|
||||||
|
|
||||||
|
<UiButton variant="outline" @click="handleClick"> Outline </UiButton>
|
||||||
|
|
||||||
|
<UiButton variant="secondary" @click="handleClick"> Secondary </UiButton>
|
||||||
|
|
||||||
|
<UiButton variant="ghost" @click="handleClick"> Ghost </UiButton>
|
||||||
|
|
||||||
|
<UiButton variant="link" @click="handleClick"> Link </UiButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-6 text-sm text-white/70">Open browser console to see button click events.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NuxtLayout>
|
||||||
|
</template>
|
||||||
102
assets/css/main.css
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* experimenta Corporate Design CSS Variables */
|
||||||
|
:root {
|
||||||
|
/* experimenta Brand Colors */
|
||||||
|
--color-purple: #2e1065;
|
||||||
|
--color-purple-dark: #1a0a3a;
|
||||||
|
--color-purple-light: #3d1585;
|
||||||
|
|
||||||
|
--color-pink: #e6007e;
|
||||||
|
--color-pink-dark: #e40521;
|
||||||
|
--color-pink-light: #ff1a94;
|
||||||
|
|
||||||
|
--color-orange: #f59d24;
|
||||||
|
--color-orange-dark: #e88a0f;
|
||||||
|
--color-orange-light: #ffb649;
|
||||||
|
|
||||||
|
/* Primary Colors (legacy - keep for compatibility) */
|
||||||
|
--color-primary: #2e1065;
|
||||||
|
--color-primary-dark: #1a0a3a;
|
||||||
|
--color-primary-light: #3d1585;
|
||||||
|
|
||||||
|
/* Accent Colors */
|
||||||
|
--color-accent: #f59d24;
|
||||||
|
--color-accent-dark: #e88a0f;
|
||||||
|
--color-accent-light: #ffb649;
|
||||||
|
|
||||||
|
/* Neutral Colors */
|
||||||
|
--color-gray-50: #f9fafb;
|
||||||
|
--color-gray-100: #f3f4f6;
|
||||||
|
--color-gray-200: #e5e7eb;
|
||||||
|
--color-gray-300: #d1d5db;
|
||||||
|
--color-gray-400: #9ca3af;
|
||||||
|
--color-gray-500: #6b7280;
|
||||||
|
--color-gray-600: #4b5563;
|
||||||
|
--color-gray-700: #374151;
|
||||||
|
--color-gray-800: #1f2937;
|
||||||
|
--color-gray-900: #111827;
|
||||||
|
|
||||||
|
/* Semantic Colors */
|
||||||
|
--color-success: #10b981;
|
||||||
|
--color-warning: #f59e0b;
|
||||||
|
--color-error: #ef4444;
|
||||||
|
--color-info: #3b82f6;
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
--font-sans: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
--font-mono: 'Courier New', Courier, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Base styles */
|
||||||
|
body {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
color: var(--color-gray-900);
|
||||||
|
background-color: var(--color-gray-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode support (optional for future) */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--color-gray-50: #111827;
|
||||||
|
--color-gray-900: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--color-gray-900);
|
||||||
|
color: var(--color-gray-50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* experimenta Button with Gradient Animation */
|
||||||
|
.btn-experimenta {
|
||||||
|
background: var(--color-pink);
|
||||||
|
background-image: linear-gradient(to left, var(--color-pink), var(--color-pink), var(--color-pink-dark), var(--color-pink));
|
||||||
|
background-size: 300%;
|
||||||
|
background-position: 0%;
|
||||||
|
transition: background-position 1s ease, all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-experimenta:hover {
|
||||||
|
background-position: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-experimenta:focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-experimenta:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments for mobile */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.btn-experimenta {
|
||||||
|
padding: 8px 24px !important;
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
673
assets/css/tailwind.css
Normal file
@@ -0,0 +1,673 @@
|
|||||||
|
/**
|
||||||
|
* experimenta Design System - Tailwind CSS v4
|
||||||
|
*
|
||||||
|
* This file contains:
|
||||||
|
* - CSS Custom Properties for the experimenta theme
|
||||||
|
* - Tailwind base styles
|
||||||
|
* - Custom component classes
|
||||||
|
* - Utility classes for the design system
|
||||||
|
*/
|
||||||
|
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
CSS CUSTOM PROPERTIES
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
/* shadcn-ui CSS variables (HSL format) */
|
||||||
|
--background: 222.2 84% 4.9%;
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
--card: 222.2 84% 4.9%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
--popover: 222.2 84% 4.9%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
--primary: 210 40% 98%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
--accent: 217.2 32.6% 17.5%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
--ring: 212.7 26.8% 83.9%;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
|
||||||
|
/* Primary Colors */
|
||||||
|
--color-primary: #e6007e;
|
||||||
|
--color-primary-hover: #c2006a;
|
||||||
|
--color-primary-light: #ff4081;
|
||||||
|
|
||||||
|
--color-secondary: #e91e63;
|
||||||
|
--color-secondary-dark: #c2185b;
|
||||||
|
|
||||||
|
--color-accent: #f59d24;
|
||||||
|
--color-accent-hover: #ffb347;
|
||||||
|
|
||||||
|
--color-red: #e40521;
|
||||||
|
|
||||||
|
/* Purple Variants (Background) */
|
||||||
|
--color-purple-dark: #2e1065;
|
||||||
|
--color-purple-deeper: #1a0a3a;
|
||||||
|
--color-purple-darkest: #0f051d;
|
||||||
|
|
||||||
|
/* Semantic Colors */
|
||||||
|
--color-success: #46c74a;
|
||||||
|
--color-success-dark: #3ba83e;
|
||||||
|
|
||||||
|
--color-error: #e53e3e;
|
||||||
|
--color-error-dark: #c53030;
|
||||||
|
|
||||||
|
--color-warning: #f59d24;
|
||||||
|
--color-warning-dark: #dd8a1e;
|
||||||
|
|
||||||
|
--color-info: #4299e1;
|
||||||
|
--color-info-dark: #3182ce;
|
||||||
|
|
||||||
|
/* Background Gradients */
|
||||||
|
--bg-gradient-primary: linear-gradient(135deg, #2e1065 0%, #1a0a3a 50%, #0f051d 100%);
|
||||||
|
--bg-gradient-footer: linear-gradient(135deg, #1a0a3a 0%, #0f051d 100%);
|
||||||
|
--bg-gradient-glass: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(255, 255, 255, 0.1),
|
||||||
|
rgba(255, 255, 255, 0.05)
|
||||||
|
);
|
||||||
|
--bg-gradient-button: linear-gradient(to left, #e6007e, #e6007e, #e40521, #e6007e);
|
||||||
|
--bg-gradient-success: linear-gradient(90deg, #46c74a 0%, #66d96a 50%, #46c74a 100%);
|
||||||
|
|
||||||
|
/* Header & Footer */
|
||||||
|
--bg-header: rgba(46, 16, 101, 0.95);
|
||||||
|
|
||||||
|
/* Glass-morphism */
|
||||||
|
--glass-border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
--glass-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
||||||
|
|
||||||
|
/* Section Backgrounds */
|
||||||
|
--bg-section: rgba(255, 255, 255, 0.08);
|
||||||
|
--bg-section-hover: rgba(255, 255, 255, 0.15);
|
||||||
|
|
||||||
|
/* Text Colors */
|
||||||
|
--text-primary: #ffffff;
|
||||||
|
--text-secondary: rgba(255, 255, 255, 0.9);
|
||||||
|
--text-muted: rgba(255, 255, 255, 0.8);
|
||||||
|
--text-disabled: rgba(255, 255, 255, 0.5);
|
||||||
|
|
||||||
|
--text-dark: #333333;
|
||||||
|
--text-dark-secondary: #666666;
|
||||||
|
|
||||||
|
/* Border Colors */
|
||||||
|
--border-accent: 4px solid #f59d24;
|
||||||
|
--border-light: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
--border-footer: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
|
||||||
|
/* Spacing */
|
||||||
|
--space-1: 4px;
|
||||||
|
--space-2: 8px;
|
||||||
|
--space-3: 12px;
|
||||||
|
--space-4: 16px;
|
||||||
|
--space-5: 20px;
|
||||||
|
--space-6: 24px;
|
||||||
|
--space-8: 30px;
|
||||||
|
--space-10: 40px;
|
||||||
|
--space-15: 60px;
|
||||||
|
--space-20: 80px;
|
||||||
|
|
||||||
|
/* Border Radius */
|
||||||
|
--radius-sm: 6px;
|
||||||
|
--radius-md: 12px;
|
||||||
|
--radius-lg: 15px;
|
||||||
|
--radius-xl: 20px;
|
||||||
|
--radius-2xl: 25px;
|
||||||
|
|
||||||
|
/* Containers */
|
||||||
|
--container-main: 800px;
|
||||||
|
--container-wide: 1200px;
|
||||||
|
--container-full: 1760px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Base Styles */
|
||||||
|
* {
|
||||||
|
@apply box-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
@apply w-full overflow-x-hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply font-sans antialiased;
|
||||||
|
@apply bg-gradient-primary;
|
||||||
|
@apply text-white;
|
||||||
|
@apply min-h-screen w-full;
|
||||||
|
@apply overflow-x-hidden m-0 p-0;
|
||||||
|
background-attachment: fixed;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
h1 {
|
||||||
|
@apply text-4xl md:text-3xl sm:text-2xl;
|
||||||
|
@apply font-light tracking-tight;
|
||||||
|
@apply mb-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
@apply text-3xl md:text-2xl sm:text-xl;
|
||||||
|
@apply font-light;
|
||||||
|
@apply mb-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
@apply text-2xl md:text-xl sm:text-lg;
|
||||||
|
@apply font-normal;
|
||||||
|
@apply mb-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
@apply text-lg md:text-base sm:text-sm;
|
||||||
|
@apply leading-relaxed;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
@apply transition-colors duration-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus Styles (Accessibility) */
|
||||||
|
*:focus-visible {
|
||||||
|
@apply outline-none ring-2 ring-accent ring-offset-2;
|
||||||
|
ring-offset-color: var(--color-purple-darkest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
COMPONENT CLASSES
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
/* Buttons */
|
||||||
|
.btn-primary {
|
||||||
|
@apply px-8 py-2.5 rounded-2xl;
|
||||||
|
@apply text-lg font-medium text-white;
|
||||||
|
@apply cursor-pointer;
|
||||||
|
@apply transition-all duration-1000;
|
||||||
|
@apply outline-0 border-0;
|
||||||
|
background: var(--color-primary);
|
||||||
|
background-image: var(--bg-gradient-button);
|
||||||
|
background-size: 300%;
|
||||||
|
background-position: left;
|
||||||
|
line-height: 1.7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-position: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* experimenta Button (Official Design) */
|
||||||
|
.btn-experimenta {
|
||||||
|
@apply inline-block relative;
|
||||||
|
@apply px-[30px] py-[10px];
|
||||||
|
@apply text-lg font-medium text-white;
|
||||||
|
@apply rounded-[25px];
|
||||||
|
@apply cursor-pointer;
|
||||||
|
@apply border-0 outline-0;
|
||||||
|
@apply no-underline;
|
||||||
|
background: var(--color-primary);
|
||||||
|
background-image: var(--bg-gradient-button);
|
||||||
|
background-size: 300%;
|
||||||
|
background-position: 0%;
|
||||||
|
line-height: 1.7em;
|
||||||
|
transition: background-position 1s, all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-experimenta:hover {
|
||||||
|
background-position: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive button sizes */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.btn-primary {
|
||||||
|
@apply px-6 py-2 text-base;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-experimenta {
|
||||||
|
@apply px-6 py-2 text-base;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
@apply bg-transparent border-2 border-accent text-accent;
|
||||||
|
@apply px-8 py-2.5 rounded-2xl;
|
||||||
|
@apply text-lg font-medium;
|
||||||
|
@apply transition-all duration-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
@apply bg-accent text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards */
|
||||||
|
.card-glass {
|
||||||
|
background: var(--bg-gradient-glass);
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
@apply rounded-xl border shadow-glass;
|
||||||
|
border: var(--border-light);
|
||||||
|
@apply p-15 md:p-10 sm:p-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.card-glass {
|
||||||
|
@apply rounded-lg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info {
|
||||||
|
background: var(--bg-section);
|
||||||
|
@apply rounded-xl;
|
||||||
|
@apply p-8 md:p-6 sm:p-5;
|
||||||
|
@apply border-l-4 border-accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info h3 {
|
||||||
|
@apply text-accent font-medium mb-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info p {
|
||||||
|
@apply text-white/90;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Messages */
|
||||||
|
.status-message {
|
||||||
|
@apply text-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
@apply flex items-center justify-center;
|
||||||
|
@apply w-25 h-25 rounded-full;
|
||||||
|
@apply text-6xl text-white;
|
||||||
|
@apply mb-8;
|
||||||
|
@apply animate-pulse;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.status-icon {
|
||||||
|
@apply text-5xl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-success .status-icon {
|
||||||
|
@apply bg-success;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-error .status-icon {
|
||||||
|
@apply bg-error;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress Bar */
|
||||||
|
.progress-container {
|
||||||
|
background: var(--bg-section);
|
||||||
|
@apply rounded-2xl md:rounded-lg;
|
||||||
|
@apply p-8 md:p-6 sm:p-5;
|
||||||
|
@apply border-l-4 border-accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-header {
|
||||||
|
@apply flex justify-between items-center;
|
||||||
|
@apply flex-wrap gap-4;
|
||||||
|
@apply mb-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.progress-header {
|
||||||
|
@apply flex-col items-start gap-4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-title {
|
||||||
|
@apply text-xl md:text-lg sm:text-base;
|
||||||
|
@apply font-medium text-accent;
|
||||||
|
@apply m-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-stats {
|
||||||
|
@apply text-lg md:text-base sm:text-sm;
|
||||||
|
@apply text-white/90 font-normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-wrapper {
|
||||||
|
@apply relative w-full h-8 md:h-7 sm:h-6;
|
||||||
|
@apply bg-white/10 rounded-full;
|
||||||
|
@apply overflow-hidden;
|
||||||
|
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
@apply h-full rounded-full;
|
||||||
|
background: var(--bg-gradient-success);
|
||||||
|
@apply transition-all duration-500;
|
||||||
|
@apply animate-shimmer;
|
||||||
|
background-size: 200%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-percentage {
|
||||||
|
@apply absolute inset-0;
|
||||||
|
@apply flex items-center justify-center;
|
||||||
|
@apply text-base md:text-sm font-semibold text-white;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7);
|
||||||
|
@apply z-10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forms */
|
||||||
|
.form-field {
|
||||||
|
@apply mb-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
@apply block text-lg md:text-base font-medium text-white/90;
|
||||||
|
@apply mb-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
@apply w-full px-4 py-3;
|
||||||
|
@apply bg-white/10 border border-white/20;
|
||||||
|
@apply rounded-xl;
|
||||||
|
@apply text-white;
|
||||||
|
@apply transition-all duration-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input::placeholder {
|
||||||
|
@apply text-white/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:hover {
|
||||||
|
@apply bg-white/15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
@apply outline-none ring-2 ring-accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-checkbox {
|
||||||
|
@apply flex items-start gap-3 cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-input {
|
||||||
|
@apply w-5 h-5 mt-0.5;
|
||||||
|
@apply rounded border-2 border-white/30;
|
||||||
|
@apply bg-white/10;
|
||||||
|
@apply cursor-pointer;
|
||||||
|
@apply transition-all duration-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-input:checked {
|
||||||
|
@apply bg-accent border-accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-input:focus {
|
||||||
|
@apply ring-2 ring-accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
@apply text-base text-white/90;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Links */
|
||||||
|
.link-primary {
|
||||||
|
@apply text-primary underline;
|
||||||
|
@apply font-medium;
|
||||||
|
@apply transition-colors duration-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-primary:hover {
|
||||||
|
@apply text-primary-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-accent {
|
||||||
|
@apply text-accent underline;
|
||||||
|
@apply font-medium;
|
||||||
|
@apply transition-colors duration-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-accent:hover {
|
||||||
|
@apply text-accent-hover;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header & Footer */
|
||||||
|
.header-wrapper {
|
||||||
|
background: var(--bg-header);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
@apply relative z-50;
|
||||||
|
@apply py-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
@apply max-w-container-wide mx-auto;
|
||||||
|
@apply px-5;
|
||||||
|
@apply flex justify-center items-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
background: var(--bg-gradient-footer);
|
||||||
|
@apply mt-20;
|
||||||
|
@apply relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content {
|
||||||
|
@apply max-w-container-wide mx-auto;
|
||||||
|
@apply px-5 py-15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom {
|
||||||
|
border-top: var(--border-footer);
|
||||||
|
@apply pt-8;
|
||||||
|
@apply flex justify-between items-center;
|
||||||
|
@apply flex-wrap gap-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.footer-bottom {
|
||||||
|
@apply flex-col text-center gap-4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom p {
|
||||||
|
@apply text-white/80 text-base md:text-sm;
|
||||||
|
@apply font-normal m-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom-links {
|
||||||
|
@apply flex gap-8 md:gap-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom-links a {
|
||||||
|
@apply text-white/80 no-underline;
|
||||||
|
@apply text-base md:text-sm font-bold;
|
||||||
|
@apply transition-colors duration-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom-links a:hover {
|
||||||
|
@apply text-primary-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logo */
|
||||||
|
.logo {
|
||||||
|
@apply flex items-center no-underline text-white;
|
||||||
|
@apply transition-all duration-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-svg {
|
||||||
|
@apply w-[300px] md:w-[250px] sm:w-[200px];
|
||||||
|
@apply h-auto;
|
||||||
|
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Contact Info */
|
||||||
|
.contact-info {
|
||||||
|
background: var(--bg-section);
|
||||||
|
@apply rounded-xl;
|
||||||
|
@apply p-10 md:p-6 sm:p-5;
|
||||||
|
@apply text-center;
|
||||||
|
@apply border-l-4 border-accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-info p {
|
||||||
|
@apply text-lg md:text-base sm:text-sm;
|
||||||
|
@apply text-white/90;
|
||||||
|
@apply mb-8;
|
||||||
|
@apply leading-relaxed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item {
|
||||||
|
@apply m-5 p-0;
|
||||||
|
@apply bg-transparent border-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item strong {
|
||||||
|
@apply text-accent;
|
||||||
|
@apply block mb-3;
|
||||||
|
@apply text-lg md:text-base font-semibold;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item a {
|
||||||
|
@apply text-white no-underline;
|
||||||
|
@apply text-lg md:text-base font-normal;
|
||||||
|
@apply transition-colors duration-300;
|
||||||
|
word-break: break-all;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item a:hover {
|
||||||
|
@apply text-accent-hover;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Info Section */
|
||||||
|
.info-section {
|
||||||
|
background: var(--bg-section);
|
||||||
|
@apply rounded-xl;
|
||||||
|
@apply p-8 md:p-6 sm:p-5;
|
||||||
|
@apply text-center;
|
||||||
|
@apply border-l-4 border-accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section p {
|
||||||
|
@apply text-lg md:text-base sm:text-sm;
|
||||||
|
@apply text-white/90;
|
||||||
|
@apply m-0 leading-relaxed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section a {
|
||||||
|
@apply text-accent underline;
|
||||||
|
@apply font-medium;
|
||||||
|
@apply text-lg md:text-base sm:text-sm;
|
||||||
|
@apply transition-colors duration-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section a:hover {
|
||||||
|
@apply text-primary-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
UTILITY CLASSES
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
/* Container utilities */
|
||||||
|
.container-main {
|
||||||
|
@apply max-w-container-main mx-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-wide {
|
||||||
|
@apply max-w-container-wide mx-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-full {
|
||||||
|
@apply max-w-container-full mx-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Background utilities */
|
||||||
|
.bg-gradient-primary {
|
||||||
|
background: var(--bg-gradient-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-gradient-footer {
|
||||||
|
background: var(--bg-gradient-footer);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-gradient-glass {
|
||||||
|
background: var(--bg-gradient-glass);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-gradient-button {
|
||||||
|
background: var(--bg-gradient-button);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-gradient-success {
|
||||||
|
background: var(--bg-gradient-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-header {
|
||||||
|
background: var(--bg-header);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-section {
|
||||||
|
background: var(--bg-section);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-section-hover {
|
||||||
|
background: var(--bg-section-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Backdrop utilities */
|
||||||
|
.backdrop-blur-xl {
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Shadow utilities */
|
||||||
|
.shadow-glass {
|
||||||
|
box-shadow: var(--glass-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover effects */
|
||||||
|
.hover-scale {
|
||||||
|
@apply transition-transform duration-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-scale:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile-specific utilities */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.mobile-scroll-bg {
|
||||||
|
background-attachment: scroll !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text utilities */
|
||||||
|
.text-shadow-lg {
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive padding utilities */
|
||||||
|
.responsive-padding {
|
||||||
|
@apply p-15 md:p-10 sm:p-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.responsive-padding-section {
|
||||||
|
@apply p-8 md:p-6 sm:p-5;
|
||||||
|
}
|
||||||
|
}
|
||||||
20
components.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "default",
|
||||||
|
"typescript": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.ts",
|
||||||
|
"css": "assets/css/tailwind.css",
|
||||||
|
"baseColor": "slate",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "~/components",
|
||||||
|
"utils": "~/lib/utils",
|
||||||
|
"ui": "~/components/ui",
|
||||||
|
"lib": "~/lib",
|
||||||
|
"hooks": "~/hooks"
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide"
|
||||||
|
}
|
||||||
81
docker-compose.dev.yml
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# Docker Compose for Local Development
|
||||||
|
# This file provides PostgreSQL and Redis for local development
|
||||||
|
# The Nuxt app runs natively on your Mac for faster hot reloads
|
||||||
|
|
||||||
|
version: '3.9'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# PostgreSQL Database
|
||||||
|
db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: experimenta-db-dev
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- '5432:5432'
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: experimenta_dev
|
||||||
|
POSTGRES_USER: dev
|
||||||
|
POSTGRES_PASSWORD: dev_password_change_me
|
||||||
|
POSTGRES_INITDB_ARGS: '--encoding=UTF8 --locale=C'
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD-SHELL', 'pg_isready -U dev -d experimenta_dev']
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- experimenta-network
|
||||||
|
|
||||||
|
# Redis (for BullMQ queues, sessions, caching)
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: experimenta-redis-dev
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- '6379:6379'
|
||||||
|
command: >
|
||||||
|
redis-server
|
||||||
|
--appendonly yes
|
||||||
|
--appendfsync everysec
|
||||||
|
--save 60 1000
|
||||||
|
--save 300 100
|
||||||
|
--save 900 1
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', 'redis-cli', 'ping']
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- experimenta-network
|
||||||
|
|
||||||
|
# Optional: Drizzle Studio (Database GUI)
|
||||||
|
# Uncomment to enable database management UI at http://localhost:4983
|
||||||
|
# drizzle-studio:
|
||||||
|
# image: node:20-alpine
|
||||||
|
# container_name: experimenta-drizzle-studio
|
||||||
|
# working_dir: /app
|
||||||
|
# command: sh -c "npm install -g drizzle-kit && drizzle-kit studio --host 0.0.0.0 --port 4983"
|
||||||
|
# ports:
|
||||||
|
# - '4983:4983'
|
||||||
|
# environment:
|
||||||
|
# DATABASE_URL: postgresql://dev:dev_password_change_me@db:5432/experimenta_dev
|
||||||
|
# depends_on:
|
||||||
|
# - db
|
||||||
|
# networks:
|
||||||
|
# - experimenta-network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
name: experimenta-postgres-dev
|
||||||
|
driver: local
|
||||||
|
redis_data:
|
||||||
|
name: experimenta-redis-dev
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
experimenta-network:
|
||||||
|
name: experimenta-dev-network
|
||||||
|
driver: bridge
|
||||||
2241
docs/ARCHITECTURE.md
Normal file
2392
docs/CIDAAS_INTEGRATION.md
Normal file
923
docs/DESIGN_SYSTEM.md
Normal file
@@ -0,0 +1,923 @@
|
|||||||
|
# experimenta Design System
|
||||||
|
|
||||||
|
**Version:** 1.0
|
||||||
|
**Letzte Aktualisierung:** 2025-10-29
|
||||||
|
**Font:** Roboto (Open Source Alternative zu DIN OT)
|
||||||
|
|
||||||
|
Dieses Design System definiert die visuelle Identität und Komponenten-Bibliothek für **my.experimenta.science**. Es basiert auf dem Corporate Design der experimenta Science Center Website und den bereitgestellten Design-Vorlagen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Inhaltsverzeichnis
|
||||||
|
|
||||||
|
1. [Farbpalette](#farbpalette)
|
||||||
|
2. [Typografie](#typografie)
|
||||||
|
3. [Spacing & Layout](#spacing--layout)
|
||||||
|
4. [Komponenten](#komponenten)
|
||||||
|
5. [Animationen & Transitions](#animationen--transitions)
|
||||||
|
6. [Accessibility](#accessibility)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Farbpalette
|
||||||
|
|
||||||
|
### Primärfarben
|
||||||
|
|
||||||
|
**Experimenta Magenta** (Hauptfarbe)
|
||||||
|
|
||||||
|
```css
|
||||||
|
--color-primary: #e6007e;
|
||||||
|
--color-primary-hover: #c2006a;
|
||||||
|
--color-primary-light: #ff4081;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verwendung:** Primary Buttons, Links, aktive Zustände, Brand-Elemente
|
||||||
|
|
||||||
|
**Experimenta Pink**
|
||||||
|
|
||||||
|
```css
|
||||||
|
--color-secondary: #e91e63;
|
||||||
|
--color-secondary-dark: #c2185b;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verwendung:** Secondary Buttons, Footer Headings, Social Media Icons
|
||||||
|
|
||||||
|
**Experimenta Orange** (Akzent)
|
||||||
|
|
||||||
|
```css
|
||||||
|
--color-accent: #f59d24;
|
||||||
|
--color-accent-hover: #ffb347;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verwendung:** Border-Akzente (links), Hover-Effekte, Highlights, Labels
|
||||||
|
|
||||||
|
**Experimenta Rot** (Button-Gradient)
|
||||||
|
|
||||||
|
```css
|
||||||
|
--color-red: #e40521;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verwendung:** Button-Gradienten, Error-Zustände (zusammen mit Magenta)
|
||||||
|
|
||||||
|
### Background-Farben
|
||||||
|
|
||||||
|
**Dark Gradient** (Haupt-Background)
|
||||||
|
|
||||||
|
```css
|
||||||
|
--bg-gradient-primary: linear-gradient(135deg, #2e1065 0%, #1a0a3a 50%, #0f051d 100%);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Purple Variants**
|
||||||
|
|
||||||
|
```css
|
||||||
|
--color-purple-dark: #2e1065; /* Start des Gradienten */
|
||||||
|
--color-purple-deeper: #1a0a3a; /* Mitte des Gradienten */
|
||||||
|
--color-purple-darkest: #0f051d; /* Ende des Gradienten */
|
||||||
|
```
|
||||||
|
|
||||||
|
**Header Background**
|
||||||
|
|
||||||
|
```css
|
||||||
|
--bg-header: rgba(46, 16, 101, 0.95); /* Mit backdrop-filter: blur(10px) */
|
||||||
|
```
|
||||||
|
|
||||||
|
**Footer Background**
|
||||||
|
|
||||||
|
```css
|
||||||
|
--bg-footer: linear-gradient(135deg, #1a0a3a 0%, #0f051d 100%);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Glassmorphism (Cards & Containers)
|
||||||
|
|
||||||
|
```css
|
||||||
|
--glass-background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
|
||||||
|
--glass-backdrop: blur(15px);
|
||||||
|
--glass-border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
--glass-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Section Backgrounds (auf Glassmorphism Cards)**
|
||||||
|
|
||||||
|
```css
|
||||||
|
--bg-section: rgba(255, 255, 255, 0.08); /* Leicht heller als Card */
|
||||||
|
--bg-section-hover: rgba(255, 255, 255, 0.15);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Semantische Farben
|
||||||
|
|
||||||
|
**Success**
|
||||||
|
|
||||||
|
```css
|
||||||
|
--color-success: #46c74a;
|
||||||
|
--color-success-dark: #3ba83e;
|
||||||
|
--color-success-gradient: linear-gradient(90deg, #46c74a 0%, #66d96a 50%, #46c74a 100%);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error**
|
||||||
|
|
||||||
|
```css
|
||||||
|
--color-error: #e53e3e;
|
||||||
|
--color-error-dark: #c53030;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Warning**
|
||||||
|
|
||||||
|
```css
|
||||||
|
--color-warning: #f59d24;
|
||||||
|
--color-warning-dark: #dd8a1e;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Info**
|
||||||
|
|
||||||
|
```css
|
||||||
|
--color-info: #4299e1;
|
||||||
|
--color-info-dark: #3182ce;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Text-Farben
|
||||||
|
|
||||||
|
```css
|
||||||
|
--text-primary: #ffffff; /* Haupt-Text auf dunklem BG */
|
||||||
|
--text-secondary: rgba(255, 255, 255, 0.9); /* Sekundär-Text */
|
||||||
|
--text-muted: rgba(255, 255, 255, 0.8); /* Footer, Labels */
|
||||||
|
--text-disabled: rgba(255, 255, 255, 0.5); /* Disabled Elemente */
|
||||||
|
|
||||||
|
/* Text auf hellem Background (falls benötigt) */
|
||||||
|
--text-dark: #333333;
|
||||||
|
--text-dark-secondary: #666666;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Border-Farben
|
||||||
|
|
||||||
|
```css
|
||||||
|
--border-accent: 4px solid #f59d24; /* Links-Border für Sections */
|
||||||
|
--border-light: 1px solid rgba(255, 255, 255, 0.2); /* Card Borders */
|
||||||
|
--border-footer: 1px solid rgba(255, 255, 255, 0.1); /* Footer Divider */
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Typografie
|
||||||
|
|
||||||
|
### Font-Family
|
||||||
|
|
||||||
|
**Roboto** (Open Source, ähnlich zu DIN OT)
|
||||||
|
|
||||||
|
```css
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Import:**
|
||||||
|
|
||||||
|
```html
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Gewichte:**
|
||||||
|
|
||||||
|
- `300` - Light (Headlines)
|
||||||
|
- `400` - Regular (Body Text)
|
||||||
|
- `500` - Medium (Buttons, Labels)
|
||||||
|
- `700` - Bold (Footer Links, Strong Text)
|
||||||
|
|
||||||
|
### Font-Scale (Desktop)
|
||||||
|
|
||||||
|
| Element | Size | Weight | Line Height | Letter Spacing |
|
||||||
|
| -------------- | ---- | ------ | ----------- | -------------- |
|
||||||
|
| **H1** | 36px | 300 | 1.2 | -1px |
|
||||||
|
| **H2** | 30px | 300 | 1.3 | -0.5px |
|
||||||
|
| **H3** | 24px | 400 | 1.4 | 0 |
|
||||||
|
| **H4** | 20px | 500 | 1.4 | 0 |
|
||||||
|
| **H5** | 18px | 500 | 1.5 | 0.5px |
|
||||||
|
| **H6** | 16px | 600 | 1.5 | 0.5px |
|
||||||
|
| **Body** | 18px | 400 | 1.7 | 0 |
|
||||||
|
| **Body Small** | 16px | 400 | 1.6 | 0 |
|
||||||
|
| **Caption** | 14px | 400 | 1.6 | 0 |
|
||||||
|
| **Tiny** | 12px | 400 | 1.5 | 0 |
|
||||||
|
|
||||||
|
### Responsive Font-Scale
|
||||||
|
|
||||||
|
**Tablet (≤768px):**
|
||||||
|
|
||||||
|
```css
|
||||||
|
H1: 28px
|
||||||
|
H2: 24px
|
||||||
|
H3: 20px
|
||||||
|
Body: 16px
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mobile (≤480px):**
|
||||||
|
|
||||||
|
```css
|
||||||
|
H1: 24px
|
||||||
|
H2: 20px
|
||||||
|
H3: 18px
|
||||||
|
Body: 14px
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tailwind Klassen
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Headlines -->
|
||||||
|
<h1 class="text-4xl md:text-3xl sm:text-2xl font-light tracking-tight">
|
||||||
|
<h2 class="text-3xl md:text-2xl sm:text-xl font-light">
|
||||||
|
<h3 class="text-2xl md:text-xl sm:text-lg font-normal">
|
||||||
|
<!-- Body Text -->
|
||||||
|
<p class="text-lg md:text-base sm:text-sm leading-relaxed">
|
||||||
|
<!-- Labels -->
|
||||||
|
<span class="text-lg md:text-base sm:text-sm font-medium tracking-wide"></span>
|
||||||
|
</p>
|
||||||
|
</h3>
|
||||||
|
</h2>
|
||||||
|
</h1>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Spacing & Layout
|
||||||
|
|
||||||
|
### Spacing-System (8px Grid)
|
||||||
|
|
||||||
|
```css
|
||||||
|
--space-1: 4px;
|
||||||
|
--space-2: 8px;
|
||||||
|
--space-3: 12px;
|
||||||
|
--space-4: 16px;
|
||||||
|
--space-5: 20px;
|
||||||
|
--space-6: 24px;
|
||||||
|
--space-8: 30px;
|
||||||
|
--space-10: 40px;
|
||||||
|
--space-15: 60px;
|
||||||
|
--space-20: 80px;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container Max-Widths
|
||||||
|
|
||||||
|
```css
|
||||||
|
--container-main: 800px; /* Main Content (forms, text) */
|
||||||
|
--container-wide: 1200px; /* Footer, wide sections */
|
||||||
|
--container-full: 1760px; /* Full-width content (aus Website) */
|
||||||
|
```
|
||||||
|
|
||||||
|
### Border-Radius
|
||||||
|
|
||||||
|
```css
|
||||||
|
--radius-sm: 6px; /* Small elements (badges) */
|
||||||
|
--radius-md: 12px; /* Sections, Info-Cards */
|
||||||
|
--radius-lg: 15px; /* Standard Cards (mobile) */
|
||||||
|
--radius-xl: 20px; /* Standard Cards (desktop) */
|
||||||
|
--radius-2xl: 25px; /* Buttons */
|
||||||
|
--radius-full: 100%; /* Icons, Avatar */
|
||||||
|
```
|
||||||
|
|
||||||
|
### Breakpoints
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Mobile First */
|
||||||
|
--breakpoint-sm: 480px; /* Small phones */
|
||||||
|
--breakpoint-md: 768px; /* Tablets */
|
||||||
|
--breakpoint-lg: 1024px; /* Small desktops */
|
||||||
|
--breakpoint-xl: 1280px; /* Large desktops */
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verwendung in Tailwind:**
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Mobile First: base = mobile, md = tablet, lg = desktop -->
|
||||||
|
<div class="p-4 md:p-8 lg:p-15"></div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Standard Paddings
|
||||||
|
|
||||||
|
**Cards:**
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Desktop */
|
||||||
|
padding: 60px 40px;
|
||||||
|
|
||||||
|
/* Tablet */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
padding: 30px 15px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Sections:**
|
||||||
|
|
||||||
|
```css
|
||||||
|
padding: 30px;
|
||||||
|
|
||||||
|
/* Tablet */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: 25px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
padding: 20px 15px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Komponenten
|
||||||
|
|
||||||
|
### 1. Buttons
|
||||||
|
|
||||||
|
#### Primary Button
|
||||||
|
|
||||||
|
**Style:**
|
||||||
|
|
||||||
|
```css
|
||||||
|
background: #e6007e;
|
||||||
|
background-image: linear-gradient(to left, #e6007e, #e6007e, #e40521, #e6007e);
|
||||||
|
background-size: 300%;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 10px 30px;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition:
|
||||||
|
background-position 1s,
|
||||||
|
all 0.3s ease;
|
||||||
|
|
||||||
|
/* Hover */
|
||||||
|
background-position: 100%;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tailwind:**
|
||||||
|
|
||||||
|
```html
|
||||||
|
<button class="btn-primary">Zur experimenta Startseite</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
**CSS Klasse (Tailwind Config):**
|
||||||
|
|
||||||
|
```css
|
||||||
|
.btn-primary {
|
||||||
|
@apply bg-gradient-to-l from-primary via-red to-primary;
|
||||||
|
@apply bg-[length:300%] bg-left;
|
||||||
|
@apply text-white px-8 py-2.5 rounded-full;
|
||||||
|
@apply text-lg font-medium;
|
||||||
|
@apply transition-all duration-300;
|
||||||
|
@apply hover:bg-right;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Secondary Button
|
||||||
|
|
||||||
|
```html
|
||||||
|
<button class="btn-secondary">Abbrechen</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
.btn-secondary {
|
||||||
|
@apply bg-transparent border-2 border-accent text-accent;
|
||||||
|
@apply px-8 py-2.5 rounded-full;
|
||||||
|
@apply text-lg font-medium;
|
||||||
|
@apply transition-all duration-300;
|
||||||
|
@apply hover:bg-accent hover:text-white;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Button Sizes
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Large (default) -->
|
||||||
|
<button class="btn-primary text-lg px-8 py-2.5">
|
||||||
|
<!-- Medium -->
|
||||||
|
<button class="btn-primary text-base px-6 py-2">
|
||||||
|
<!-- Small (mobile) -->
|
||||||
|
<button class="btn-primary text-base px-6 py-2 md:px-8 md:py-2.5"></button>
|
||||||
|
</button>
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Cards
|
||||||
|
|
||||||
|
#### Glass-morphism Card (Main Pattern)
|
||||||
|
|
||||||
|
**Style:**
|
||||||
|
|
||||||
|
```css
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
||||||
|
padding: 60px 40px;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tailwind:**
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="card-glass">
|
||||||
|
<!-- Content -->
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**CSS Klasse:**
|
||||||
|
|
||||||
|
```css
|
||||||
|
.card-glass {
|
||||||
|
@apply bg-gradient-to-br from-white/10 to-white/5;
|
||||||
|
@apply backdrop-blur-xl;
|
||||||
|
@apply rounded-2xl border border-white/20;
|
||||||
|
@apply shadow-2xl;
|
||||||
|
@apply p-15 md:p-10 sm:p-8;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Info Section Card
|
||||||
|
|
||||||
|
**Style:**
|
||||||
|
|
||||||
|
```css
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 30px;
|
||||||
|
border-left: 4px solid #f59d24;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tailwind:**
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="card-info">
|
||||||
|
<h3 class="text-accent font-medium mb-4">Überschrift</h3>
|
||||||
|
<p class="text-white/90">Inhalt...</p>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**CSS Klasse:**
|
||||||
|
|
||||||
|
```css
|
||||||
|
.card-info {
|
||||||
|
@apply bg-white/8 rounded-xl;
|
||||||
|
@apply p-8 md:p-6 sm:p-5;
|
||||||
|
@apply border-l-4 border-accent;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Status Messages
|
||||||
|
|
||||||
|
#### Success Message
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="status-message status-success">
|
||||||
|
<div class="status-icon">✓</div>
|
||||||
|
<h1>Verlängerung erfolgreich!</h1>
|
||||||
|
<p>Ihre Jahreskarte wurde verlängert.</p>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**CSS:**
|
||||||
|
|
||||||
|
```css
|
||||||
|
.status-icon {
|
||||||
|
@apply flex items-center justify-center;
|
||||||
|
@apply w-25 h-25 rounded-full;
|
||||||
|
@apply text-6xl text-white;
|
||||||
|
@apply mb-8;
|
||||||
|
@apply animate-pulse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-success .status-icon {
|
||||||
|
@apply bg-success;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Error Message
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="status-message status-error">
|
||||||
|
<div class="status-icon">✖</div>
|
||||||
|
<h1>Ein Fehler ist aufgetreten</h1>
|
||||||
|
<p>Bitte versuchen Sie es erneut.</p>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
.status-error .status-icon {
|
||||||
|
@apply bg-error;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Progress Bar
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="progress-container">
|
||||||
|
<div class="progress-header">
|
||||||
|
<h3 class="progress-title">Verlängerungsfortschritt</h3>
|
||||||
|
<div class="progress-stats">5 / 10</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="progress-bar-wrapper">
|
||||||
|
<div class="progress-bar" style="width: 50%"></div>
|
||||||
|
<div class="progress-percentage">50%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**CSS:**
|
||||||
|
|
||||||
|
```css
|
||||||
|
.progress-container {
|
||||||
|
@apply bg-white/8 rounded-2xl;
|
||||||
|
@apply p-8 md:p-6;
|
||||||
|
@apply border-l-4 border-accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-wrapper {
|
||||||
|
@apply relative w-full h-8;
|
||||||
|
@apply bg-white/10 rounded-full;
|
||||||
|
@apply overflow-hidden;
|
||||||
|
@apply shadow-inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
@apply h-full rounded-full;
|
||||||
|
@apply bg-gradient-to-r from-success via-[#66d96a] to-success;
|
||||||
|
@apply bg-[length:200%] animate-shimmer;
|
||||||
|
@apply transition-all duration-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-percentage {
|
||||||
|
@apply absolute inset-0;
|
||||||
|
@apply flex items-center justify-center;
|
||||||
|
@apply text-base font-semibold text-white;
|
||||||
|
@apply drop-shadow-lg;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Forms
|
||||||
|
|
||||||
|
#### Input Field
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="email" class="form-label">E-Mail-Adresse</label>
|
||||||
|
<input type="email" id="email" class="form-input" placeholder="ihre.email@example.com" />
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**CSS:**
|
||||||
|
|
||||||
|
```css
|
||||||
|
.form-label {
|
||||||
|
@apply block text-lg font-medium text-white/90;
|
||||||
|
@apply mb-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
@apply w-full px-4 py-3;
|
||||||
|
@apply bg-white/10 border border-white/20;
|
||||||
|
@apply rounded-xl;
|
||||||
|
@apply text-white placeholder:text-white/50;
|
||||||
|
@apply focus:outline-none focus:ring-2 focus:ring-accent;
|
||||||
|
@apply transition-all duration-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:hover {
|
||||||
|
@apply bg-white/15;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Checkbox
|
||||||
|
|
||||||
|
```html
|
||||||
|
<label class="form-checkbox">
|
||||||
|
<input type="checkbox" class="checkbox-input" />
|
||||||
|
<span class="checkbox-label">Adresse für zukünftige Bestellungen speichern</span>
|
||||||
|
</label>
|
||||||
|
```
|
||||||
|
|
||||||
|
**CSS:**
|
||||||
|
|
||||||
|
```css
|
||||||
|
.form-checkbox {
|
||||||
|
@apply flex items-start gap-3 cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-input {
|
||||||
|
@apply w-5 h-5 mt-0.5;
|
||||||
|
@apply rounded border-2 border-white/30;
|
||||||
|
@apply bg-white/10;
|
||||||
|
@apply checked:bg-accent checked:border-accent;
|
||||||
|
@apply focus:ring-2 focus:ring-accent;
|
||||||
|
@apply cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
@apply text-base text-white/90;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Links
|
||||||
|
|
||||||
|
```html
|
||||||
|
<a href="#" class="link-primary">Mehr erfahren</a> <a href="#" class="link-accent">Hier klicken</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
**CSS:**
|
||||||
|
|
||||||
|
```css
|
||||||
|
.link-primary {
|
||||||
|
@apply text-primary underline;
|
||||||
|
@apply font-medium;
|
||||||
|
@apply transition-colors duration-300;
|
||||||
|
@apply hover:text-primary-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-accent {
|
||||||
|
@apply text-accent underline;
|
||||||
|
@apply font-medium;
|
||||||
|
@apply transition-colors duration-300;
|
||||||
|
@apply hover:text-accent-hover;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. Logo
|
||||||
|
|
||||||
|
**SVG Logo** (Experimenta X-Logo)
|
||||||
|
|
||||||
|
Wird in den Design-Vorlagen verwendet. Sollte als Vue-Komponente gespeichert werden:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- components/ExperimentaLogo.vue -->
|
||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
class="logo"
|
||||||
|
viewBox="0 0 382.94 87.17"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
>
|
||||||
|
<!-- SVG paths from design examples -->
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.logo {
|
||||||
|
width: 300px;
|
||||||
|
height: auto;
|
||||||
|
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.logo {
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.logo {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Animationen & Transitions
|
||||||
|
|
||||||
|
### Pulse (für Status Icons)
|
||||||
|
|
||||||
|
```css
|
||||||
|
@keyframes pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-pulse {
|
||||||
|
animation: pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shimmer (für Progress Bars)
|
||||||
|
|
||||||
|
```css
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-shimmer {
|
||||||
|
animation: shimmer 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Button Gradient Animation
|
||||||
|
|
||||||
|
```css
|
||||||
|
.btn-primary {
|
||||||
|
background-size: 300%;
|
||||||
|
background-position: left;
|
||||||
|
transition:
|
||||||
|
background-position 1s ease,
|
||||||
|
all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-position: right;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hover Transitions
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Standard Hover */
|
||||||
|
.hover-scale {
|
||||||
|
@apply transition-transform duration-300;
|
||||||
|
@apply hover:scale-105;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logo Hover */
|
||||||
|
.logo:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Social Links Hover */
|
||||||
|
.social-link:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Accessibility
|
||||||
|
|
||||||
|
### Farb-Kontraste
|
||||||
|
|
||||||
|
Alle Text-Farben wurden auf **WCAG AA Standard** getestet:
|
||||||
|
|
||||||
|
- **Weiß auf Dark Purple**: ✅ AAA (Kontrast > 7:1)
|
||||||
|
- **Magenta Buttons**: ✅ AA (Kontrast > 4.5:1)
|
||||||
|
- **Orange Akzente**: ✅ AA (Kontrast > 4.5:1)
|
||||||
|
|
||||||
|
### Focus States
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Alle interaktiven Elemente */
|
||||||
|
.focus-visible {
|
||||||
|
@apply focus:outline-none focus:ring-2 focus:ring-accent focus:ring-offset-2 focus:ring-offset-purple-darkest;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Screen Reader Support
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Buttons -->
|
||||||
|
<button aria-label="Zur experimenta Startseite zurückkehren">Zurück</button>
|
||||||
|
|
||||||
|
<!-- Status Messages -->
|
||||||
|
<div role="alert" aria-live="polite">
|
||||||
|
<p>Ihre Jahreskarte wurde erfolgreich verlängert.</p>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Keyboard Navigation
|
||||||
|
|
||||||
|
- **Tab-Reihenfolge**: Logisch von oben nach unten
|
||||||
|
- **Focus Indicators**: Sichtbarer Fokus-Ring auf allen interaktiven Elementen
|
||||||
|
- **Skip Links**: "Zum Hauptinhalt springen" Link am Anfang
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verwendung in Nuxt 4
|
||||||
|
|
||||||
|
### 1. Tailwind Config importieren
|
||||||
|
|
||||||
|
Siehe `tailwind.config.ts` für die vollständige Theme-Konfiguration.
|
||||||
|
|
||||||
|
### 2. Komponenten verwenden
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div class="card-glass">
|
||||||
|
<div class="status-icon bg-success">✓</div>
|
||||||
|
<h1 class="text-4xl md:text-3xl sm:text-2xl font-light tracking-tight mb-8">Erfolgreich!</h1>
|
||||||
|
<p class="text-lg md:text-base leading-relaxed text-white/90">Ihre Aktion war erfolgreich.</p>
|
||||||
|
|
||||||
|
<button class="btn-primary mt-8">Weiter</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. shadcn-nuxt Komponenten anpassen
|
||||||
|
|
||||||
|
Die shadcn-nuxt Komponenten können mit den experimenta-Farben überschrieben werden:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- components/ui/button/Button.vue -->
|
||||||
|
<Button class="btn-primary">Click me</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Beispiel: Complete Page Layout
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-gradient-primary">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="bg-header backdrop-blur-lg sticky top-0 z-50 py-8">
|
||||||
|
<div class="container-wide mx-auto px-5">
|
||||||
|
<ExperimentaLogo />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="container-main mx-auto px-5 py-10">
|
||||||
|
<div class="card-glass">
|
||||||
|
<!-- Success Icon -->
|
||||||
|
<div class="status-icon bg-success">✓</div>
|
||||||
|
|
||||||
|
<!-- Headline -->
|
||||||
|
<h1 class="text-4xl md:text-3xl sm:text-2xl font-light tracking-tight mb-8">
|
||||||
|
Verlängerung erfolgreich!
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<!-- Body Text -->
|
||||||
|
<p class="text-lg md:text-base leading-relaxed text-white/90 mb-6">
|
||||||
|
Ihre Pädagogische Jahreskarte wurde erfolgreich verlängert.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Info Section -->
|
||||||
|
<div class="card-info mt-8">
|
||||||
|
<h3 class="text-accent font-medium text-lg mb-4">Ihre Vorteile</h3>
|
||||||
|
<p class="text-white/90">
|
||||||
|
Mit der Jahreskarte erhalten Sie ein Jahr lang freien Eintritt...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Button -->
|
||||||
|
<button class="btn-primary mt-8">Zur experimenta Startseite</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="bg-footer mt-20">
|
||||||
|
<div class="container-wide mx-auto px-5 py-15">
|
||||||
|
<div
|
||||||
|
class="border-t border-white/10 pt-8 flex flex-wrap justify-between items-center gap-5"
|
||||||
|
>
|
||||||
|
<p class="text-white/80">
|
||||||
|
© 2025 experimenta gGmbH – Das Science Center. Alle Rechte vorbehalten.
|
||||||
|
</p>
|
||||||
|
<div class="flex gap-8">
|
||||||
|
<a href="#" class="link-primary">Kontakt</a>
|
||||||
|
<a href="#" class="link-primary">Impressum</a>
|
||||||
|
<a href="#" class="link-primary">Datenschutz</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Weiterführende Ressourcen
|
||||||
|
|
||||||
|
- **Roboto Font:** [Google Fonts](https://fonts.google.com/specimen/Roboto)
|
||||||
|
- **Tailwind CSS v4:** [Tailwind Docs](https://tailwindcss.com/docs)
|
||||||
|
- **shadcn-nuxt:** [shadcn-nuxt Docs](https://www.shadcn-vue.com/docs/installation/nuxt.html)
|
||||||
|
- **WCAG Accessibility:** [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Maintainer:** Experimenta Team
|
||||||
|
**Fragen?** → `docs@experimenta.science`
|
||||||
103
docs/EXPERIMENTA_BUTTON.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# experimenta Button Component
|
||||||
|
|
||||||
|
## Beschreibung
|
||||||
|
|
||||||
|
Der experimenta-Button ist eine spezielle Button-Variante, die das offizielle experimenta-Design umsetzt. Der Button verfügt über einen animierten Gradient-Effekt beim Hovern, der von Pink (#e6007e) zu Rot (#e40521) wechselt.
|
||||||
|
|
||||||
|
## Design-Eigenschaften
|
||||||
|
|
||||||
|
- **Farben**: Pink (#e6007e) zu Rot (#e40521) Gradient
|
||||||
|
- **Border-Radius**: 25px (abgerundete Ecken)
|
||||||
|
- **Padding**: 10px 30px (Desktop), 8px 24px (Mobile)
|
||||||
|
- **Font-Size**: 18px (Desktop), 16px (Mobile)
|
||||||
|
- **Hover-Effekt**: Animierter Gradient, der sich über 1 Sekunde von links nach rechts bewegt
|
||||||
|
- **Transition**: Smooth transition für alle Eigenschaften (0.3s ease)
|
||||||
|
|
||||||
|
## Verwendung
|
||||||
|
|
||||||
|
### Als Button
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<UiButtonButton variant="experimenta" size="experimenta" @click="handleClick">
|
||||||
|
Button Text
|
||||||
|
</UiButtonButton>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Als Link
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<UiButtonButton variant="experimenta" size="experimenta" as="a" href="https://www.experimenta.science/">
|
||||||
|
Zur experimenta Startseite
|
||||||
|
</UiButtonButton>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mit NuxtLink
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<UiButtonButton variant="experimenta" size="experimenta" as="NuxtLink" to="/some-page">
|
||||||
|
Interne Seite
|
||||||
|
</UiButtonButton>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
- **variant**: `"experimenta"` (erforderlich für das experimenta-Design)
|
||||||
|
- **size**: `"experimenta"` (empfohlen für das richtige Padding und die Schriftgröße)
|
||||||
|
- **as**: `"button"` (Standard) | `"a"` | `"NuxtLink"` - Element-Typ
|
||||||
|
- **href** / **to**: URL/Route (wenn `as="a"` oder `as="NuxtLink"`)
|
||||||
|
- **@click**: Event-Handler (wenn `as="button"`)
|
||||||
|
|
||||||
|
## Responsive Verhalten
|
||||||
|
|
||||||
|
Der Button passt sich automatisch an mobile Geräte an:
|
||||||
|
- **Desktop**: 18px Schriftgröße, 10px 30px Padding
|
||||||
|
- **Mobile (≤480px)**: 16px Schriftgröße, 8px 24px Padding
|
||||||
|
|
||||||
|
## Beispiel
|
||||||
|
|
||||||
|
Live-Beispiel auf der Startseite: `app/pages/index.vue`
|
||||||
|
|
||||||
|
## Technische Details
|
||||||
|
|
||||||
|
### Implementierung
|
||||||
|
|
||||||
|
Die Button-Komponente verwendet:
|
||||||
|
- `class-variance-authority` (CVA) für Varianten-Management
|
||||||
|
- `reka-ui` Primitive für Flexibilität
|
||||||
|
- Custom CSS für den Gradient-Effekt (`assets/css/main.css`)
|
||||||
|
- CSS Custom Properties für Farben (`--color-pink`, `--color-pink-dark`)
|
||||||
|
|
||||||
|
### Dateien
|
||||||
|
|
||||||
|
- **Komponente**: `app/components/ui/button/Button.vue`
|
||||||
|
- **Varianten**: `app/components/ui/button/index.ts`
|
||||||
|
- **Styles**: `assets/css/main.css` (`.btn-experimenta`)
|
||||||
|
|
||||||
|
## CSS-Implementierung
|
||||||
|
|
||||||
|
Der Gradient-Effekt wird mit Custom CSS implementiert:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.btn-experimenta {
|
||||||
|
background: var(--color-pink);
|
||||||
|
background-image: linear-gradient(to left, var(--color-pink), var(--color-pink), var(--color-pink-dark), var(--color-pink));
|
||||||
|
background-size: 300%;
|
||||||
|
background-position: 0%;
|
||||||
|
transition: background-position 1s ease, all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-experimenta:hover {
|
||||||
|
background-position: 100%;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Barrierefreiheit
|
||||||
|
|
||||||
|
Der Button unterstützt:
|
||||||
|
- ✅ Keyboard-Navigation (Tab, Enter, Space)
|
||||||
|
- ✅ Focus-Styles
|
||||||
|
- ✅ Screen-Reader (via Primitive-Komponente)
|
||||||
|
- ✅ Disabled-State
|
||||||
|
|
||||||
|
|
||||||
996
docs/PRD.md
Normal file
@@ -0,0 +1,996 @@
|
|||||||
|
# Product Requirements Document (PRD)
|
||||||
|
|
||||||
|
## my.experimenta.science E-Commerce App
|
||||||
|
|
||||||
|
**Version:** 1.0
|
||||||
|
**Datum:** 28. Oktober 2025
|
||||||
|
**Status:** Draft
|
||||||
|
**Autor:** experimenta Development Team
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Executive Summary
|
||||||
|
|
||||||
|
### 1.1 Projektvision
|
||||||
|
|
||||||
|
Die **my.experimenta.science** App ist eine moderne, komponentenbasierte E-Commerce-Plattform für das experimenta Science Center. Sie soll den bestehenden Webshop zunächst ergänzen.
|
||||||
|
|
||||||
|
### 1.2 Ziele
|
||||||
|
|
||||||
|
**Primäre Ziele:**
|
||||||
|
|
||||||
|
- Vereinfachter Online-Verkauf von Makerspace-Jahreskarten
|
||||||
|
- Mobile-first Nutzererlebnis mit exzellenter Desktop-Kompatibilität
|
||||||
|
- Nahtlose Integration mit bestehenden Systemen (NAV ERP, Cidaas)
|
||||||
|
- Skalierbare Architektur für zukünftige Produkterweiterungen
|
||||||
|
|
||||||
|
**Geschäftsziele:**
|
||||||
|
|
||||||
|
- Steigerung der Online-Verkäufe durch verbesserte UX
|
||||||
|
- Reduzierung manueller Prozesse durch Automatisierung
|
||||||
|
- Erhöhung der Kundenzufriedenheit
|
||||||
|
- Grundlage für digitale Transformation des Ticketing-Systems
|
||||||
|
|
||||||
|
### 1.3 Erfolgsmetriken
|
||||||
|
|
||||||
|
- Erfolgreiche Verkäufe von Makerspace-Jahreskarten
|
||||||
|
- Conversion Rate > 3%
|
||||||
|
- Page Load Time < 2 Sekunden (mobile)
|
||||||
|
- Fehlerfreie Synchronisation mit NAV ERP
|
||||||
|
- Positive Nutzerfeedbacks
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Produktübersicht
|
||||||
|
|
||||||
|
### 2.1 Was ist my.experimenta.science?
|
||||||
|
|
||||||
|
Eine spezialisierte E-Commerce-App, die es Besuchern des experimenta Science Centers ermöglicht, Produkte und Services online zu erwerben:
|
||||||
|
|
||||||
|
- Jahreskarten für den Makerspace
|
||||||
|
- Pädagogische Jahreskarten (Post-MVP)
|
||||||
|
- Experimenta-Tickets mit Platzreservierung (Post-MVP)
|
||||||
|
- Laborkurse für Schulen (Post-MVP)
|
||||||
|
|
||||||
|
### 2.2 Abgrenzung
|
||||||
|
|
||||||
|
**Im Scope (MVP):**
|
||||||
|
|
||||||
|
- Registrierung und Login
|
||||||
|
- Anzeige von Makerspace-Jahreskarten
|
||||||
|
- Warenkorb-Funktionalität
|
||||||
|
- Checkout-Prozess
|
||||||
|
- PayPal-Bezahlung
|
||||||
|
- NAV ERP Push-Integration
|
||||||
|
|
||||||
|
**Out of Scope (MVP):**
|
||||||
|
|
||||||
|
- Rollen-System (Pädagogen, Unternehmen)
|
||||||
|
- Pädagogische Jahreskarten
|
||||||
|
- Genehmigungsworkflows
|
||||||
|
- Platzreservierung
|
||||||
|
- Multi-Payment-Provider
|
||||||
|
- Laborkurse
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Zielgruppen
|
||||||
|
|
||||||
|
### 3.1 Primäre Zielgruppe (MVP)
|
||||||
|
|
||||||
|
**Privatpersonen:**
|
||||||
|
|
||||||
|
- Besucher des experimenta Science Centers
|
||||||
|
- Interessenten am Makerspace
|
||||||
|
- Alter: 18-65 Jahre
|
||||||
|
- Technikaffinität: mittel bis hoch
|
||||||
|
- Gerät: überwiegend Smartphone, teilweise Desktop
|
||||||
|
|
||||||
|
### 3.2 Zukünftige Zielgruppen (Post-MVP)
|
||||||
|
|
||||||
|
**Pädagogen/Erzieher:**
|
||||||
|
|
||||||
|
- Lehrkräfte an Schulen
|
||||||
|
- Erzieher in Kindergärten
|
||||||
|
- Benötigen Genehmigungsprozess für vergünstigte Tickets
|
||||||
|
|
||||||
|
**Unternehmen:**
|
||||||
|
|
||||||
|
- Firmen mit Interesse an Gruppenbesuchen
|
||||||
|
- Corporate Events
|
||||||
|
- Teambuilding-Maßnahmen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. User Stories & Use Cases
|
||||||
|
|
||||||
|
### 4.1 MVP User Stories
|
||||||
|
|
||||||
|
#### US-001: Benutzerregistrierung
|
||||||
|
|
||||||
|
**Als** Besucher
|
||||||
|
**möchte ich** mich mit meiner E-Mail-Adresse registrieren
|
||||||
|
**damit** ich Produkte kaufen kann
|
||||||
|
|
||||||
|
**Akzeptanzkriterien:**
|
||||||
|
|
||||||
|
- Registrierungsformular mit Feldern: E-Mail, Passwort, Vorname, Nachname
|
||||||
|
- Passwort-Anforderungen: mind. 8 Zeichen, Groß-/Kleinbuchstaben, Zahl
|
||||||
|
- Registrierung erfolgt über Cidaas Registration API
|
||||||
|
- Custom Registrierungs-Maske im experimenta Design (nicht Cidaas hosted)
|
||||||
|
- Bestätigungs-E-Mail wird von Cidaas versendet
|
||||||
|
- E-Mail muss bestätigt werden bevor Login möglich ist
|
||||||
|
- Nach erster Anmeldung wird User-Profil in lokaler DB angelegt (über OAuth2 Callback)
|
||||||
|
- User erhält Fehlermeldung wenn E-Mail bereits registriert
|
||||||
|
- Validierung: Client-seitig (UX) + Server-seitig (Sicherheit)
|
||||||
|
- Übersetzung in Deutsch und Englisch
|
||||||
|
|
||||||
|
**Technische Details:**
|
||||||
|
|
||||||
|
- Custom Registrierungsseite: `/auth?tab=register`
|
||||||
|
- POST `/api/auth/register` → Cidaas Registration API
|
||||||
|
- Response: "Bitte bestätigen Sie Ihre E-Mail"
|
||||||
|
- User-Profil wird bei erstem Login erstellt (nicht bei Registrierung)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-002: Benutzer-Login
|
||||||
|
|
||||||
|
**Als** registrierter Nutzer
|
||||||
|
**möchte ich** mich mit meinen Zugangsdaten anmelden
|
||||||
|
**damit** ich auf mein Profil und meine Bestellungen zugreifen kann
|
||||||
|
|
||||||
|
**Akzeptanzkriterien:**
|
||||||
|
|
||||||
|
- Custom Login-Maske im experimenta Design (nicht Cidaas hosted)
|
||||||
|
- Login erfolgt über OAuth2 Authorization Code Flow mit PKCE
|
||||||
|
- E-Mail-Adresse als Identifier (kein Benutzername)
|
||||||
|
- Weiterleitung zur Cidaas Login-Seite für Credential-Eingabe
|
||||||
|
- Nach erfolgreicher Authentifizierung: OAuth2 Callback
|
||||||
|
- User-Profil wird in lokaler DB erstellt (erste Anmeldung) oder aktualisiert
|
||||||
|
- Session wird erstellt (30 Tage Gültigkeit)
|
||||||
|
- Automatische Weiterleitung zur ursprünglich angeforderten Seite (oder Homepage)
|
||||||
|
- Bei fehlgeschlagenem Login: Klare Fehlermeldung
|
||||||
|
- Rate Limiting: Max. 5 Login-Versuche pro 15 Minuten
|
||||||
|
- "Passwort vergessen"-Link zu Cidaas Password Reset
|
||||||
|
|
||||||
|
**Technische Details:**
|
||||||
|
|
||||||
|
- Custom Login-Seite: `/auth?tab=login`
|
||||||
|
- OAuth2 Flow:
|
||||||
|
1. POST `/api/auth/login` → Redirect zu Cidaas
|
||||||
|
2. User authentifiziert sich bei Cidaas
|
||||||
|
3. Cidaas redirected zu `/api/auth/callback?code=xxx`
|
||||||
|
4. Server exchanged code für tokens
|
||||||
|
5. User-Profil wird aus Cidaas UserInfo geholt
|
||||||
|
6. User-Profil in DB erstellt/aktualisiert (via `experimenta_id`)
|
||||||
|
7. Encrypted session cookie gesetzt
|
||||||
|
8. Redirect zu Homepage
|
||||||
|
- Session: HTTP-only, Secure, SameSite=Lax, 30 Tage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-002a: Benutzer-Logout
|
||||||
|
|
||||||
|
**Als** angemeldeter Nutzer
|
||||||
|
**möchte ich** mich sicher abmelden
|
||||||
|
**damit** meine Daten geschützt bleiben (besonders auf gemeinsam genutzten Geräten)
|
||||||
|
|
||||||
|
**Akzeptanzkriterien:**
|
||||||
|
|
||||||
|
- Logout-Button ist im Benutzerm enü prominent platziert
|
||||||
|
- Klick auf Logout löscht die Session sofort
|
||||||
|
- User wird zur Homepage weitergeleitet
|
||||||
|
- Session-Cookie wird vollständig entfernt
|
||||||
|
- Nach Logout ist kein Zugriff auf geschützte Bereiche mehr möglich
|
||||||
|
- Optional: Single Sign-Out bei Cidaas (Logout aus allen Apps)
|
||||||
|
|
||||||
|
**Technische Details:**
|
||||||
|
|
||||||
|
- POST `/api/auth/logout`
|
||||||
|
- `clearUserSession(event)` löscht Session-Cookie
|
||||||
|
- Redirect zu `/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-002b: Session-Management
|
||||||
|
|
||||||
|
**Als** angemeldeter Nutzer
|
||||||
|
**möchte ich** dass meine Session sicher verwaltet wird
|
||||||
|
**damit** ich nicht nach jeder Interaktion neu anmelden muss, aber dennoch geschützt bin
|
||||||
|
|
||||||
|
**Akzeptanzkriterien:**
|
||||||
|
|
||||||
|
- Session bleibt 30 Tage gültig (configurable)
|
||||||
|
- Session wird bei jeder Interaktion automatisch verlängert (sliding expiration)
|
||||||
|
- Session-Cookie ist verschlüsselt (AES-256-GCM)
|
||||||
|
- Session-Cookie ist HTTP-only (nicht via JavaScript auslesbar)
|
||||||
|
- Session-Cookie nur über HTTPS übertragen (Secure flag)
|
||||||
|
- CSRF-Schutz durch SameSite=Lax Cookie-Attribut
|
||||||
|
- Nach Ablauf der Session: Automatisches Logout + Weiterleitung zu `/auth`
|
||||||
|
- User sieht Meldung: "Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an."
|
||||||
|
|
||||||
|
**Technische Details:**
|
||||||
|
|
||||||
|
- Session Implementierung: `nuxt-auth-utils` Module
|
||||||
|
- Cookie Name: `experimenta-session`
|
||||||
|
- Verschlüsselung: AES-256-GCM via `nuxt-auth-utils`
|
||||||
|
- Max-Age: 2592000 Sekunden (30 Tage)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-002c: Geschützte Bereiche
|
||||||
|
|
||||||
|
**Als** System
|
||||||
|
**möchte ich** dass bestimmte Bereiche nur für angemeldete Nutzer zugänglich sind
|
||||||
|
**damit** nicht-autorisierte Zugriffe verhindert werden
|
||||||
|
|
||||||
|
**Akzeptanzkriterien:**
|
||||||
|
|
||||||
|
- Geschützte Bereiche: Profil, Bestellhistorie, Checkout (teilweise)
|
||||||
|
- Unangemeldete User werden zu `/auth` weitergeleitet
|
||||||
|
- Nach erfolgreichem Login: Automatische Weiterleitung zur ursprünglich angeforderten Seite
|
||||||
|
- Ursprüngliche URL wird temporär gespeichert (max. 10 Minuten)
|
||||||
|
- API-Endpoints prüfen Session und geben 401 bei fehlender Authentifizierung
|
||||||
|
- Klare visuelle Kennzeichnung geschützter Bereiche (z.B. Schloss-Icon)
|
||||||
|
|
||||||
|
**Technische Details:**
|
||||||
|
|
||||||
|
- Middleware: `middleware/auth.ts`
|
||||||
|
- Usage: `definePageMeta({ middleware: 'auth' })`
|
||||||
|
- Redirect-Cookie: `redirect_after_login` (10min TTL)
|
||||||
|
- Protected API: `requireUserSession(event)` wirft 401
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-002d: Sicherheit & Rate Limiting
|
||||||
|
|
||||||
|
**Als** System
|
||||||
|
**möchte ich** dass Authentifizierungs-Endpoints vor Missbrauch geschützt sind
|
||||||
|
**damit** Brute-Force-Angriffe und Spam verhindert werden
|
||||||
|
|
||||||
|
**Akzeptanzkriterien:**
|
||||||
|
|
||||||
|
- Login: Max. 5 Versuche pro 15 Minuten pro IP-Adresse
|
||||||
|
- Registrierung: Max. 3 Versuche pro Stunde pro IP-Adresse
|
||||||
|
- Bei Überschreitung: HTTP 429 (Too Many Requests) mit Retry-After Header
|
||||||
|
- Klare Fehlermeldung: "Zu viele Versuche. Bitte versuchen Sie es in X Sekunden erneut."
|
||||||
|
- Rate Limit-Zähler wird bei erfolgreichem Login zurückgesetzt
|
||||||
|
- PKCE (Proof Key for Code Exchange) wird für OAuth2 verwendet
|
||||||
|
- State-Parameter schützt vor CSRF-Angriffen
|
||||||
|
- JWT-Tokens von Cidaas werden validiert (Signatur, Expiration, Issuer, Audience)
|
||||||
|
|
||||||
|
**Technische Details:**
|
||||||
|
|
||||||
|
- Rate Limiting Middleware: `server/middleware/rate-limit.ts`
|
||||||
|
- In-Memory Store für Rate Limits (Production: Redis empfohlen)
|
||||||
|
- PKCE: Code verifier (64 chars random) → SHA-256 Challenge
|
||||||
|
- State: 32 bytes random string, validiert im Callback
|
||||||
|
- JWT Validation: `jose` Library mit JWKS Caching
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-003: Makerspace-Jahreskarte ansehen
|
||||||
|
|
||||||
|
**Als** Nutzer
|
||||||
|
**möchte ich** Details zur Makerspace-Jahreskarte sehen
|
||||||
|
**damit** ich informiert eine Kaufentscheidung treffen kann
|
||||||
|
|
||||||
|
**Akzeptanzkriterien:**
|
||||||
|
|
||||||
|
- Produktseite zeigt: Name, Beschreibung, Preis, Bild
|
||||||
|
- Informationen kommen aus der lokalen DB (synchronisiert von NAV)
|
||||||
|
- Call-to-Action: "In den Warenkorb"
|
||||||
|
- Mobile-optimierte Darstellung
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-004: Produkt in den Warenkorb legen
|
||||||
|
|
||||||
|
**Als** Nutzer
|
||||||
|
**möchte ich** die Jahreskarte in den Warenkorb legen
|
||||||
|
**damit** ich sie später kaufen kann
|
||||||
|
|
||||||
|
**Akzeptanzkriterien:**
|
||||||
|
|
||||||
|
- Button "In den Warenkorb" ist prominent platziert
|
||||||
|
- Warenkorb-Icon zeigt Anzahl der Artikel
|
||||||
|
- Feedback nach Hinzufügen (z.B. Toast-Notification)
|
||||||
|
- Warenkorb ist persistent (auch nach Logout)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-005: Warenkorb ansehen
|
||||||
|
|
||||||
|
**Als** Nutzer
|
||||||
|
**möchte ich** meinen Warenkorb einsehen und bearbeiten
|
||||||
|
**damit** ich meine Bestellung überprüfen kann
|
||||||
|
|
||||||
|
**Akzeptanzkriterien:**
|
||||||
|
|
||||||
|
- Warenkorb zeigt alle hinzugefügten Artikel
|
||||||
|
- Menge kann angepasst werden
|
||||||
|
- Artikel können entfernt werden
|
||||||
|
- Gesamtpreis wird angezeigt
|
||||||
|
- Button "Zur Kasse" führt zum Checkout
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-006: Checkout durchführen
|
||||||
|
|
||||||
|
**Als** Nutzer
|
||||||
|
**möchte ich** meine Bestellung abschließen
|
||||||
|
**damit** ich die Jahreskarte erhalte
|
||||||
|
|
||||||
|
**Akzeptanzkriterien:**
|
||||||
|
|
||||||
|
- Übersicht der Bestellung (Artikel, Preis)
|
||||||
|
- Eingabe/Bestätigung von Rechnungsdaten
|
||||||
|
- Auswahl der Zahlungsmethode (MVP: nur PayPal)
|
||||||
|
- Weiterleitung zu PayPal
|
||||||
|
- Nach erfolgreicher Zahlung: Bestätigungsseite
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-007: Bestellbestätigung erhalten
|
||||||
|
|
||||||
|
**Als** Nutzer
|
||||||
|
**möchte ich** eine Bestätigung meiner Bestellung sehen
|
||||||
|
**damit** ich weiß, dass der Kauf erfolgreich war
|
||||||
|
|
||||||
|
**Akzeptanzkriterien:**
|
||||||
|
|
||||||
|
- Bestätigungsseite mit Bestellnummer
|
||||||
|
- E-Mail mit Bestelldetails und Jahreskarte (PDF/Link)
|
||||||
|
- Bestellung wird in der Bestellhistorie angezeigt
|
||||||
|
- Daten werden an NAV ERP übermittelt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-008: Gespeicherte Rechnungsadresse verwenden
|
||||||
|
|
||||||
|
**Als** wiederkehrender Nutzer
|
||||||
|
**möchte ich** meine Rechnungsadresse gespeichert haben
|
||||||
|
**damit** ich sie beim nächsten Kauf nicht erneut eingeben muss
|
||||||
|
|
||||||
|
**Akzeptanzkriterien:**
|
||||||
|
|
||||||
|
- Beim Checkout wird gespeicherte Adresse automatisch vorausgefüllt
|
||||||
|
- Option "Adresse für zukünftige Käufe speichern" ist beim ersten Kauf vorausgewählt
|
||||||
|
- Gespeicherte Adresse kann im Profil bearbeitet werden
|
||||||
|
- Adresse kann beim Checkout vor Abschluss editiert werden
|
||||||
|
- Im Profil unter `/profil/adresse` einsehbar und änderbar
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.2 Post-MVP User Stories
|
||||||
|
|
||||||
|
#### US-101: Rollenauswahl nach Registrierung
|
||||||
|
|
||||||
|
**Als** neuer Nutzer
|
||||||
|
**möchte ich** nach dem ersten Login meine Rolle auswählen
|
||||||
|
**damit** ich auf für mich relevante Produkte zugreifen kann
|
||||||
|
|
||||||
|
**Akzeptanzkriterien:**
|
||||||
|
|
||||||
|
- Modal/Seite zur Rollenauswahl nach erstem Login
|
||||||
|
- Optionen: Privatperson, Pädagoge/Erzieher, Unternehmen
|
||||||
|
- Auswahl wird im User-Profil gespeichert
|
||||||
|
- Pädagogen-Rolle löst Genehmigungsprozess aus
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-102: Pädagogische Jahreskarte beantragen
|
||||||
|
|
||||||
|
**Als** Pädagoge
|
||||||
|
**möchte ich** eine pädagogische Jahreskarte beantragen
|
||||||
|
**damit** ich die experimenta kostenlos besuchen kann
|
||||||
|
|
||||||
|
**Akzeptanzkriterien:**
|
||||||
|
|
||||||
|
- Antragsformular mit Nachweis (Schulnachweis, etc.)
|
||||||
|
- Status: "In Prüfung"
|
||||||
|
- Kann reservieren, aber nicht kaufen
|
||||||
|
- Benachrichtigung bei Genehmigung/Ablehnung
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Funktionale Anforderungen
|
||||||
|
|
||||||
|
### 5.1 Authentifizierung & Benutzerverwaltung
|
||||||
|
|
||||||
|
#### F-001: Cidaas Integration
|
||||||
|
|
||||||
|
- Integration mit Cidaas über OIDC/OAuth2
|
||||||
|
- Custom Registrierungs- und Login-Masken im experimenta Design
|
||||||
|
- E-Mail-basierte Registrierung (minimal)
|
||||||
|
- Session Management
|
||||||
|
|
||||||
|
#### F-002: User-Profil Verwaltung
|
||||||
|
|
||||||
|
- User-Profile werden in lokaler PostgreSQL-DB gespeichert
|
||||||
|
- Cidaas dient nur zur Authentifizierung
|
||||||
|
- Verknüpfung über Cidaas User-ID
|
||||||
|
- Profildaten: E-Mail, Name, Adresse (für Rechnungen)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.2 Produktverwaltung
|
||||||
|
|
||||||
|
#### F-003: Produkt-Synchronisation
|
||||||
|
|
||||||
|
- NAV ERP sendet Produktdaten per Push an Server-Endpunkt
|
||||||
|
- Endpunkt: `POST /api/erp/products`
|
||||||
|
- Produktdaten werden in lokaler DB gespeichert
|
||||||
|
- Felder: ID, Name, Beschreibung, Preis, Lagerbestand, Status
|
||||||
|
|
||||||
|
#### F-004: Produktanzeige
|
||||||
|
|
||||||
|
- Produktseite für Makerspace-Jahreskarten
|
||||||
|
- Responsives Layout (mobile-first)
|
||||||
|
- Bilder und Texte aus lokaler DB (synchronisiert via X-API)
|
||||||
|
- Preisanzeige in Euro
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.3 Warenkorb & Checkout
|
||||||
|
|
||||||
|
#### F-005: Warenkorb-Funktionalität
|
||||||
|
|
||||||
|
- Session-basierter Warenkorb für nicht-angemeldete User
|
||||||
|
- DB-persistenter Warenkorb für angemeldete User
|
||||||
|
- CRUD-Operationen: Hinzufügen, Entfernen, Mengenänderung
|
||||||
|
- Warenkorb-Icon mit Badge (Artikelanzahl)
|
||||||
|
|
||||||
|
#### F-006: Checkout-Prozess
|
||||||
|
|
||||||
|
- Schritt 1: Warenkorb-Übersicht
|
||||||
|
- Schritt 2: Rechnungsdaten
|
||||||
|
- Schritt 3: Zahlungsmethode (MVP: nur PayPal)
|
||||||
|
- Schritt 4: Bestellübersicht & Bestätigung
|
||||||
|
|
||||||
|
#### F-007: PayPal Integration
|
||||||
|
|
||||||
|
- PayPal Checkout Integration
|
||||||
|
- Redirect zu PayPal für Zahlung
|
||||||
|
- Webhook für Payment-Bestätigung
|
||||||
|
- Fehlerbehandlung bei fehlgeschlagener Zahlung
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.4 Bestellverwaltung
|
||||||
|
|
||||||
|
#### F-008: Bestellung speichern
|
||||||
|
|
||||||
|
- Bestellung wird nach erfolgreicher Zahlung in DB gespeichert
|
||||||
|
- Status: "Bezahlt", "In Bearbeitung", "Abgeschlossen"
|
||||||
|
- Bestellnummer generieren
|
||||||
|
- Timestamp & User-ID verknüpfen
|
||||||
|
|
||||||
|
#### F-009: Bestellbestätigung
|
||||||
|
|
||||||
|
- Bestätigungsseite nach erfolgreichem Kauf
|
||||||
|
- E-Mail mit Bestelldetails
|
||||||
|
- PDF-Ticket/Jahreskarte als Anhang oder Download-Link
|
||||||
|
|
||||||
|
#### F-010: Bestellhistorie
|
||||||
|
|
||||||
|
- User kann eigene Bestellungen einsehen
|
||||||
|
- Filtermöglichkeiten: Status, Datum
|
||||||
|
- Details-Ansicht pro Bestellung
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.5 ERP & API Integrationen
|
||||||
|
|
||||||
|
#### F-011: NAV ERP Push-Endpunkt
|
||||||
|
|
||||||
|
- REST-API Endpunkt: `POST /api/erp/products`
|
||||||
|
- Authentifizierung via API-Key oder OAuth
|
||||||
|
- Payload: Produktdaten (JSON)
|
||||||
|
- Validierung & Speicherung in DB
|
||||||
|
- Logging & Error Handling
|
||||||
|
|
||||||
|
#### F-012: X-API Integration
|
||||||
|
|
||||||
|
- Abruf von Veranstaltungstexten und Bildern
|
||||||
|
- Caching in lokaler DB
|
||||||
|
- Regelmäßige Synchronisation (Cronjob)
|
||||||
|
|
||||||
|
#### F-013: Bestellung an NAV senden (via X-API)
|
||||||
|
|
||||||
|
- Nach erfolgreichem Kauf: Bestellung an NAV übermitteln
|
||||||
|
- REST-API Call zu X-API Endpoint `/shopware/order`
|
||||||
|
- **Authentifizierung:** HTTP Basic Auth (Username + Password)
|
||||||
|
- X-API konvertiert JSON zu SOAP für NAV ERP
|
||||||
|
- Retry-Mechanismus bei Fehlern (exponentieller Backoff)
|
||||||
|
- Status-Tracking
|
||||||
|
- **Environments:**
|
||||||
|
- Development: `https://x-api-dev.experimenta.science`
|
||||||
|
- Staging: `https://x-api-stage.experimenta.science`
|
||||||
|
- Production: `https://x-api.experimenta.science`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Nicht-funktionale Anforderungen
|
||||||
|
|
||||||
|
### 6.1 Performance
|
||||||
|
|
||||||
|
- **Page Load Time:** < 2 Sekunden (mobile, 4G)
|
||||||
|
- **Time to Interactive:** < 3 Sekunden
|
||||||
|
- **API Response Time:** < 500ms (95th percentile)
|
||||||
|
- **Checkout Response:** < 1 Sekunde (nach PayPal Erfolg)
|
||||||
|
- **Concurrent Users:** 500+ gleichzeitige Nutzer
|
||||||
|
- **Queue Processing:** Order submission innerhalb 5 Minuten nach Payment
|
||||||
|
|
||||||
|
### 6.2 Skalierbarkeit
|
||||||
|
|
||||||
|
- Horizontal skalierbar (Docker Container)
|
||||||
|
- Stateless Server-Design
|
||||||
|
- DB Connection Pooling
|
||||||
|
- Redis für Caching-Strategie (Redis optional)
|
||||||
|
|
||||||
|
### 6.3 Sicherheit
|
||||||
|
|
||||||
|
- **HTTPS only** (TLS 1.3)
|
||||||
|
- **DSGVO-konform:** Datensparsamkeit, Einwilligungen, Löschkonzept
|
||||||
|
- **PCI-DSS-konform:** Keine Speicherung von Kreditkartendaten
|
||||||
|
- **Input Validation:** Alle User-Inputs validieren
|
||||||
|
- **Rate Limiting:** API-Endpunkte gegen Missbrauch schützen
|
||||||
|
- **Secrets Management:** Keine Secrets im Code (Environment Variables)
|
||||||
|
|
||||||
|
### 6.4 Verfügbarkeit
|
||||||
|
|
||||||
|
- **Uptime:** 99.5% (außer geplante Wartung)
|
||||||
|
- **Backup:** Tägliche DB-Backups
|
||||||
|
- **Disaster Recovery:** Wiederherstellung innerhalb 24h
|
||||||
|
|
||||||
|
### 6.5 Usability
|
||||||
|
|
||||||
|
- **Mobile-first Design:** Optimiert für Smartphones
|
||||||
|
- **Responsive:** Funktioniert auf allen Geräten (320px - 4K)
|
||||||
|
- **Accessibility:** WCAG 2.1 Level AA konform
|
||||||
|
- **Intuitive Navigation:** Maximal 3 Klicks zum Ziel
|
||||||
|
- **Corporate Design:** experimenta Styleguide (Farben, Fonts)
|
||||||
|
|
||||||
|
### 6.6 Wartbarkeit
|
||||||
|
|
||||||
|
- **Clean Code:** ESLint, Prettier
|
||||||
|
- **Dokumentation:** Inline-Kommentare, README
|
||||||
|
- **Testing:** Unit Tests (>80% Coverage), E2E Tests
|
||||||
|
- **CI/CD:** Automatisierte Builds & Deployments
|
||||||
|
- **Monitoring:** Logging, Error Tracking (Sentry optional)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. User Experience & Design
|
||||||
|
|
||||||
|
### 7.1 Design-Prinzipien
|
||||||
|
|
||||||
|
- **Mobile-first:** Primär für Smartphones optimiert
|
||||||
|
- **Minimal:** Fokus auf Kernfunktionen, keine Ablenkungen
|
||||||
|
- **Schnell:** Kurze Ladezeiten, optimierte Assets
|
||||||
|
- **Konsistent:** Einheitliches Design im gesamten System
|
||||||
|
|
||||||
|
### 7.2 Corporate Design Integration
|
||||||
|
|
||||||
|
- Farben aus experimenta Styleguide
|
||||||
|
- Schriftarten aus experimenta Styleguide
|
||||||
|
- Logo-Nutzung gemäß Brand Guidelines
|
||||||
|
- Icons: Material Design Icons oder Heroicons
|
||||||
|
|
||||||
|
### 7.3 Key Screens (MVP)
|
||||||
|
|
||||||
|
#### Homepage
|
||||||
|
|
||||||
|
- Hero-Bereich mit Call-to-Action
|
||||||
|
- Makerspace-Jahreskarte prominent anzeigen
|
||||||
|
- Login/Registrierung Button (Header)
|
||||||
|
- Warenkorb-Icon (Header)
|
||||||
|
|
||||||
|
#### Produktseite
|
||||||
|
|
||||||
|
- Großes Produktbild
|
||||||
|
- Name, Beschreibung, Preis
|
||||||
|
- "In den Warenkorb" Button (sticky)
|
||||||
|
- Zusätzliche Informationen (Accordion)
|
||||||
|
|
||||||
|
#### Warenkorb
|
||||||
|
|
||||||
|
- Liste der Artikel
|
||||||
|
- Menge anpassen, entfernen
|
||||||
|
- Gesamtpreis
|
||||||
|
- "Zur Kasse" Button
|
||||||
|
|
||||||
|
#### Checkout
|
||||||
|
|
||||||
|
- Multi-Step Form (Progress Indicator)
|
||||||
|
- Rechnungsdaten (Formular)
|
||||||
|
- Zahlungsmethode (PayPal Button)
|
||||||
|
- Bestellübersicht
|
||||||
|
|
||||||
|
#### Bestätigung
|
||||||
|
|
||||||
|
- Erfolgs-Icon
|
||||||
|
- Bestellnummer
|
||||||
|
- Zusammenfassung
|
||||||
|
- Link zur Bestellhistorie
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Technische Anforderungen
|
||||||
|
|
||||||
|
### 8.1 Tech Stack
|
||||||
|
|
||||||
|
Siehe [TECH_STACK.md](./TECH_STACK.md) für Details.
|
||||||
|
|
||||||
|
**Übersicht:**
|
||||||
|
|
||||||
|
- Frontend: Nuxt 4
|
||||||
|
- UI-Framework: Nuxt UI oder shadcn-vue
|
||||||
|
- Backend: Nuxt Server APIs
|
||||||
|
- Datenbank: PostgreSQL
|
||||||
|
- ORM: Drizzle
|
||||||
|
- Auth: Cidaas (OIDC/OAuth2)
|
||||||
|
- Payment: PayPal SDK
|
||||||
|
- Deployment: Docker, Hetzner Proxmox
|
||||||
|
- CI/CD: GitLab
|
||||||
|
|
||||||
|
### 8.2 Hosting & Infrastructure
|
||||||
|
|
||||||
|
- **Hosting:** Hetzner Dedicated Server / VPS
|
||||||
|
- **Virtualisierung:** Proxmox Container
|
||||||
|
- **Container Runtime:** Docker
|
||||||
|
- **Reverse Proxy:** Nginx oder Traefik
|
||||||
|
- **SSL:** Let's Encrypt (automatisch)
|
||||||
|
- **Domain:** my.experimenta.science
|
||||||
|
|
||||||
|
### 8.3 CI/CD Pipeline
|
||||||
|
|
||||||
|
- **Repository:** GitLab (intern gehostet)
|
||||||
|
- **Pipeline:** GitLab CI/CD
|
||||||
|
- **Deploy-Strategie:** Blue-Green oder Rolling Deployment
|
||||||
|
- **SSH-Zugang:** GitLab Runner mit SSH-Key oder Runner auf Server
|
||||||
|
- **Stages:** Build → Test → Deploy (Staging) → Deploy (Production)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Datenmodell (MVP)
|
||||||
|
|
||||||
|
### 9.1 Hauptentitäten
|
||||||
|
|
||||||
|
#### User
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
id: string(UUID)
|
||||||
|
cidaas_user_id: string(unique)
|
||||||
|
email: string
|
||||||
|
first_name: string ? last_name : string ? phone : string ? created_at : timestamp
|
||||||
|
updated_at: timestamp
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Product
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
id: string (UUID)
|
||||||
|
nav_product_id: string (unique)
|
||||||
|
name: string
|
||||||
|
description: text
|
||||||
|
price: decimal
|
||||||
|
image_url: string?
|
||||||
|
stock: integer
|
||||||
|
status: enum (active, inactive)
|
||||||
|
created_at: timestamp
|
||||||
|
updated_at: timestamp
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Cart
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
id: string (UUID)
|
||||||
|
user_id: string? (FK to User, null for anonymous)
|
||||||
|
session_id: string? (for anonymous users)
|
||||||
|
created_at: timestamp
|
||||||
|
updated_at: timestamp
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### CartItem
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
id: string (UUID)
|
||||||
|
cart_id: string (FK to Cart)
|
||||||
|
product_id: string (FK to Product)
|
||||||
|
quantity: integer
|
||||||
|
price_snapshot: decimal (Preis zum Zeitpunkt des Hinzufügens)
|
||||||
|
created_at: timestamp
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Order
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
id: string (UUID)
|
||||||
|
order_number: string (unique, z.B. "EXP-2025-00001")
|
||||||
|
user_id: string (FK to User)
|
||||||
|
status: enum (pending, paid, processing, completed, cancelled)
|
||||||
|
total_amount: decimal
|
||||||
|
payment_method: enum (paypal)
|
||||||
|
payment_id: string? (PayPal Transaction ID)
|
||||||
|
billing_address: jsonb
|
||||||
|
created_at: timestamp
|
||||||
|
updated_at: timestamp
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### OrderItem
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
id: string (UUID)
|
||||||
|
order_id: string (FK to Order)
|
||||||
|
product_id: string (FK to Product)
|
||||||
|
quantity: integer
|
||||||
|
price: decimal
|
||||||
|
created_at: timestamp
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. API-Spezifikation
|
||||||
|
|
||||||
|
### 10.1 Public API Endpoints
|
||||||
|
|
||||||
|
#### Authentication
|
||||||
|
|
||||||
|
- `POST /api/auth/login` - Initiiert Cidaas Login
|
||||||
|
- `POST /api/auth/callback` - OAuth Callback von Cidaas
|
||||||
|
- `POST /api/auth/logout` - Beendet Session
|
||||||
|
|
||||||
|
#### Products
|
||||||
|
|
||||||
|
- `GET /api/products` - Liste aller aktiven Produkte
|
||||||
|
- `GET /api/products/:id` - Details zu einem Produkt
|
||||||
|
|
||||||
|
#### Cart
|
||||||
|
|
||||||
|
- `GET /api/cart` - Warenkorb abrufen
|
||||||
|
- `POST /api/cart/items` - Artikel hinzufügen
|
||||||
|
- `PATCH /api/cart/items/:id` - Menge ändern
|
||||||
|
- `DELETE /api/cart/items/:id` - Artikel entfernen
|
||||||
|
|
||||||
|
#### Orders
|
||||||
|
|
||||||
|
- `POST /api/orders` - Bestellung erstellen
|
||||||
|
- `GET /api/orders` - Bestellhistorie
|
||||||
|
- `GET /api/orders/:id` - Bestelldetails
|
||||||
|
|
||||||
|
#### Payment
|
||||||
|
|
||||||
|
- `POST /api/payment/paypal/create` - PayPal Order erstellen
|
||||||
|
- `POST /api/payment/paypal/capture` - PayPal Zahlung erfassen
|
||||||
|
- `POST /api/payment/paypal/webhook` - PayPal Webhook
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10.2 Internal API Endpoints (ERP Integration)
|
||||||
|
|
||||||
|
#### NAV ERP Push
|
||||||
|
|
||||||
|
- `POST /api/erp/products` - Produkte von NAV empfangen
|
||||||
|
- `POST /api/erp/stock` - Lagerbestände aktualisieren
|
||||||
|
|
||||||
|
**Authentication:** API-Key oder OAuth Client Credentials
|
||||||
|
|
||||||
|
**Payload Example (Produkt):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nav_product_id": "MS-JK-2025",
|
||||||
|
"name": "Makerspace Jahreskarte 2025",
|
||||||
|
"description": "Jahresticket für unbegrenzten Zugang zum Makerspace",
|
||||||
|
"price": 99.0,
|
||||||
|
"stock": 500,
|
||||||
|
"status": "active",
|
||||||
|
"image_url": "https://api.experimenta.science/images/ms-jk.jpg"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Abhängigkeiten & Risiken
|
||||||
|
|
||||||
|
### 11.1 Externe Abhängigkeiten
|
||||||
|
|
||||||
|
- **Cidaas:** Verfügbarkeit und Stabilität der Auth-Platform
|
||||||
|
- **NAV ERP:** Zuverlässigkeit der Push-Integration
|
||||||
|
- **X-API:** Verfügbarkeit der Veranstaltungsdaten
|
||||||
|
- **PayPal:** Uptime des Payment-Gateways
|
||||||
|
- **Hetzner:** Infrastruktur-Verfügbarkeit
|
||||||
|
|
||||||
|
### 11.2 Technische Risiken
|
||||||
|
|
||||||
|
- **Cidaas Integration:** Komplexität der Custom-UI Integration
|
||||||
|
- **ERP Synchronisation:** Dateninkonsistenzen, Timing-Probleme
|
||||||
|
- **Payment Failures:** Umgang mit fehlgeschlagenen Transaktionen
|
||||||
|
- **Skalierung:** Performance bei hohem Nutzeraufkommen
|
||||||
|
|
||||||
|
### 11.3 Mitigationsstrategien
|
||||||
|
|
||||||
|
- **Cidaas:** Ausführliches Testing, Fallback-Mechanismen
|
||||||
|
- **ERP:** Retry-Logik, Monitoring, manuelle Sync-Option
|
||||||
|
- **Payment:** Klare Error-Messages, Support-Kontakt prominent
|
||||||
|
- **Skalierung:** Load Testing, horizontale Skalierung vorbereiten
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Zeitplan & Meilensteine
|
||||||
|
|
||||||
|
### 12.1 MVP (Phase 1) - 8-12 Wochen
|
||||||
|
|
||||||
|
**Sprint 1-2 (Woche 1-4): Foundation**
|
||||||
|
|
||||||
|
- Projekt-Setup (Nuxt 4, Drizzle, PostgreSQL)
|
||||||
|
- Datenbank-Schema erstellen
|
||||||
|
- Basic Layout & Routing
|
||||||
|
- Cidaas Integration (Auth)
|
||||||
|
|
||||||
|
**Sprint 3-4 (Woche 5-8): Core Features**
|
||||||
|
|
||||||
|
- Produktseite implementieren
|
||||||
|
- Warenkorb-Funktionalität
|
||||||
|
- NAV ERP Push-Endpunkt
|
||||||
|
- User-Profil
|
||||||
|
|
||||||
|
**Sprint 5-6 (Woche 9-12): Checkout & Payment**
|
||||||
|
|
||||||
|
- Checkout-Flow implementieren
|
||||||
|
- PayPal Integration
|
||||||
|
- Bestellbestätigung & E-Mail
|
||||||
|
- Testing & Bug Fixes
|
||||||
|
|
||||||
|
**Sprint 7 (Optional): Launch Preparation**
|
||||||
|
|
||||||
|
- UAT (User Acceptance Testing)
|
||||||
|
- Performance-Optimierung
|
||||||
|
- Dokumentation
|
||||||
|
- Production Deployment
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 12.2 Post-MVP Roadmap
|
||||||
|
|
||||||
|
**Phase 2 (Q2 2025): Pädagogen & Rollen**
|
||||||
|
|
||||||
|
- Rollen-System implementieren
|
||||||
|
- Pädagogische Jahreskarten
|
||||||
|
- Genehmigungsworkflow
|
||||||
|
- Admin-Panel (Basic)
|
||||||
|
|
||||||
|
**Phase 3 (Q3 2025): Experimenta-Tickets**
|
||||||
|
|
||||||
|
- Ticket-Varianten
|
||||||
|
- Science Dome Platzreservierung
|
||||||
|
- Kalender-Integration
|
||||||
|
- Multi-Payment-Provider
|
||||||
|
|
||||||
|
**Phase 4 (Q4 2025): Laborkurse**
|
||||||
|
|
||||||
|
- Kurs-Verwaltung
|
||||||
|
- Schulen-Accounts
|
||||||
|
- Gruppenbuchungen
|
||||||
|
- Erweiterte Reporting-Funktionen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Testing-Strategie
|
||||||
|
|
||||||
|
### 13.1 Test-Arten
|
||||||
|
|
||||||
|
**Unit Tests:**
|
||||||
|
|
||||||
|
- Nuxt Composables
|
||||||
|
- Utility Functions
|
||||||
|
- Drizzle Queries (Mocked)
|
||||||
|
- Ziel: >80% Coverage
|
||||||
|
|
||||||
|
**Integration Tests:**
|
||||||
|
|
||||||
|
- API Endpoints
|
||||||
|
- Database Operations
|
||||||
|
- Auth Flow
|
||||||
|
|
||||||
|
**E2E Tests:**
|
||||||
|
|
||||||
|
- User Flows (Registrierung bis Kauf)
|
||||||
|
- Payment Flow (mit Sandbox)
|
||||||
|
- Responsive Design
|
||||||
|
|
||||||
|
### 13.2 Test-Tools
|
||||||
|
|
||||||
|
- **Vitest:** Unit & Integration Tests
|
||||||
|
- **Playwright:** E2E Tests
|
||||||
|
- **MSW (Mock Service Worker):** API Mocking
|
||||||
|
- **Testing Library:** Component Tests
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Monitoring & Support
|
||||||
|
|
||||||
|
### 14.1 Monitoring
|
||||||
|
|
||||||
|
- **Application Monitoring:** Error Tracking (Sentry optional)
|
||||||
|
- **Performance Monitoring:** Core Web Vitals
|
||||||
|
- **Uptime Monitoring:** Ping-Service
|
||||||
|
- **Logging:** Strukturiertes Logging (JSON)
|
||||||
|
|
||||||
|
### 14.2 Support
|
||||||
|
|
||||||
|
- **E-Mail-Support:** support@experimenta.science
|
||||||
|
- **FAQ-Seite:** Häufige Fragen & Antworten
|
||||||
|
- **Status-Page:** System-Status & geplante Wartungen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Open Questions
|
||||||
|
|
||||||
|
### 15.1 Zu klären
|
||||||
|
|
||||||
|
- [ ] Welches UI-Framework: Nuxt UI vs. shadcn-vue?
|
||||||
|
- [ ] Detailliertes Design der Custom Cidaas Login/Registrierungs-UI
|
||||||
|
- [ ] Exakte Struktur der NAV ERP Push-Payload
|
||||||
|
- [ ] X-API Dokumentation & Zugang
|
||||||
|
- [ ] E-Mail-Versand: Eigener SMTP oder Service (SendGrid, etc.)?
|
||||||
|
- [ ] Ticket-Format: PDF, QR-Code, oder anderes?
|
||||||
|
- [ ] Admin-Panel: Ab wann benötigt?
|
||||||
|
|
||||||
|
### 15.2 Entscheidungen treffen
|
||||||
|
|
||||||
|
- [ ] GitLab Runner: Auf Server oder SSH-Zugang?
|
||||||
|
- [ ] Caching-Strategie: Redis verwenden?
|
||||||
|
- [ ] Staging-Environment: Separate Instanz?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 16. Glossar
|
||||||
|
|
||||||
|
| Begriff | Bedeutung |
|
||||||
|
| ---------------- | ---------------------------------------------------------- |
|
||||||
|
| **MVP** | Minimum Viable Product - Erste funktionsfähige Version |
|
||||||
|
| **NAV ERP** | Microsoft Dynamics NAV - ERP-System der experimenta |
|
||||||
|
| **X-API** | Externe API für Veranstaltungsdaten |
|
||||||
|
| **Cidaas** | Customer Identity and Access Management Platform von Widas |
|
||||||
|
| **OIDC** | OpenID Connect - Authentifizierungsprotokoll |
|
||||||
|
| **JK** | Jahreskarte |
|
||||||
|
| **MS** | Makerspace |
|
||||||
|
| **Päd. JK** | Pädagogische Jahreskarte |
|
||||||
|
| **Science Dome** | 150-Sitzer Kino/Planetarium der experimenta |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 17. Anhang
|
||||||
|
|
||||||
|
### 17.1 Referenzen
|
||||||
|
|
||||||
|
- [Nuxt 4 Dokumentation](https://nuxt.com)
|
||||||
|
- [Drizzle ORM](https://orm.drizzle.team)
|
||||||
|
- [Cidaas Dokumentation](https://docs.cidaas.com)
|
||||||
|
- [PayPal Developer Docs](https://developer.paypal.com)
|
||||||
|
|
||||||
|
### 17.2 Änderungshistorie
|
||||||
|
|
||||||
|
| Version | Datum | Autor | Änderungen |
|
||||||
|
| ------- | ---------- | -------- | ---------------------- |
|
||||||
|
| 1.0 | 2025-10-28 | Dev Team | Initiales PRD erstellt |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Ende des Dokuments**
|
||||||
1698
docs/TECH_STACK.md
Normal file
132
docs/design-examples/components/ExperimentaButton.vue
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* ExperimentaButton Component
|
||||||
|
*
|
||||||
|
* Experimenta-branded button with animated gradient background.
|
||||||
|
* Based on the experimenta Design System.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* <ExperimentaButton>Click me</ExperimentaButton>
|
||||||
|
* <ExperimentaButton variant="secondary">Cancel</ExperimentaButton>
|
||||||
|
* <ExperimentaButton size="small">Small</ExperimentaButton>
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** Button variant */
|
||||||
|
variant?: 'primary' | 'secondary'
|
||||||
|
/** Button size */
|
||||||
|
size?: 'small' | 'medium' | 'large'
|
||||||
|
/** Disabled state */
|
||||||
|
disabled?: boolean
|
||||||
|
/** Button type */
|
||||||
|
type?: 'button' | 'submit' | 'reset'
|
||||||
|
/** Link behavior (renders as <a> tag) */
|
||||||
|
href?: string
|
||||||
|
/** Target for links */
|
||||||
|
target?: '_blank' | '_self' | '_parent' | '_top'
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
variant: 'primary',
|
||||||
|
size: 'large',
|
||||||
|
disabled: false,
|
||||||
|
type: 'button',
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
click: [event: MouseEvent]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function handleClick(event: MouseEvent) {
|
||||||
|
emit('click', event)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component
|
||||||
|
:is="href ? 'a' : 'button'"
|
||||||
|
:href="href"
|
||||||
|
:target="href ? target : undefined"
|
||||||
|
:type="!href ? type : undefined"
|
||||||
|
:disabled="!href ? disabled : undefined"
|
||||||
|
:class="[
|
||||||
|
'btn-experimenta',
|
||||||
|
`btn-${variant}`,
|
||||||
|
`btn-${size}`,
|
||||||
|
{
|
||||||
|
'btn-disabled': disabled,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
@click="handleClick"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Base Button Styles */
|
||||||
|
.btn-experimenta {
|
||||||
|
@apply inline-block cursor-pointer;
|
||||||
|
@apply font-medium text-white;
|
||||||
|
@apply transition-all;
|
||||||
|
@apply outline-0 border-0;
|
||||||
|
@apply rounded-2xl;
|
||||||
|
text-decoration: none;
|
||||||
|
line-height: 1.7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Primary Variant */
|
||||||
|
.btn-primary {
|
||||||
|
background: #e6007e;
|
||||||
|
background-image: linear-gradient(to left, #e6007e, #e6007e, #e40521, #e6007e);
|
||||||
|
background-size: 300%;
|
||||||
|
background-position: left;
|
||||||
|
transition:
|
||||||
|
background-position 1s ease,
|
||||||
|
all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(.btn-disabled) {
|
||||||
|
background-position: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Secondary Variant */
|
||||||
|
.btn-secondary {
|
||||||
|
@apply bg-transparent border-2 border-accent text-accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover:not(.btn-disabled) {
|
||||||
|
@apply bg-accent text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sizes */
|
||||||
|
.btn-large {
|
||||||
|
@apply text-lg px-8 py-2.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-medium {
|
||||||
|
@apply text-base px-6 py-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-small {
|
||||||
|
@apply text-base px-6 py-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.btn-large {
|
||||||
|
@apply text-base px-6 py-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disabled State */
|
||||||
|
.btn-disabled {
|
||||||
|
@apply opacity-50 cursor-not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus State (Accessibility) */
|
||||||
|
.btn-experimenta:focus-visible {
|
||||||
|
@apply outline-none ring-2 ring-accent ring-offset-2;
|
||||||
|
ring-offset-color: var(--color-purple-darkest, #0f051d);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
115
docs/design-examples/components/ExperimentaCard.vue
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* ExperimentaCard Component
|
||||||
|
*
|
||||||
|
* Glass-morphism card component based on the experimenta Design System.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* <ExperimentaCard>Content here</ExperimentaCard>
|
||||||
|
* <ExperimentaCard variant="info">Info section</ExperimentaCard>
|
||||||
|
* <ExperimentaCard title="Card Title">Content</ExperimentaCard>
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** Card variant */
|
||||||
|
variant?: 'glass' | 'info' | 'contact' | 'progress'
|
||||||
|
/** Card title (optional) */
|
||||||
|
title?: string
|
||||||
|
/** Title color (only for info/contact variants) */
|
||||||
|
titleColor?: 'accent' | 'primary'
|
||||||
|
/** Show left accent border */
|
||||||
|
accentBorder?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
variant: 'glass',
|
||||||
|
titleColor: 'accent',
|
||||||
|
accentBorder: false,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'card-experimenta',
|
||||||
|
`card-${variant}`,
|
||||||
|
{
|
||||||
|
'card-accent-border': accentBorder || variant !== 'glass',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<!-- Title Slot or Prop -->
|
||||||
|
<h3 v-if="title || $slots.title" :class="['card-title', `title-${titleColor}`]">
|
||||||
|
<slot name="title">{{ title }}</slot>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Base Card Styles */
|
||||||
|
.card-experimenta {
|
||||||
|
@apply rounded-xl;
|
||||||
|
@apply p-8 md:p-6 sm:p-5;
|
||||||
|
@apply transition-all duration-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glass-morphism Variant (Main Card) */
|
||||||
|
.card-glass {
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
@apply border border-white/20;
|
||||||
|
@apply shadow-glass;
|
||||||
|
@apply rounded-2xl;
|
||||||
|
@apply p-15 md:p-10 sm:p-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.card-glass {
|
||||||
|
@apply rounded-lg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Info Variant */
|
||||||
|
.card-info {
|
||||||
|
@apply bg-white/8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Contact Variant */
|
||||||
|
.card-contact {
|
||||||
|
@apply bg-white/8;
|
||||||
|
@apply text-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress Variant */
|
||||||
|
.card-progress {
|
||||||
|
@apply bg-white/8;
|
||||||
|
@apply rounded-2xl md:rounded-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Accent Border (Left) */
|
||||||
|
.card-accent-border {
|
||||||
|
@apply border-l-4 border-accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card Title */
|
||||||
|
.card-title {
|
||||||
|
@apply font-medium mb-4;
|
||||||
|
@apply text-lg md:text-base;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-accent {
|
||||||
|
@apply text-accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-primary {
|
||||||
|
@apply text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover Effect (Optional) */
|
||||||
|
.card-experimenta:hover {
|
||||||
|
@apply shadow-2xl;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
216
docs/design-examples/components/ExperimentaLogo.vue
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* ExperimentaLogo Component
|
||||||
|
*
|
||||||
|
* Official experimenta Science Center logo (X-Logo with gradients).
|
||||||
|
* SVG is taken from the design templates.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* <ExperimentaLogo />
|
||||||
|
* <ExperimentaLogo size="small" />
|
||||||
|
* <ExperimentaLogo href="https://www.experimenta.science" />
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** Logo size */
|
||||||
|
size?: 'small' | 'medium' | 'large'
|
||||||
|
/** Link URL (if logo should be clickable) */
|
||||||
|
href?: string
|
||||||
|
/** Link target */
|
||||||
|
target?: '_blank' | '_self'
|
||||||
|
/** Accessible label */
|
||||||
|
ariaLabel?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
size: 'large',
|
||||||
|
href: undefined,
|
||||||
|
target: '_self',
|
||||||
|
ariaLabel: 'experimenta Science Center Logo',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component
|
||||||
|
:is="href ? 'a' : 'div'"
|
||||||
|
:href="href"
|
||||||
|
:target="href ? target : undefined"
|
||||||
|
:class="[
|
||||||
|
'logo-wrapper',
|
||||||
|
{
|
||||||
|
'logo-clickable': href,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:aria-label="ariaLabel"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
:class="['logo-svg', `logo-${size}`]"
|
||||||
|
viewBox="0 0 382.94 87.17"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
role="img"
|
||||||
|
:aria-label="ariaLabel"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<!-- Gradients for logo -->
|
||||||
|
<linearGradient
|
||||||
|
id="logo-gradient-a"
|
||||||
|
x1="102.63"
|
||||||
|
y1="152.32"
|
||||||
|
x2="135.19"
|
||||||
|
y2="191.11"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop offset="0.16" stop-color="#bf144c" />
|
||||||
|
<stop offset="0.29" stop-color="#ce0f60" />
|
||||||
|
<stop offset="0.47" stop-color="#de0b75" />
|
||||||
|
<stop offset="0.59" stop-color="#e4097d" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="logo-gradient-b"
|
||||||
|
x1="104.87"
|
||||||
|
y1="170.45"
|
||||||
|
x2="104.87"
|
||||||
|
y2="170.45"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop offset="0.16" stop-color="#bf144c" />
|
||||||
|
<stop offset="0.29" stop-color="#ab1a4e" />
|
||||||
|
<stop offset="0.43" stop-color="#9f1d4f" />
|
||||||
|
<stop offset="0.57" stop-color="#9b1e4f" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="logo-gradient-c"
|
||||||
|
x1="68.79"
|
||||||
|
y1="182.84"
|
||||||
|
x2="154.66"
|
||||||
|
y2="182.84"
|
||||||
|
xlink:href="#logo-gradient-b"
|
||||||
|
/>
|
||||||
|
<linearGradient
|
||||||
|
id="logo-gradient-d"
|
||||||
|
x1="94.04"
|
||||||
|
y1="182.21"
|
||||||
|
x2="114.5"
|
||||||
|
y2="126"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop offset="0.22" stop-color="#e4097d" />
|
||||||
|
<stop offset="0.32" stop-color="#e4115e" />
|
||||||
|
<stop offset="0.45" stop-color="#e5193d" />
|
||||||
|
<stop offset="0.55" stop-color="#e51e28" />
|
||||||
|
<stop offset="0.62" stop-color="#e52021" />
|
||||||
|
<stop offset="0.9" stop-color="#f7a822" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- X Logo -->
|
||||||
|
<polygon
|
||||||
|
points="143.78 50 151.18 39.6 144.43 39.6 139.68 46.33 135.13 39.6 127.79 39.6 135.32 50.08 127.29 61.23 134.09 61.23 139.3 53.78 144.43 61.23 151.59 61.23 143.78 50"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- "experimenta" Text -->
|
||||||
|
<path
|
||||||
|
d="M245.79,175.33a9.2,9.2,0,0,0-1.85-3.39,7.28,7.28,0,0,0-2.9-2,10.29,10.29,0,0,0-3.65-.63c-3.12,0-5.47.95-7,2.82l-.56-2.23h-7.72v5.29h3.13v24.7h6.13v-8.4a8.29,8.29,0,0,0,1.7.42,17.3,17.3,0,0,0,2.6.19,11.8,11.8,0,0,0,4.53-.82,9.29,9.29,0,0,0,3.39-2.39,10.78,10.78,0,0,0,2.11-3.78,15.69,15.69,0,0,0,.74-5A16,16,0,0,0,245.79,175.33Zm-12.76,0a5.26,5.26,0,0,1,2.73-.75,4,4,0,0,1,3.12,1.4,5.85,5.85,0,0,1,1.25,4,10.56,10.56,0,0,1-.4,3.12,5.84,5.84,0,0,1-1.1,2.09,4.11,4.11,0,0,1-1.62,1.18,7.15,7.15,0,0,1-4.21.12,4.71,4.71,0,0,1-1.43-.58v-8.48A3.79,3.79,0,0,1,233,175.35Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<!-- Additional text paths omitted for brevity - see design-example1.html for full SVG -->
|
||||||
|
|
||||||
|
<!-- Gradient Logo Mark (X with colors) -->
|
||||||
|
<path
|
||||||
|
d="M93.1,181.53l20.42-19.22c.87-.76,1.61-1.66,2.61-1.8,1.19-.17,3,1.09,3.3,1.28s10.7,6.64,12.57,7.77l15.75,9.71c4.25-1.29,7.2-4.59,7.46-7.92a5.92,5.92,0,0,0-3-5.65l-2.48-1.53h0s-17.5-10.32-22.43-13.18a22.83,22.83,0,0,0-11-3c-4.71.09-8.62,2.32-12.24,5h0l-24,18.2,3.64,4.7a16.1,16.1,0,0,0,6.48,4.83A17.88,17.88,0,0,0,93.1,181.53Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="url(#logo-gradient-a)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M104.87,170.45h0"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="url(#logo-gradient-b)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M93.1,181.53h0l11.77-11.08a3.11,3.11,0,0,1-1.35-.18,4.24,4.24,0,0,1-1.33-1.11l-7.57-8.93-14.57,11-7.26,5.5s-4.11,2.67-4,6.32c.15,4.65,5.34,6.69,5.34,6.69l30.63,13.13a35.93,35.93,0,0,0,11.38,2.53c3.39.08,6.83-1.14,10.49-2.24l24.76-8.46c1.26-.41,3.26-1.65,3.26-3.17,0-1.2-1.57-2.73-3.33-3.51a16.28,16.28,0,0,0-8.57-1.19c-4.7.44-25.95,7.65-25.95,7.65L93.16,184.23a1.28,1.28,0,0,1-.91-1.58A2.68,2.68,0,0,1,93.1,181.53Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="url(#logo-gradient-c)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M147.75,179.27c4.25-1.29,7.2-4.59,7.46-7.92a5.92,5.92,0,0,0-3-5.65l-2.48-1.53L132,169.56l-27.13.89h0a3.11,3.11,0,0,1-1.35-.18,4.24,4.24,0,0,1-1.33-1.11l-7.57-8.93-14.57,11,3.64,4.7a16.1,16.1,0,0,0,6.48,4.83,17.88,17.88,0,0,0,2.93.73h0a66.73,66.73,0,0,0,7.06.19C107.65,182,144.08,180.38,147.75,179.27Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#e4097d"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M104.87,170.45a3.26,3.26,0,0,1-1.35-.18,4.24,4.24,0,0,1-1.33-1.11l-7.67-9-3-3.55a1.94,1.94,0,0,1-.41-1.27,2.18,2.18,0,0,1,1-1.83L110,141.9a6.43,6.43,0,0,1,1.81-.87,5.82,5.82,0,0,1,1.86.09l16.49,2.64a24.38,24.38,0,0,0,9.39-.52c1.94-.49,4.31-1.25,5.39-2.91s-.6-2.9-1.11-3.27c-2.08-1.5-4.67-1.92-7.18-2.32l-25.48-4.08a23.84,23.84,0,0,0-10.88.57A19.61,19.61,0,0,0,95,133.6L71.44,149.36a6.34,6.34,0,0,0-1.08,9.38l.21.27,13.12,17a16.1,16.1,0,0,0,6.48,4.83,17.88,17.88,0,0,0,2.93.73Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="url(#logo-gradient-d)"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Logo Wrapper */
|
||||||
|
.logo-wrapper {
|
||||||
|
@apply flex items-center;
|
||||||
|
@apply transition-all duration-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-clickable {
|
||||||
|
@apply cursor-pointer no-underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-clickable:hover .logo-svg {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logo SVG */
|
||||||
|
.logo-svg {
|
||||||
|
@apply h-auto;
|
||||||
|
@apply transition-transform duration-300;
|
||||||
|
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logo Sizes */
|
||||||
|
.logo-large {
|
||||||
|
@apply w-[300px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-medium {
|
||||||
|
@apply w-[250px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-small {
|
||||||
|
@apply w-[200px];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.logo-large {
|
||||||
|
@apply w-[250px] max-w-[90%];
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-medium {
|
||||||
|
@apply w-[200px] max-w-[85%];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.logo-large {
|
||||||
|
@apply w-[200px] max-w-[85%];
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-medium {
|
||||||
|
@apply w-[180px] max-w-[80%];
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-small {
|
||||||
|
@apply w-[150px] max-w-[75%];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus State */
|
||||||
|
.logo-clickable:focus-visible {
|
||||||
|
@apply outline-none ring-2 ring-accent ring-offset-2;
|
||||||
|
ring-offset-color: var(--color-purple-darkest, #0f051d);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
125
docs/design-examples/components/ExperimentaStatusMessage.vue
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* ExperimentaStatusMessage Component
|
||||||
|
*
|
||||||
|
* Status message component with animated icon.
|
||||||
|
* Used for success, error, warning, and info states.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* <ExperimentaStatusMessage type="success" title="Erfolg!">
|
||||||
|
* Ihre Aktion war erfolgreich.
|
||||||
|
* </ExperimentaStatusMessage>
|
||||||
|
*
|
||||||
|
* <ExperimentaStatusMessage type="error" title="Fehler">
|
||||||
|
* Ein Fehler ist aufgetreten.
|
||||||
|
* </ExperimentaStatusMessage>
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** Status type */
|
||||||
|
type: 'success' | 'error' | 'warning' | 'info'
|
||||||
|
/** Status title */
|
||||||
|
title?: string
|
||||||
|
/** Custom icon (overrides default) */
|
||||||
|
icon?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
icon: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Default icons for each type
|
||||||
|
const defaultIcons = {
|
||||||
|
success: '✓',
|
||||||
|
error: '✖',
|
||||||
|
warning: '!',
|
||||||
|
info: 'i',
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayIcon = computed(() => props.icon || defaultIcons[props.type])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="status-message">
|
||||||
|
<!-- Animated Status Icon -->
|
||||||
|
<div :class="['status-icon', `status-icon-${type}`]" role="img" :aria-label="`${type} icon`">
|
||||||
|
{{ displayIcon }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<h1 v-if="title || $slots.title" class="status-title">
|
||||||
|
<slot name="title">{{ title }}</slot>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<!-- Message Content -->
|
||||||
|
<div class="status-content">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Status Message Container */
|
||||||
|
.status-message {
|
||||||
|
@apply text-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Icon */
|
||||||
|
.status-icon {
|
||||||
|
@apply flex items-center justify-center;
|
||||||
|
@apply w-25 h-25 rounded-full;
|
||||||
|
@apply text-6xl text-white;
|
||||||
|
@apply mb-8 mx-auto;
|
||||||
|
@apply animate-pulse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icon Colors by Type */
|
||||||
|
.status-icon-success {
|
||||||
|
@apply bg-success;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon-error {
|
||||||
|
@apply bg-error;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon-warning {
|
||||||
|
@apply bg-warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon-info {
|
||||||
|
@apply bg-info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Icon Size */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.status-icon {
|
||||||
|
@apply text-5xl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Title */
|
||||||
|
.status-title {
|
||||||
|
@apply text-4xl md:text-3xl sm:text-2xl;
|
||||||
|
@apply font-light tracking-tight;
|
||||||
|
@apply mb-8;
|
||||||
|
@apply text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Content */
|
||||||
|
.status-content {
|
||||||
|
@apply text-lg md:text-base sm:text-sm;
|
||||||
|
@apply text-white/90;
|
||||||
|
@apply leading-relaxed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pulse Animation Override (Custom) */
|
||||||
|
@keyframes pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
400
docs/design-examples/components/README.md
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
# experimenta Vue Komponenten-Beispiele
|
||||||
|
|
||||||
|
Dieser Ordner enthält **Referenz-Implementierungen** der experimenta Design System Komponenten als Vue 3 Single File Components (SFC).
|
||||||
|
|
||||||
|
Diese Komponenten dienen als **Vorlagen und Beispiele** für die Entwicklung eigener Komponenten oder können direkt in das Projekt kopiert werden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verfügbare Komponenten
|
||||||
|
|
||||||
|
### 1. ExperimentaButton.vue
|
||||||
|
|
||||||
|
Animierter Button mit Gradient-Hintergrund nach experimenta Design System.
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
- Primary & Secondary Variants
|
||||||
|
- Responsive Größen (Small, Medium, Large)
|
||||||
|
- Link-Verhalten (kann als `<a>` oder `<button>` gerendert werden)
|
||||||
|
- Hover-Animation mit Gradient-Shift
|
||||||
|
- Accessibility-Ready (Focus States, ARIA Labels)
|
||||||
|
|
||||||
|
**Verwendung:**
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import ExperimentaButton from './ExperimentaButton.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- Primary Button (default) -->
|
||||||
|
<ExperimentaButton @click="handleClick"> Zur Startseite </ExperimentaButton>
|
||||||
|
|
||||||
|
<!-- Secondary Button -->
|
||||||
|
<ExperimentaButton variant="secondary"> Abbrechen </ExperimentaButton>
|
||||||
|
|
||||||
|
<!-- As Link -->
|
||||||
|
<ExperimentaButton href="https://www.experimenta.science" target="_blank">
|
||||||
|
Zur experimenta Website
|
||||||
|
</ExperimentaButton>
|
||||||
|
|
||||||
|
<!-- Small Size -->
|
||||||
|
<ExperimentaButton size="small"> Small Button </ExperimentaButton>
|
||||||
|
|
||||||
|
<!-- Disabled -->
|
||||||
|
<ExperimentaButton disabled> Disabled Button </ExperimentaButton>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. ExperimentaCard.vue
|
||||||
|
|
||||||
|
Glass-morphism Card-Komponente mit verschiedenen Variants.
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
- Glass-morphism Styling (Backdrop Blur)
|
||||||
|
- Info, Contact, Progress Variants
|
||||||
|
- Optional: Akzent-Border (links)
|
||||||
|
- Slot-basiertes Design (flexibel)
|
||||||
|
|
||||||
|
**Verwendung:**
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import ExperimentaCard from './ExperimentaCard.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- Glass Card (Main) -->
|
||||||
|
<ExperimentaCard>
|
||||||
|
<h1>Willkommen!</h1>
|
||||||
|
<p>Dies ist eine Glass-morphism Card.</p>
|
||||||
|
</ExperimentaCard>
|
||||||
|
|
||||||
|
<!-- Info Card mit Titel -->
|
||||||
|
<ExperimentaCard variant="info" title="Ihre Vorteile">
|
||||||
|
<p>Mit der Jahreskarte erhalten Sie...</p>
|
||||||
|
</ExperimentaCard>
|
||||||
|
|
||||||
|
<!-- Card mit Custom Title Slot -->
|
||||||
|
<ExperimentaCard variant="contact">
|
||||||
|
<template #title>
|
||||||
|
<span>Kontakt</span>
|
||||||
|
</template>
|
||||||
|
<p>E-Mail: info@experimenta.science</p>
|
||||||
|
</ExperimentaCard>
|
||||||
|
|
||||||
|
<!-- Progress Card -->
|
||||||
|
<ExperimentaCard variant="progress">
|
||||||
|
<!-- Progress Bar Content -->
|
||||||
|
</ExperimentaCard>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. ExperimentaStatusMessage.vue
|
||||||
|
|
||||||
|
Status-Nachrichten mit animierten Icons (Success, Error, Warning, Info).
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
- 4 Status-Typen mit passenden Farben
|
||||||
|
- Animiertes Icon (Pulse Animation)
|
||||||
|
- Responsive Icon-Größe
|
||||||
|
- Slot-basierte Inhalte
|
||||||
|
|
||||||
|
**Verwendung:**
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import ExperimentaStatusMessage from './ExperimentaStatusMessage.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- Success Message -->
|
||||||
|
<ExperimentaStatusMessage type="success" title="Verlängerung erfolgreich!">
|
||||||
|
<p>Ihre Jahreskarte wurde erfolgreich verlängert.</p>
|
||||||
|
</ExperimentaStatusMessage>
|
||||||
|
|
||||||
|
<!-- Error Message -->
|
||||||
|
<ExperimentaStatusMessage type="error" title="Ein Fehler ist aufgetreten">
|
||||||
|
<p>Bitte versuchen Sie es erneut.</p>
|
||||||
|
</ExperimentaStatusMessage>
|
||||||
|
|
||||||
|
<!-- Custom Icon -->
|
||||||
|
<ExperimentaStatusMessage type="warning" title="Achtung" icon="⚠">
|
||||||
|
<p>Dies ist eine Warnung.</p>
|
||||||
|
</ExperimentaStatusMessage>
|
||||||
|
|
||||||
|
<!-- Custom Title Slot -->
|
||||||
|
<ExperimentaStatusMessage type="info">
|
||||||
|
<template #title>
|
||||||
|
<span>Information</span>
|
||||||
|
</template>
|
||||||
|
<p>Hier ist eine Info.</p>
|
||||||
|
</ExperimentaStatusMessage>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. ExperimentaLogo.vue
|
||||||
|
|
||||||
|
Offizielles experimenta X-Logo mit Farbverläufen.
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
- SVG-basiert (skalierbar, scharf)
|
||||||
|
- 3 Größen (Small, Medium, Large)
|
||||||
|
- Responsive (passt sich automatisch an)
|
||||||
|
- Optional als Link verwendbar
|
||||||
|
- Hover-Animation
|
||||||
|
|
||||||
|
**Verwendung:**
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import ExperimentaLogo from './ExperimentaLogo.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- Logo (default: large) -->
|
||||||
|
<ExperimentaLogo />
|
||||||
|
|
||||||
|
<!-- Logo als Link -->
|
||||||
|
<ExperimentaLogo href="https://www.experimenta.science" target="_blank" />
|
||||||
|
|
||||||
|
<!-- Small Logo -->
|
||||||
|
<ExperimentaLogo size="small" />
|
||||||
|
|
||||||
|
<!-- Medium Logo -->
|
||||||
|
<ExperimentaLogo size="medium" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration in Nuxt 4
|
||||||
|
|
||||||
|
### Option 1: Komponenten in `components/` verschieben
|
||||||
|
|
||||||
|
Kopiere die gewünschten Komponenten nach `components/`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp docs/design-examples/components/ExperimentaButton.vue components/
|
||||||
|
```
|
||||||
|
|
||||||
|
Nuxt erkennt sie automatisch (Auto-Imports):
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<ExperimentaButton>Click me</ExperimentaButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Option 2: Als Composable verwenden
|
||||||
|
|
||||||
|
Erstelle eine Composable-Funktion in `composables/useExperimenta.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function useExperimenta() {
|
||||||
|
return {
|
||||||
|
// Export component references
|
||||||
|
ExperimentaButton: () => import('@/docs/design-examples/components/ExperimentaButton.vue'),
|
||||||
|
ExperimentaCard: () => import('@/docs/design-examples/components/ExperimentaCard.vue'),
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Anpassungen & Erweiterungen
|
||||||
|
|
||||||
|
### shadcn-nuxt Integration
|
||||||
|
|
||||||
|
Diese Komponenten können **shadcn-nuxt Komponenten ersetzen** oder ergänzen:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- Statt shadcn Button: -->
|
||||||
|
<Button>Click me</Button>
|
||||||
|
|
||||||
|
<!-- Verwende experimenta Button: -->
|
||||||
|
<ExperimentaButton>Click me</ExperimentaButton>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tailwind Klassen verwenden
|
||||||
|
|
||||||
|
Alle Komponenten verwenden Tailwind CSS Utilities. Du kannst sie anpassen:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<ExperimentaButton class="mt-8">
|
||||||
|
Custom Margin
|
||||||
|
</ExperimentaButton>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Variants hinzufügen
|
||||||
|
|
||||||
|
Beispiel: Eine neue Button-Variant hinzufügen:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- In ExperimentaButton.vue -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
variant?: 'primary' | 'secondary' | 'tertiary' // Neu: tertiary
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Neue Variant definieren */
|
||||||
|
.btn-tertiary {
|
||||||
|
@apply bg-info text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-tertiary:hover {
|
||||||
|
@apply bg-info-dark;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TypeScript Support
|
||||||
|
|
||||||
|
Alle Komponenten sind **TypeScript-ready** mit vollständigen Prop-Definitionen.
|
||||||
|
|
||||||
|
Beispiel für Type-Safe Usage:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ExperimentaButton from './ExperimentaButton.vue'
|
||||||
|
|
||||||
|
function handleClick(event: MouseEvent) {
|
||||||
|
console.log('Button clicked', event)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ExperimentaButton variant="primary" size="large" :disabled="false" @click="handleClick">
|
||||||
|
Click me
|
||||||
|
</ExperimentaButton>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Accessibility (A11y)
|
||||||
|
|
||||||
|
Alle Komponenten folgen **WCAG 2.1 AA Standards**:
|
||||||
|
|
||||||
|
- ✅ **Keyboard Navigation** (Tab, Enter, Space)
|
||||||
|
- ✅ **Focus Indicators** (sichtbarer Focus-Ring)
|
||||||
|
- ✅ **ARIA Labels** (Screen Reader Support)
|
||||||
|
- ✅ **Color Contrast** (mindestens 4.5:1 Ratio)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Beispiel für Vitest Unit Tests:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ExperimentaButton.spec.ts
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import ExperimentaButton from './ExperimentaButton.vue'
|
||||||
|
|
||||||
|
describe('ExperimentaButton', () => {
|
||||||
|
it('renders primary button by default', () => {
|
||||||
|
const wrapper = mount(ExperimentaButton, {
|
||||||
|
slots: { default: 'Click me' },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.find('.btn-primary').exists()).toBe(true)
|
||||||
|
expect(wrapper.text()).toBe('Click me')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emits click event', async () => {
|
||||||
|
const wrapper = mount(ExperimentaButton)
|
||||||
|
await wrapper.trigger('click')
|
||||||
|
|
||||||
|
expect(wrapper.emitted('click')).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders as link when href is provided', () => {
|
||||||
|
const wrapper = mount(ExperimentaButton, {
|
||||||
|
props: { href: 'https://example.com' },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.element.tagName).toBe('A')
|
||||||
|
expect(wrapper.attributes('href')).toBe('https://example.com')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Storybook Integration (Optional)
|
||||||
|
|
||||||
|
Erstelle Stories für visuelle Dokumentation:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ExperimentaButton.stories.ts
|
||||||
|
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||||
|
import ExperimentaButton from './ExperimentaButton.vue'
|
||||||
|
|
||||||
|
const meta: Meta<typeof ExperimentaButton> = {
|
||||||
|
title: 'Components/ExperimentaButton',
|
||||||
|
component: ExperimentaButton,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof ExperimentaButton>
|
||||||
|
|
||||||
|
export const Primary: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'primary',
|
||||||
|
},
|
||||||
|
render: (args) => ({
|
||||||
|
components: { ExperimentaButton },
|
||||||
|
setup() {
|
||||||
|
return { args }
|
||||||
|
},
|
||||||
|
template: '<ExperimentaButton v-bind="args">Click me</ExperimentaButton>',
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Secondary: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'secondary',
|
||||||
|
},
|
||||||
|
render: (args) => ({
|
||||||
|
components: { ExperimentaButton },
|
||||||
|
setup() {
|
||||||
|
return { args }
|
||||||
|
},
|
||||||
|
template: '<ExperimentaButton v-bind="args">Cancel</ExperimentaButton>',
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Weitere Ressourcen
|
||||||
|
|
||||||
|
- **Design System Dokumentation**: `docs/DESIGN_SYSTEM.md`
|
||||||
|
- **Tailwind Config**: `tailwind.config.ts`
|
||||||
|
- **CSS Custom Properties**: `assets/css/tailwind.css`
|
||||||
|
- **Design-Vorlagen**: `design-examples/*.html`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Fragen oder Feedback?** → docs@experimenta.science
|
||||||
981
docs/design-examples/design-example1.html
Normal file
@@ -0,0 +1,981 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Jahreskarte verlängert | experimenta</title>
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
background: linear-gradient(135deg, #2e1065 0%, #1a0a3a 50%, #0f051d 100%);
|
||||||
|
background-attachment: fixed;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
color: white;
|
||||||
|
line-height: 1.6;
|
||||||
|
min-height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Simplified Header */
|
||||||
|
.header-wrapper {
|
||||||
|
background: rgba(46, 16, 101, 0.95);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
position: relative;
|
||||||
|
z-index: 100;
|
||||||
|
padding: 30px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: white;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-svg {
|
||||||
|
width: 300px;
|
||||||
|
height: auto;
|
||||||
|
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content */
|
||||||
|
.main-content {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 40px 20px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-message {
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 60px 40px;
|
||||||
|
margin: 40px 0;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-icon {
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #46c74a;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 100%;
|
||||||
|
font-size: 4rem;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
animation: pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 36px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: white;
|
||||||
|
font-weight: 300;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-text {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress Bar Styles */
|
||||||
|
.progress-container {
|
||||||
|
margin: 40px 0;
|
||||||
|
padding: 30px;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 15px;
|
||||||
|
border-left: 4px solid #f59d24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #f59d24;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-stats {
|
||||||
|
font-size: 18px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 30px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 15px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #46c74a 0%, #66d96a 50%, #46c74a 100%);
|
||||||
|
border-radius: 15px;
|
||||||
|
transition: width 0s ease-out;
|
||||||
|
position: relative;
|
||||||
|
width: 0%;
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-percentage {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7);
|
||||||
|
z-index: 2;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 30px;
|
||||||
|
margin: 30px 0;
|
||||||
|
border-left: 4px solid #f59d24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info h3 {
|
||||||
|
color: #f59d24;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info p {
|
||||||
|
font-size: 18px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-info {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 40px 30px;
|
||||||
|
margin: 30px 0;
|
||||||
|
text-align: center;
|
||||||
|
border-left: 4px solid #f59d24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-info p {
|
||||||
|
font-size: 18px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item strong {
|
||||||
|
color: #f59d24;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item a {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 400;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
word-break: break-all;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item a:hover {
|
||||||
|
color: #ffb347;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 30px;
|
||||||
|
margin: 30px 0;
|
||||||
|
text-align: center;
|
||||||
|
border-left: 4px solid #f59d24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section p {
|
||||||
|
font-size: 18px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section a {
|
||||||
|
color: #f59d24;
|
||||||
|
text-decoration: underline;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 18px;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section a:hover {
|
||||||
|
color: #ff4081;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
background: #e6007e;
|
||||||
|
background-image: linear-gradient(to left, #e6007e, #e6007e, #e40521, #e6007e);
|
||||||
|
background-size: 300%;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 10px 30px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background-position 1s,
|
||||||
|
all 0.3s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 30px;
|
||||||
|
text-transform: none;
|
||||||
|
line-height: 1.7em;
|
||||||
|
position: relative;
|
||||||
|
outline: 0;
|
||||||
|
border-width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button:hover {
|
||||||
|
background-position: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Simplified Footer */
|
||||||
|
.footer {
|
||||||
|
background: linear-gradient(135deg, #1a0a3a 0%, #0f051d 100%);
|
||||||
|
margin-top: 80px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 60px 20px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-main {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2.5fr 1fr 1fr 1.5fr;
|
||||||
|
gap: 50px;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section h3 {
|
||||||
|
color: #e91e63;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section p {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links li {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links a {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links a:hover {
|
||||||
|
color: #ff4081;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-logo-svg {
|
||||||
|
width: 200px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partner-section {
|
||||||
|
margin: 30px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partner-logos {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partner-logo {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
min-width: 120px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partner-logo:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links a {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: rgba(233, 30, 99, 0.2);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links a:hover {
|
||||||
|
background: #e91e63;
|
||||||
|
transform: translateY(-3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom {
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
padding-top: 30px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom p {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom-links a {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom-links a:hover {
|
||||||
|
color: #ff4081;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recognition-section {
|
||||||
|
text-align: center;
|
||||||
|
margin: 40px 0;
|
||||||
|
padding: 30px;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recognition-section h4 {
|
||||||
|
color: #e91e63;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body {
|
||||||
|
background-attachment: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
padding: 0 15px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-svg {
|
||||||
|
width: 250px;
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
padding: 40px 15px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-message {
|
||||||
|
padding: 40px 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
padding: 25px 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-title {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-stats {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-wrapper {
|
||||||
|
height: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-percentage {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info,
|
||||||
|
.contact-info,
|
||||||
|
.info-section {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 25px 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info p,
|
||||||
|
.contact-info p,
|
||||||
|
.info-section p,
|
||||||
|
.contact-item a,
|
||||||
|
.contact-item strong,
|
||||||
|
.info-section a {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-text {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-main {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom p,
|
||||||
|
.footer-bottom-links a {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom-links {
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-logo-svg {
|
||||||
|
width: 180px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-svg {
|
||||||
|
width: 200px;
|
||||||
|
max-width: 85%;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
padding: 30px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-message {
|
||||||
|
padding: 30px 15px;
|
||||||
|
margin: 15px 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
padding: 20px 15px;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-stats {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-wrapper {
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-percentage {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info,
|
||||||
|
.contact-info,
|
||||||
|
.info-section {
|
||||||
|
padding: 20px 15px;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info p,
|
||||||
|
.contact-info p,
|
||||||
|
.info-section p,
|
||||||
|
.contact-item a,
|
||||||
|
.contact-item strong,
|
||||||
|
.info-section a {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-text {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
padding: 8px 24px;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom p,
|
||||||
|
.footer-bottom-links a {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom-links {
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<!-- Simplified Header -->
|
||||||
|
<header class="header-wrapper">
|
||||||
|
<div class="header-content">
|
||||||
|
<a href="https://www.experimenta.science/" class="logo">
|
||||||
|
<svg
|
||||||
|
class="logo-svg"
|
||||||
|
viewBox="0 0 382.94 87.17"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<linearGradient
|
||||||
|
id="a"
|
||||||
|
x1="102.63"
|
||||||
|
y1="152.32"
|
||||||
|
x2="135.19"
|
||||||
|
y2="191.11"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop offset="0.16" stop-color="#bf144c" />
|
||||||
|
<stop offset="0.29" stop-color="#ce0f60" />
|
||||||
|
<stop offset="0.47" stop-color="#de0b75" />
|
||||||
|
<stop offset="0.59" stop-color="#e4097d" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="b"
|
||||||
|
x1="104.87"
|
||||||
|
y1="170.45"
|
||||||
|
x2="104.87"
|
||||||
|
y2="170.45"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop offset="0.16" stop-color="#bf144c" />
|
||||||
|
<stop offset="0.29" stop-color="#ab1a4e" />
|
||||||
|
<stop offset="0.43" stop-color="#9f1d4f" />
|
||||||
|
<stop offset="0.57" stop-color="#9b1e4f" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="c"
|
||||||
|
x1="68.79"
|
||||||
|
y1="182.84"
|
||||||
|
x2="154.66"
|
||||||
|
y2="182.84"
|
||||||
|
xlink:href="#b"
|
||||||
|
/>
|
||||||
|
<linearGradient
|
||||||
|
id="d"
|
||||||
|
x1="94.04"
|
||||||
|
y1="182.21"
|
||||||
|
x2="114.5"
|
||||||
|
y2="126"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop offset="0.22" stop-color="#e4097d" />
|
||||||
|
<stop offset="0.32" stop-color="#e4115e" />
|
||||||
|
<stop offset="0.45" stop-color="#e5193d" />
|
||||||
|
<stop offset="0.55" stop-color="#e51e28" />
|
||||||
|
<stop offset="0.62" stop-color="#e52021" />
|
||||||
|
<stop offset="0.9" stop-color="#f7a822" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<polygon
|
||||||
|
points="143.78 50 151.18 39.6 144.43 39.6 139.68 46.33 135.13 39.6 127.79 39.6 135.32 50.08 127.29 61.23 134.09 61.23 139.3 53.78 144.43 61.23 151.59 61.23 143.78 50"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M245.79,175.33a9.2,9.2,0,0,0-1.85-3.39,7.28,7.28,0,0,0-2.9-2,10.29,10.29,0,0,0-3.65-.63c-3.12,0-5.47.95-7,2.82l-.56-2.23h-7.72v5.29h3.13v24.7h6.13v-8.4a8.29,8.29,0,0,0,1.7.42,17.3,17.3,0,0,0,2.6.19,11.8,11.8,0,0,0,4.53-.82,9.29,9.29,0,0,0,3.39-2.39,10.78,10.78,0,0,0,2.11-3.78,15.69,15.69,0,0,0,.74-5A16,16,0,0,0,245.79,175.33Zm-12.76,0a5.26,5.26,0,0,1,2.73-.75,4,4,0,0,1,3.12,1.4,5.85,5.85,0,0,1,1.25,4,10.56,10.56,0,0,1-.4,3.12,5.84,5.84,0,0,1-1.1,2.09,4.11,4.11,0,0,1-1.62,1.18,7.15,7.15,0,0,1-4.21.12,4.71,4.71,0,0,1-1.43-.58v-8.48A3.79,3.79,0,0,1,233,175.35Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M270.8,174.1a8.1,8.1,0,0,0-2.42-2.86,9.3,9.3,0,0,0-3.24-1.5,14.1,14.1,0,0,0-7.91.28,10.48,10.48,0,0,0-3.7,2.14,10,10,0,0,0-2.49,3.58,12.71,12.71,0,0,0-.9,5,13.24,13.24,0,0,0,.79,4.74,9.75,9.75,0,0,0,6,5.88,14.71,14.71,0,0,0,4.91.77,15.93,15.93,0,0,0,3-.26,14.61,14.61,0,0,0,2.59-.68,13.18,13.18,0,0,0,2.05-.92,8,8,0,0,0,1.47-1l.19-.18L269,184.85l-.33.28a8.34,8.34,0,0,1-2.51,1.36,11.84,11.84,0,0,1-4,.57,9.83,9.83,0,0,1-2.28-.26,6,6,0,0,1-1.85-.81,4.16,4.16,0,0,1-1.28-1.39,4.33,4.33,0,0,1-.5-1.75h15l.05-.28a19.77,19.77,0,0,0,.43-4A9.75,9.75,0,0,0,270.8,174.1Zm-11.06.54a6.52,6.52,0,0,1,1.95-.29,4.29,4.29,0,0,1,3.24,1.11,4.13,4.13,0,0,1,1.05,2.72h-9.55a4.05,4.05,0,0,1,.5-1.39,4.45,4.45,0,0,1,1.18-1.33A5.21,5.21,0,0,1,259.74,174.64Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M194.59,174.1a8,8,0,0,0-2.43-2.86,9.26,9.26,0,0,0-3.23-1.5A14.09,14.09,0,0,0,181,170a10.48,10.48,0,0,0-3.7,2.14,10,10,0,0,0-2.49,3.58,12.7,12.7,0,0,0-.91,5,13.23,13.23,0,0,0,.8,4.74,9.75,9.75,0,0,0,6,5.88,14.71,14.71,0,0,0,4.91.77,15.82,15.82,0,0,0,3-.26,14.38,14.38,0,0,0,2.59-.68,12.91,12.91,0,0,0,2.06-.92,8,8,0,0,0,1.47-1l.19-.18-2.11-4.18-.34.28a8.22,8.22,0,0,1-2.51,1.36,11.78,11.78,0,0,1-4,.57,9.7,9.7,0,0,1-2.28-.26,6,6,0,0,1-1.86-.81,4.23,4.23,0,0,1-1.27-1.39,4.33,4.33,0,0,1-.5-1.75h15l.06-.28a20.47,20.47,0,0,0,.42-4A9.75,9.75,0,0,0,194.59,174.1Zm-11.06.54a6.52,6.52,0,0,1,1.95-.29,4.31,4.31,0,0,1,3.24,1.11,4.07,4.07,0,0,1,1,2.72h-9.54a4,4,0,0,1,.49-1.39,4.35,4.35,0,0,1,1.19-1.33A5.21,5.21,0,0,1,183.53,174.64Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M298.09,178.12v-.34c0-3-.49-5.07-1.52-6.37a5.07,5.07,0,0,0-4.22-2,6.89,6.89,0,0,0-3.69,1,11.46,11.46,0,0,0-2.44,2l-.58-2.52h-10v5.29h5.18v11h-5.18v5.3H296v-5.3h-9.08v-8.47a8.94,8.94,0,0,1,1.61-1.76,3.84,3.84,0,0,1,2.48-.76,1.2,1.2,0,0,1,1.11.54,3.85,3.85,0,0,1,.39,2v.34Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M350.79,173.27a5.61,5.61,0,0,0-1.16-2.29,4.05,4.05,0,0,0-1.79-1.21,7.17,7.17,0,0,0-2.24-.33,6.19,6.19,0,0,0-3.12.82,5,5,0,0,0-1.79,1.7,3.66,3.66,0,0,0-1.7-1.81,6.39,6.39,0,0,0-2.92-.71,6,6,0,0,0-3.17.82,5.75,5.75,0,0,0-1.71,1.57l-.53-1.93h-4.78v21.62h6V176.07a1.89,1.89,0,0,1,.72-.94,2,2,0,0,1,1.92-.27,1.05,1.05,0,0,1,.53.44,3.5,3.5,0,0,1,.41,1.11,9.26,9.26,0,0,1,.17,2v13.16h6V175.94a1.5,1.5,0,0,1,.63-.9,2.12,2.12,0,0,1,1.18-.31,1.62,1.62,0,0,1,1.29.63,3.88,3.88,0,0,1,.56,2.42v13.74h6V176.86A14.23,14.23,0,0,0,350.79,173.27Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M375.88,174.1a8,8,0,0,0-2.43-2.86,9.3,9.3,0,0,0-3.24-1.5,14.1,14.1,0,0,0-7.91.28,10.44,10.44,0,0,0-3.69,2.14,10.11,10.11,0,0,0-2.5,3.58,12.71,12.71,0,0,0-.9,5,13.46,13.46,0,0,0,.79,4.74,9.89,9.89,0,0,0,2.32,3.6,10,10,0,0,0,3.71,2.28,14.65,14.65,0,0,0,4.9.77,15.93,15.93,0,0,0,3-.26,14.61,14.61,0,0,0,2.59-.68,13.77,13.77,0,0,0,2.06-.92,8.18,8.18,0,0,0,1.47-1l.19-.18-2.12-4.18-.33.28a8.34,8.34,0,0,1-2.51,1.36,11.82,11.82,0,0,1-4,.57,9.7,9.7,0,0,1-2.28-.26,5.93,5.93,0,0,1-1.86-.81,4.23,4.23,0,0,1-1.27-1.39,4.18,4.18,0,0,1-.5-1.75h15l.06-.28a20.52,20.52,0,0,0,.43-4A9.75,9.75,0,0,0,375.88,174.1Zm-11.06.54a6.5,6.5,0,0,1,1.94-.29,4.33,4.33,0,0,1,3.25,1.11,4.18,4.18,0,0,1,1.05,2.72h-9.55a3.84,3.84,0,0,1,.49-1.39,4.35,4.35,0,0,1,1.19-1.33A5.21,5.21,0,0,1,364.82,174.64Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M401.33,174a7,7,0,0,0-1.61-2.75,5.79,5.79,0,0,0-2.46-1.46,10.19,10.19,0,0,0-3-.44,9,9,0,0,0-7,3.07l-.57-2.48h-7.94v5.29h3v16.33h6.13V177.67a4.13,4.13,0,0,1,1.59-2,4.48,4.48,0,0,1,2.62-.83,3.6,3.6,0,0,1,2.67,1,4.72,4.72,0,0,1,1,3.43v12.24h6.14V178.15A13.05,13.05,0,0,0,401.33,174Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M425.62,183.62l-1.41,1.17a9.47,9.47,0,0,1-1.29.89,7,7,0,0,1-3.53.92,3.83,3.83,0,0,1-3.12-1.32,7,7,0,0,1-1.14-4.45v-5.64h11V169.9h-11v-6.43L409,165.21v4.69h-4.14v5.29H409v5.64c0,3.87.83,6.74,2.46,8.54s4.09,2.73,7.31,2.73a12.69,12.69,0,0,0,2.7-.3,14.51,14.51,0,0,0,2.64-.84,15.43,15.43,0,0,0,2.35-1.24,8.09,8.09,0,0,0,1.85-1.59l.17-.2Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M449,186.22c0-.27-.06-.54-.1-.81-.05-.71-.08-1.36-.08-1.94,0-.78.05-1.66.14-2.65s.16-2.25.16-3.67a10.5,10.5,0,0,0-.44-3.12,6,6,0,0,0-1.45-2.44,6.56,6.56,0,0,0-2.6-1.57,11.86,11.86,0,0,0-3.81-.54,22.68,22.68,0,0,0-5.29.55,23.45,23.45,0,0,0-3.93,1.32l-.28.12,1.49,4.95.36-.17a22.8,22.8,0,0,1,2.83-1.07,12,12,0,0,1,3.71-.53,4.18,4.18,0,0,1,2.5.62,2.33,2.33,0,0,1,.77,2v.8a2.53,2.53,0,0,1,0,.47l-1.71-.15c-.56,0-1.1-.08-1.61-.08a17.54,17.54,0,0,0-3.92.41,9.29,9.29,0,0,0-3.07,1.26,5.82,5.82,0,0,0-2,2.22,7.07,7.07,0,0,0-.68,3.19,6.12,6.12,0,0,0,1.94,4.7,7.43,7.43,0,0,0,5.19,1.76,8.26,8.26,0,0,0,4.33-1,7.94,7.94,0,0,0,2.17-1.9l.49,2.56h7.66v-5.3Zm-12.43-2.67a2.4,2.4,0,0,1,.88-.71,4.48,4.48,0,0,1,1.31-.43,7.73,7.73,0,0,1,1.51-.15,13.48,13.48,0,0,1,1.73.11c.38,0,.69.09.94.13v2.13a4.16,4.16,0,0,1-4.08,2.05,2.92,2.92,0,0,1-2-.55,2.07,2.07,0,0,1-.57-1.57A1.68,1.68,0,0,1,436.52,183.55Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<polygon
|
||||||
|
points="249.76 48.18 241.03 39.45 230.96 49.53 230.96 52.96 233.6 50.29 240.61 57.31 238.01 59.91 239.67 61.58 253.68 47.56 253.68 44.27 249.76 48.18"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M314.12,162.94a5.56,5.56,0,1,1-5.55-5.55A5.55,5.55,0,0,1,314.12,162.94Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M275.61,204.76l.56,0,.9-.06,1.06-.05,1,0a6.75,6.75,0,0,1,2.74.49,4.2,4.2,0,0,1,1.72,1.32,5.15,5.15,0,0,1,.88,2,11.58,11.58,0,0,1,.25,2.5,11.24,11.24,0,0,1-.25,2.4,5.62,5.62,0,0,1-.9,2.09,4.62,4.62,0,0,1-1.79,1.48,6.81,6.81,0,0,1-2.95.56l-.73,0-.91,0-.89-.06a4.37,4.37,0,0,1-.65-.06Zm3.57,2c-.22,0-.44,0-.66,0l-.48.05v8.34l.22,0h.29l.29,0h.24a3.13,3.13,0,0,0,1.64-.38,2.41,2.41,0,0,0,.92-1,4.31,4.31,0,0,0,.39-1.4,13.77,13.77,0,0,0,.09-1.57,13.6,13.6,0,0,0-.08-1.4,4.16,4.16,0,0,0-.38-1.34,2.46,2.46,0,0,0-.88-1A2.85,2.85,0,0,0,279.18,206.76Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M286.81,208.92a11,11,0,0,1,1.65-.55,9.5,9.5,0,0,1,2.21-.23,5.19,5.19,0,0,1,1.58.22,2.63,2.63,0,0,1,1.05.64,2.38,2.38,0,0,1,.57,1,4.26,4.26,0,0,1,.18,1.28c0,.61,0,1.13-.06,1.55s-.06.81-.06,1.14,0,.53,0,.84a3.93,3.93,0,0,1,.06.48h1.15v2h-3L292,216h-.08a3.31,3.31,0,0,1-1,1,3.46,3.46,0,0,1-1.77.4,3,3,0,0,1-2.1-.71,2.45,2.45,0,0,1-.78-1.89,2.86,2.86,0,0,1,.27-1.29,2.28,2.28,0,0,1,.8-.89,4,4,0,0,1,1.25-.52,7.54,7.54,0,0,1,1.64-.16l.67,0c.24,0,.52,0,.87.07a2.07,2.07,0,0,0,0-.35v-.34a1.13,1.13,0,0,0-.39-1,1.86,1.86,0,0,0-1.15-.29,5.11,5.11,0,0,0-1.62.23,9.05,9.05,0,0,0-1.22.46Zm3,6.53a2.12,2.12,0,0,0,1.27-.31,2.1,2.1,0,0,0,.61-.67v-1.06a4.92,4.92,0,0,0-.52-.08,6.06,6.06,0,0,0-.76-.05,3.05,3.05,0,0,0-.67.07,2.22,2.22,0,0,0-.6.19,1.31,1.31,0,0,0-.42.35.86.86,0,0,0-.16.51,1,1,0,0,0,.29.78A1.39,1.39,0,0,0,289.84,215.45Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M302.86,214.76a.64.64,0,0,0-.44-.57,6.8,6.8,0,0,0-1.07-.39q-.64-.18-1.41-.36a6.44,6.44,0,0,1-1.4-.52,3.49,3.49,0,0,1-1.08-.84,1.93,1.93,0,0,1-.44-1.3,2.2,2.2,0,0,1,.3-1.16,2.59,2.59,0,0,1,.8-.85,3.82,3.82,0,0,1,1.21-.52,6.09,6.09,0,0,1,1.52-.18,7.73,7.73,0,0,1,1.53.13,7.07,7.07,0,0,1,1.15.3,4.28,4.28,0,0,1,.83.4l.6.4-1,1.58-.61-.33-.73-.31a6.89,6.89,0,0,0-.81-.23,3.81,3.81,0,0,0-.82-.09,3.15,3.15,0,0,0-1.21.2.63.63,0,0,0-.46.6c0,.21.14.39.43.52a6.8,6.8,0,0,0,1.08.36l1.4.37a8.06,8.06,0,0,1,1.41.5,3.52,3.52,0,0,1,1.08.81,1.89,1.89,0,0,1,.43,1.28,2.57,2.57,0,0,1-1,2.13,4.69,4.69,0,0,1-2.92.77,7,7,0,0,1-2.64-.45,6,6,0,0,1-1.79-1.06l1.07-1.67a4.73,4.73,0,0,0,.61.43,5.31,5.31,0,0,0,.86.44,7.61,7.61,0,0,0,1,.33,4.55,4.55,0,0,0,1.08.13,2.26,2.26,0,0,0,1-.19A.68.68,0,0,0,302.86,214.76Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M311.81,213.14h1.95v1.76l.11,0a5.28,5.28,0,0,0,.9.29,4.3,4.3,0,0,0,1,.12,2.61,2.61,0,0,0,1.54-.4,1.27,1.27,0,0,0,.55-1.08,1.22,1.22,0,0,0-.45-1,4.42,4.42,0,0,0-1.12-.66c-.44-.19-.93-.37-1.46-.57a7.09,7.09,0,0,1-1.46-.72,4.15,4.15,0,0,1-1.12-1.09,3,3,0,0,1-.45-1.71,3.35,3.35,0,0,1,.31-1.46,3.4,3.4,0,0,1,.87-1.15,4.1,4.1,0,0,1,1.35-.75,5.29,5.29,0,0,1,1.74-.27,11.64,11.64,0,0,1,2.13.2,6.05,6.05,0,0,1,1.68.53v3.62h-2v-2l-.11,0c-.26-.07-.54-.12-.84-.17a6.8,6.8,0,0,0-.9-.06,2.27,2.27,0,0,0-1.34.34,1,1,0,0,0-.49.91,1.19,1.19,0,0,0,.45,1,5.35,5.35,0,0,0,1.12.67c.45.2.93.4,1.46.61a7.15,7.15,0,0,1,1.46.74,4,4,0,0,1,1.12,1.09,2.86,2.86,0,0,1,.45,1.65,3.92,3.92,0,0,1-.35,1.69,3.41,3.41,0,0,1-1,1.2,4.33,4.33,0,0,1-1.51.73,7.68,7.68,0,0,1-3.14.14,7.59,7.59,0,0,1-1.07-.27,8.12,8.12,0,0,1-.86-.34l-.6-.3Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M330.86,216.36a5.24,5.24,0,0,1-1.71.83,7.74,7.74,0,0,1-2,.27,5.92,5.92,0,0,1-2.05-.33,4.16,4.16,0,0,1-1.51-1,4.12,4.12,0,0,1-.94-1.48,5.6,5.6,0,0,1-.32-1.92,5,5,0,0,1,.38-2,3.93,3.93,0,0,1,1-1.48,4.55,4.55,0,0,1,1.57-.92,6.21,6.21,0,0,1,2-.31,7.43,7.43,0,0,1,1.87.23,9.76,9.76,0,0,1,1.34.45v3.13h-2v-1.64a5.22,5.22,0,0,0-1.1-.12,3.35,3.35,0,0,0-1,.15,2.32,2.32,0,0,0-.87.48,2.6,2.6,0,0,0-.63.82,3.12,3.12,0,0,0,0,2.25,2.51,2.51,0,0,0,.55.83,2.42,2.42,0,0,0,.88.56,3.13,3.13,0,0,0,1.17.21,4.15,4.15,0,0,0,1.66-.29,7.06,7.06,0,0,0,1-.49Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M332.69,215.26h3.13v-5h-3.13v-2h5.44v6.94h3.29v2h-8.73Zm2.7-9.36a1.26,1.26,0,0,1,.41-.94,1.58,1.58,0,0,1,1.14-.39,1.74,1.74,0,0,1,1.19.39,1.19,1.19,0,0,1,.44.94,1.16,1.16,0,0,1-.44.94,1.84,1.84,0,0,1-1.19.36,1.66,1.66,0,0,1-1.14-.36A1.23,1.23,0,0,1,335.39,205.9Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M351.63,216.27a3.11,3.11,0,0,1-.6.42,5.25,5.25,0,0,1-.85.38,6.28,6.28,0,0,1-1.08.29,7,7,0,0,1-1.24.1,5.9,5.9,0,0,1-2-.32,4,4,0,0,1-1.52-.93,4.14,4.14,0,0,1-.95-1.48,6,6,0,0,1,0-4,4.07,4.07,0,0,1,1-1.46,4.36,4.36,0,0,1,1.52-.89,6,6,0,0,1,1.88-.29,5.78,5.78,0,0,1,1.4.18,3.92,3.92,0,0,1,1.33.61,3.23,3.23,0,0,1,1,1.17,3.87,3.87,0,0,1,.39,1.85,8.31,8.31,0,0,1-.18,1.65h-6.41a2,2,0,0,0,.24,1,1.91,1.91,0,0,0,.59.64,2.56,2.56,0,0,0,.84.37,4.57,4.57,0,0,0,1,.11,5.35,5.35,0,0,0,1.75-.24,3.81,3.81,0,0,0,1.12-.61Zm-3.84-6.35a2.73,2.73,0,0,0-.87.13,2.34,2.34,0,0,0-.74.37,2,2,0,0,0-.54.61,1.71,1.71,0,0,0-.25.81h4.37a1.76,1.76,0,0,0-2-1.92Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M352.84,208.32H356l.27,1.17h.07a3.42,3.42,0,0,1,1.17-1,3.68,3.68,0,0,1,1.84-.43,4.11,4.11,0,0,1,1.23.18,2.26,2.26,0,0,1,1,.59,2.84,2.84,0,0,1,.65,1.11,5.39,5.39,0,0,1,.24,1.73v5.53H360.1v-5.05a2.14,2.14,0,0,0-.49-1.56,1.7,1.7,0,0,0-1.24-.48,2,2,0,0,0-1.2.38,1.9,1.9,0,0,0-.74.95v5.76h-2.31v-6.93h-1.28Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M372.83,216.36a5.09,5.09,0,0,1-1.7.83,7.74,7.74,0,0,1-2.05.27,5.88,5.88,0,0,1-2.05-.33,4.29,4.29,0,0,1-1.52-1,4,4,0,0,1-.93-1.48,5.6,5.6,0,0,1-.32-1.92,5,5,0,0,1,.37-2,4.17,4.17,0,0,1,1-1.48,4.6,4.6,0,0,1,1.58-.92,6.21,6.21,0,0,1,2-.31,7.43,7.43,0,0,1,1.87.23,9.1,9.1,0,0,1,1.33.45v3.13h-1.95v-1.64a5.24,5.24,0,0,0-1.11-.12,3.28,3.28,0,0,0-1,.15,2.32,2.32,0,0,0-.87.48,2.46,2.46,0,0,0-.63.82,3,3,0,0,0,0,2.25,2.36,2.36,0,0,0,.55.83,2.42,2.42,0,0,0,.88.56,3.13,3.13,0,0,0,1.17.21,4.19,4.19,0,0,0,1.66-.29,7.71,7.71,0,0,0,1-.49Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M383.11,216.27a3.34,3.34,0,0,1-.59.42,5.81,5.81,0,0,1-.86.38,6.16,6.16,0,0,1-1.07.29,7.11,7.11,0,0,1-1.25.1,5.85,5.85,0,0,1-2-.32,4,4,0,0,1-1.52-.93,4,4,0,0,1-1-1.48,5.67,5.67,0,0,1-.33-2,5.26,5.26,0,0,1,.38-2.06,4.17,4.17,0,0,1,1-1.46,4.36,4.36,0,0,1,1.52-.89,6,6,0,0,1,1.89-.29,5.7,5.7,0,0,1,1.39.18,3.75,3.75,0,0,1,1.33.61,3.36,3.36,0,0,1,1,1.17,4,4,0,0,1,.38,1.85,8.31,8.31,0,0,1-.18,1.65h-6.4a2,2,0,0,0,.24,1,1.79,1.79,0,0,0,.58.64,2.66,2.66,0,0,0,.84.37,4.6,4.6,0,0,0,1,.11,5.39,5.39,0,0,0,1.75-.24,3.87,3.87,0,0,0,1.11-.61Zm-3.84-6.35a2.73,2.73,0,0,0-.87.13,2.45,2.45,0,0,0-.74.37,2,2,0,0,0-.54.61,1.83,1.83,0,0,0-.25.81h4.38a2,2,0,0,0-.49-1.4A1.94,1.94,0,0,0,379.27,209.92Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M397.17,213h1.95v3.56a4.32,4.32,0,0,1-1.45.64,7.37,7.37,0,0,1-4-.11,5,5,0,0,1-1.85-1.12,5.51,5.51,0,0,1-1.28-2,8.35,8.35,0,0,1-.48-3,7.5,7.5,0,0,1,.54-3.07,5.58,5.58,0,0,1,1.39-2,5.16,5.16,0,0,1,1.91-1.09,6.82,6.82,0,0,1,2.06-.33,7.84,7.84,0,0,1,1.81.18,6.52,6.52,0,0,1,1.21.41v3.7H397v-2a7.11,7.11,0,0,0-1.14-.09,3.27,3.27,0,0,0-1.29.26,2.83,2.83,0,0,0-1,.79,3.77,3.77,0,0,0-.69,1.34,6.48,6.48,0,0,0-.25,1.92,6,6,0,0,0,.23,1.75,4,4,0,0,0,.67,1.36,3,3,0,0,0,1.09.88,3.26,3.26,0,0,0,1.46.31,6.12,6.12,0,0,0,1.14-.1Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M409.26,216.27a3.11,3.11,0,0,1-.6.42,5.25,5.25,0,0,1-.85.38,6.41,6.41,0,0,1-1.07.29,7.19,7.19,0,0,1-1.25.1,5.9,5.9,0,0,1-2-.32,4,4,0,0,1-1.52-.93,4.14,4.14,0,0,1-.95-1.48,5.44,5.44,0,0,1-.33-2,5.26,5.26,0,0,1,.38-2.06,4.17,4.17,0,0,1,1-1.46,4.36,4.36,0,0,1,1.52-.89,6,6,0,0,1,1.88-.29,5.78,5.78,0,0,1,1.4.18,3.92,3.92,0,0,1,1.33.61,3.23,3.23,0,0,1,1,1.17,3.87,3.87,0,0,1,.39,1.85,9.12,9.12,0,0,1-.18,1.65H403a2,2,0,0,0,.24,1,1.79,1.79,0,0,0,.58.64,2.47,2.47,0,0,0,.84.37,4.57,4.57,0,0,0,1,.11,5.35,5.35,0,0,0,1.75-.24,3.81,3.81,0,0,0,1.12-.61Zm-3.84-6.35a2.73,2.73,0,0,0-.87.13,2.34,2.34,0,0,0-.74.37,2,2,0,0,0-.54.61,1.83,1.83,0,0,0-.25.81h4.37a1.75,1.75,0,0,0-2-1.92Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M410.47,208.32h3.11l.27,1.17h.07a3.46,3.46,0,0,1,1.18-1,3.64,3.64,0,0,1,1.83-.43a4.06,4.06,0,0,1,1.23.18,2.26,2.26,0,0,1,1,.59,2.84,2.84,0,0,1,.65,1.11,5.39,5.39,0,0,1,.24,1.73v5.53h-2.31v-5.05a2.1,2.1,0,0,0-.49-1.56,1.7,1.7,0,0,0-1.24-.48,2,2,0,0,0-1.2.38,1.9,1.9,0,0,0-.74.95v5.76h-2.31v-6.93h-1.28Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M422.81,210.28h-1.75v-2h1.75v-2l2.32-.66v2.69h4.66v2h-4.66v2.54a3.07,3.07,0,0,0,.51,2,1.8,1.8,0,0,0,1.44.62,3,3,0,0,0,.87-.12,2.85,2.85,0,0,0,.71-.29,4.07,4.07,0,0,0,.57-.39l.47-.39,1.07,1.6a4.11,4.11,0,0,1-.76.65,7,7,0,0,1-1,.51,5.78,5.78,0,0,1-1.09.35,5.4,5.4,0,0,1-1.12.12,3.84,3.84,0,0,1-3-1.11,5.17,5.17,0,0,1-1-3.53Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M440.74,216.27a3.34,3.34,0,0,1-.59.42,5.81,5.81,0,0,1-.86.38,6.16,6.16,0,0,1-1.07.29,7.11,7.11,0,0,1-1.25.1,5.85,5.85,0,0,1-2-.32,4,4,0,0,1-1.52-.93,4,4,0,0,1-1-1.48,5.67,5.67,0,0,1-.32-2,5.26,5.26,0,0,1,.37-2.06,4.17,4.17,0,0,1,1-1.46,4.36,4.36,0,0,1,1.52-.89,6,6,0,0,1,1.89-.29,5.7,5.7,0,0,1,1.39.18,3.83,3.83,0,0,1,1.33.61,3.36,3.36,0,0,1,1,1.17,4,4,0,0,1,.38,1.85,8.31,8.31,0,0,1-.18,1.65h-6.4a2,2,0,0,0,.24,1,1.81,1.81,0,0,0,.59.64,2.51,2.51,0,0,0,.83.37,4.67,4.67,0,0,0,1,.11,5.39,5.39,0,0,0,1.75-.24,3.87,3.87,0,0,0,1.11-.61Zm-3.84-6.35a2.73,2.73,0,0,0-.87.13,2.45,2.45,0,0,0-.74.37,2,2,0,0,0-.79,1.42h4.38a2,2,0,0,0-.49-1.4A1.94,1.94,0,0,0,436.9,209.92Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M442.43,215.26h2.21v-5h-2.21v-2h4l.26,1.17h.07a5,5,0,0,1,1.14-1,2.84,2.84,0,0,1,1.5-.39,2,2,0,0,1,1.68.78,4.28,4.28,0,0,1,.61,2.61h-2.08a1.72,1.72,0,0,0-.19-.93.64.64,0,0,0-.59-.3,1.74,1.74,0,0,0-1.15.36,3.6,3.6,0,0,0-.74.82v3.79h3.86v2h-8.38Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M93.1,181.53l20.42-19.22c.87-.76,1.61-1.66,2.61-1.8,1.19-.17,3,1.09,3.3,1.28s10.7,6.64,12.57,7.77l15.75,9.71c4.25-1.29,7.2-4.59,7.46-7.92a5.92,5.92,0,0,0-3-5.65l-2.48-1.53h0s-17.5-10.32-22.43-13.18a22.83,22.83,0,0,0-11-3c-4.71.09-8.62,2.32-12.24,5h0l-24,18.2,3.64,4.7a16.1,16.1,0,0,0,6.48,4.83A17.88,17.88,0,0,0,93.1,181.53Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="url(#a)"
|
||||||
|
/>
|
||||||
|
<path d="M104.87,170.45h0" transform="translate(-68.76 -130.29)" fill="url(#b)" />
|
||||||
|
<path
|
||||||
|
d="M93.1,181.53h0l11.77-11.08a3.11,3.11,0,0,1-1.35-.18,4.24,4.24,0,0,1-1.33-1.11l-7.57-8.93-14.57,11-7.26,5.5s-4.11,2.67-4,6.32c.15,4.65,5.34,6.69,5.34,6.69l30.63,13.13a35.93,35.93,0,0,0,11.38,2.53c3.39.08,6.83-1.14,10.49-2.24l24.76-8.46c1.26-.41,3.26-1.65,3.26-3.17,0-1.2-1.57-2.73-3.33-3.51a16.28,16.28,0,0,0-8.57-1.19c-4.7.44-25.95,7.65-25.95,7.65L93.16,184.23a1.28,1.28,0,0,1-.91-1.58A2.68,2.68,0,0,1,93.1,181.53Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="url(#c)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M147.75,179.27c4.25-1.29,7.2-4.59,7.46-7.92a5.92,5.92,0,0,0-3-5.65l-2.48-1.53L132,169.56l-27.13.89h0a3.11,3.11,0,0,1-1.35-.18,4.24,4.24,0,0,1-1.33-1.11l-7.57-8.93-14.57,11,3.64,4.7a16.1,16.1,0,0,0,6.48,4.83,17.88,17.88,0,0,0,2.93.73h0a66.73,66.73,0,0,0,7.06.19C107.65,182,144.08,180.38,147.75,179.27Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#e4097d"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M104.87,170.45a3.26,3.26,0,0,1-1.35-.18,4.24,4.24,0,0,1-1.33-1.11l-7.67-9-3-3.55a1.94,1.94,0,0,1-.41-1.27,2.18,2.18,0,0,1,1-1.83L110,141.9a6.43,6.43,0,0,1,1.81-.87,5.82,5.82,0,0,1,1.86.09l16.49,2.64a24.38,24.38,0,0,0,9.39-.52c1.94-.49,4.31-1.25,5.39-2.91s-.6-2.9-1.11-3.27c-2.08-1.5-4.67-1.92-7.18-2.32l-25.48-4.08a23.84,23.84,0,0,0-10.88.57A19.61,19.61,0,0,0,95,133.6L71.44,149.36a6.34,6.34,0,0,0-1.08,9.38l.21.27,13.12,17a16.1,16.1,0,0,0,6.48,4.83,17.88,17.88,0,0,0,2.93.73Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="url(#d)"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="main-content">
|
||||||
|
<div class="success-message">
|
||||||
|
<div class="success-icon">✓</div>
|
||||||
|
<h1>Jahreskarten-Verlängerungen</h1>
|
||||||
|
|
||||||
|
<!-- Progress Bar Section -->
|
||||||
|
<div class="progress-container">
|
||||||
|
<div class="progress-header">
|
||||||
|
<h3 class="progress-title">Verlängerungsfortschritt</h3>
|
||||||
|
<div class="progress-stats">{{payload.count}} / {{payload.total}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar-wrapper">
|
||||||
|
<div class="progress-bar" id="progressBar"></div>
|
||||||
|
<div class="progress-percentage" id="progressPercentage">{{payload.perc}}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="https://www.experimenta.science/" class="back-button"
|
||||||
|
>Zur experimenta Startseite</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="footer-content">
|
||||||
|
<div class="footer-bottom">
|
||||||
|
<p>© 2025 experimenta gGmbH – Das Science Center. Alle Rechte vorbehalten.</p>
|
||||||
|
<div class="footer-bottom-links">
|
||||||
|
<a href="https://www.experimenta.science/kontakt/">Kontakt</a>
|
||||||
|
<a href="https://www.experimenta.science/impressum/">Impressum</a>
|
||||||
|
<a href="https://www.experimenta.science/datenschutz/">Datenschutz</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Wait for page to load, then animate progress bar
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
// Get the percentage value from the template variable
|
||||||
|
const percentage = parseFloat('{{payload.perc}}') || 0
|
||||||
|
const progressBar = document.getElementById('progressBar')
|
||||||
|
|
||||||
|
// Animate the progress bar after a short delay
|
||||||
|
setTimeout(function () {
|
||||||
|
progressBar.style.width = percentage + '%'
|
||||||
|
}, 0)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
867
docs/design-examples/design-example2-success.html
Normal file
@@ -0,0 +1,867 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Jahreskarte verlängert | experimenta</title>
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
background: linear-gradient(135deg, #2e1065 0%, #1a0a3a 50%, #0f051d 100%);
|
||||||
|
background-attachment: fixed;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
color: white;
|
||||||
|
line-height: 1.6;
|
||||||
|
min-height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Simplified Header */
|
||||||
|
.header-wrapper {
|
||||||
|
background: rgba(46, 16, 101, 0.95);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
position: relative;
|
||||||
|
z-index: 100;
|
||||||
|
padding: 30px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: white;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-svg {
|
||||||
|
width: 300px;
|
||||||
|
height: auto;
|
||||||
|
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content */
|
||||||
|
.main-content {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 40px 20px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-message {
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 60px 40px;
|
||||||
|
margin: 40px 0;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-icon {
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #46c74a;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 100%;
|
||||||
|
font-size: 4rem;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
animation: pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 36px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: white;
|
||||||
|
font-weight: 300;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-text {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 30px;
|
||||||
|
margin: 30px 0;
|
||||||
|
border-left: 4px solid #f59d24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info h3 {
|
||||||
|
color: #f59d24;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info p {
|
||||||
|
font-size: 18px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-info {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 40px 30px;
|
||||||
|
margin: 30px 0;
|
||||||
|
text-align: center;
|
||||||
|
border-left: 4px solid #f59d24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-info p {
|
||||||
|
font-size: 18px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item strong {
|
||||||
|
color: #f59d24;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item a {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 400;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
word-break: break-all;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item a:hover {
|
||||||
|
color: #ffb347;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 30px;
|
||||||
|
margin: 30px 0;
|
||||||
|
text-align: center;
|
||||||
|
border-left: 4px solid #f59d24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section p {
|
||||||
|
font-size: 18px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section a {
|
||||||
|
color: #f59d24;
|
||||||
|
text-decoration: underline;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 18px;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section a:hover {
|
||||||
|
color: #ff4081;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
background: #e6007e;
|
||||||
|
background-image: linear-gradient(to left, #e6007e, #e6007e, #e40521, #e6007e);
|
||||||
|
background-size: 300%;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 10px 30px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background-position 1s,
|
||||||
|
all 0.3s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 30px;
|
||||||
|
text-transform: none;
|
||||||
|
line-height: 1.7em;
|
||||||
|
position: relative;
|
||||||
|
outline: 0;
|
||||||
|
border-width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button:hover {
|
||||||
|
background-position: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Simplified Footer */
|
||||||
|
.footer {
|
||||||
|
background: linear-gradient(135deg, #1a0a3a 0%, #0f051d 100%);
|
||||||
|
margin-top: 80px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 60px 20px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-main {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2.5fr 1fr 1fr 1.5fr;
|
||||||
|
gap: 50px;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section h3 {
|
||||||
|
color: #e91e63;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section p {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links li {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links a {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links a:hover {
|
||||||
|
color: #ff4081;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-logo-svg {
|
||||||
|
width: 200px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partner-section {
|
||||||
|
margin: 30px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partner-logos {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partner-logo {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
min-width: 120px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partner-logo:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links a {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: rgba(233, 30, 99, 0.2);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links a:hover {
|
||||||
|
background: #e91e63;
|
||||||
|
transform: translateY(-3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom {
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
padding-top: 30px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom p {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom-links a {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom-links a:hover {
|
||||||
|
color: #ff4081;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recognition-section {
|
||||||
|
text-align: center;
|
||||||
|
margin: 40px 0;
|
||||||
|
padding: 30px;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recognition-section h4 {
|
||||||
|
color: #e91e63;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body {
|
||||||
|
background-attachment: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
padding: 0 15px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-svg {
|
||||||
|
width: 250px;
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
padding: 40px 15px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-message {
|
||||||
|
padding: 40px 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info,
|
||||||
|
.contact-info,
|
||||||
|
.info-section {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 25px 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info p,
|
||||||
|
.contact-info p,
|
||||||
|
.info-section p,
|
||||||
|
.contact-item a,
|
||||||
|
.contact-item strong,
|
||||||
|
.info-section a {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-text {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-main {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom p,
|
||||||
|
.footer-bottom-links a {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom-links {
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-logo-svg {
|
||||||
|
width: 180px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-svg {
|
||||||
|
width: 200px;
|
||||||
|
max-width: 85%;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
padding: 30px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-message {
|
||||||
|
padding: 30px 15px;
|
||||||
|
margin: 15px 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info,
|
||||||
|
.contact-info,
|
||||||
|
.info-section {
|
||||||
|
padding: 20px 15px;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info p,
|
||||||
|
.contact-info p,
|
||||||
|
.info-section p,
|
||||||
|
.contact-item a,
|
||||||
|
.contact-item strong,
|
||||||
|
.info-section a {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-text {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
padding: 8px 24px;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom p,
|
||||||
|
.footer-bottom-links a {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom-links {
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<!-- Simplified Header -->
|
||||||
|
<header class="header-wrapper">
|
||||||
|
<div class="header-content">
|
||||||
|
<a href="https://www.experimenta.science/" class="logo">
|
||||||
|
<svg
|
||||||
|
class="logo-svg"
|
||||||
|
viewBox="0 0 382.94 87.17"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<linearGradient
|
||||||
|
id="a"
|
||||||
|
x1="102.63"
|
||||||
|
y1="152.32"
|
||||||
|
x2="135.19"
|
||||||
|
y2="191.11"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop offset="0.16" stop-color="#bf144c" />
|
||||||
|
<stop offset="0.29" stop-color="#ce0f60" />
|
||||||
|
<stop offset="0.47" stop-color="#de0b75" />
|
||||||
|
<stop offset="0.59" stop-color="#e4097d" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="b"
|
||||||
|
x1="104.87"
|
||||||
|
y1="170.45"
|
||||||
|
x2="104.87"
|
||||||
|
y2="170.45"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop offset="0.16" stop-color="#bf144c" />
|
||||||
|
<stop offset="0.29" stop-color="#ab1a4e" />
|
||||||
|
<stop offset="0.43" stop-color="#9f1d4f" />
|
||||||
|
<stop offset="0.57" stop-color="#9b1e4f" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="c"
|
||||||
|
x1="68.79"
|
||||||
|
y1="182.84"
|
||||||
|
x2="154.66"
|
||||||
|
y2="182.84"
|
||||||
|
xlink:href="#b"
|
||||||
|
/>
|
||||||
|
<linearGradient
|
||||||
|
id="d"
|
||||||
|
x1="94.04"
|
||||||
|
y1="182.21"
|
||||||
|
x2="114.5"
|
||||||
|
y2="126"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop offset="0.22" stop-color="#e4097d" />
|
||||||
|
<stop offset="0.32" stop-color="#e4115e" />
|
||||||
|
<stop offset="0.45" stop-color="#e5193d" />
|
||||||
|
<stop offset="0.55" stop-color="#e51e28" />
|
||||||
|
<stop offset="0.62" stop-color="#e52021" />
|
||||||
|
<stop offset="0.9" stop-color="#f7a822" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<polygon
|
||||||
|
points="143.78 50 151.18 39.6 144.43 39.6 139.68 46.33 135.13 39.6 127.79 39.6 135.32 50.08 127.29 61.23 134.09 61.23 139.3 53.78 144.43 61.23 151.59 61.23 143.78 50"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M245.79,175.33a9.2,9.2,0,0,0-1.85-3.39,7.28,7.28,0,0,0-2.9-2,10.29,10.29,0,0,0-3.65-.63c-3.12,0-5.47.95-7,2.82l-.56-2.23h-7.72v5.29h3.13v24.7h6.13v-8.4a8.29,8.29,0,0,0,1.7.42,17.3,17.3,0,0,0,2.6.19,11.8,11.8,0,0,0,4.53-.82,9.29,9.29,0,0,0,3.39-2.39,10.78,10.78,0,0,0,2.11-3.78,15.69,15.69,0,0,0,.74-5A16,16,0,0,0,245.79,175.33Zm-12.76,0a5.26,5.26,0,0,1,2.73-.75,4,4,0,0,1,3.12,1.4,5.85,5.85,0,0,1,1.25,4,10.56,10.56,0,0,1-.4,3.12,5.84,5.84,0,0,1-1.1,2.09,4.11,4.11,0,0,1-1.62,1.18,7.15,7.15,0,0,1-4.21.12,4.71,4.71,0,0,1-1.43-.58v-8.48A3.79,3.79,0,0,1,233,175.35Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M270.8,174.1a8.1,8.1,0,0,0-2.42-2.86,9.3,9.3,0,0,0-3.24-1.5,14.1,14.1,0,0,0-7.91.28,10.48,10.48,0,0,0-3.7,2.14,10,10,0,0,0-2.49,3.58,12.71,12.71,0,0,0-.9,5,13.24,13.24,0,0,0,.79,4.74,9.75,9.75,0,0,0,6,5.88,14.71,14.71,0,0,0,4.91.77,15.93,15.93,0,0,0,3-.26,14.61,14.61,0,0,0,2.59-.68,13.18,13.18,0,0,0,2.05-.92,8,8,0,0,0,1.47-1l.19-.18L269,184.85l-.33.28a8.34,8.34,0,0,1-2.51,1.36,11.84,11.84,0,0,1-4,.57,9.83,9.83,0,0,1-2.28-.26,6,6,0,0,1-1.85-.81,4.16,4.16,0,0,1-1.28-1.39,4.33,4.33,0,0,1-.5-1.75h15l.05-.28a19.77,19.77,0,0,0,.43-4A9.75,9.75,0,0,0,270.8,174.1Zm-11.06.54a6.52,6.52,0,0,1,1.95-.29,4.29,4.29,0,0,1,3.24,1.11,4.13,4.13,0,0,1,1.05,2.72h-9.55a4.05,4.05,0,0,1,.5-1.39,4.45,4.45,0,0,1,1.18-1.33A5.21,5.21,0,0,1,259.74,174.64Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M194.59,174.1a8,8,0,0,0-2.43-2.86,9.26,9.26,0,0,0-3.23-1.5A14.09,14.09,0,0,0,181,170a10.48,10.48,0,0,0-3.7,2.14,10,10,0,0,0-2.49,3.58,12.7,12.7,0,0,0-.91,5,13.23,13.23,0,0,0,.8,4.74,9.75,9.75,0,0,0,6,5.88,14.71,14.71,0,0,0,4.91.77,15.82,15.82,0,0,0,3-.26,14.38,14.38,0,0,0,2.59-.68,12.91,12.91,0,0,0,2.06-.92,8,8,0,0,0,1.47-1l.19-.18-2.11-4.18-.34.28a8.22,8.22,0,0,1-2.51,1.36,11.78,11.78,0,0,1-4,.57,9.7,9.7,0,0,1-2.28-.26,6,6,0,0,1-1.86-.81,4.23,4.23,0,0,1-1.27-1.39,4.33,4.33,0,0,1-.5-1.75h15l.06-.28a20.47,20.47,0,0,0,.42-4A9.75,9.75,0,0,0,194.59,174.1Zm-11.06.54a6.52,6.52,0,0,1,1.95-.29,4.31,4.31,0,0,1,3.24,1.11,4.07,4.07,0,0,1,1,2.72h-9.54a4,4,0,0,1,.49-1.39,4.35,4.35,0,0,1,1.19-1.33A5.21,5.21,0,0,1,183.53,174.64Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M298.09,178.12v-.34c0-3-.49-5.07-1.52-6.37a5.07,5.07,0,0,0-4.22-2,6.89,6.89,0,0,0-3.69,1,11.46,11.46,0,0,0-2.44,2l-.58-2.52h-10v5.29h5.18v11h-5.18v5.3H296v-5.3h-9.08v-8.47a8.94,8.94,0,0,1,1.61-1.76,3.84,3.84,0,0,1,2.48-.76,1.2,1.2,0,0,1,1.11.54,3.85,3.85,0,0,1,.39,2v.34Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M350.79,173.27a5.61,5.61,0,0,0-1.16-2.29,4.05,4.05,0,0,0-1.79-1.21,7.17,7.17,0,0,0-2.24-.33,6.19,6.19,0,0,0-3.12.82,5,5,0,0,0-1.79,1.7,3.66,3.66,0,0,0-1.7-1.81,6.39,6.39,0,0,0-2.92-.71,6,6,0,0,0-3.17.82,5.75,5.75,0,0,0-1.71,1.57l-.53-1.93h-4.78v21.62h6V176.07a1.89,1.89,0,0,1,.72-.94,2,2,0,0,1,1.92-.27,1.05,1.05,0,0,1,.53.44,3.5,3.5,0,0,1,.41,1.11,9.26,9.26,0,0,1,.17,2v13.16h6V175.94a1.5,1.5,0,0,1,.63-.9,2.12,2.12,0,0,1,1.18-.31,1.62,1.62,0,0,1,1.29.63,3.88,3.88,0,0,1,.56,2.42v13.74h6V176.86A14.23,14.23,0,0,0,350.79,173.27Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M375.88,174.1a8,8,0,0,0-2.43-2.86,9.3,9.3,0,0,0-3.24-1.5,14.1,14.1,0,0,0-7.91.28,10.44,10.44,0,0,0-3.69,2.14,10.11,10.11,0,0,0-2.5,3.58,12.71,12.71,0,0,0-.9,5,13.46,13.46,0,0,0,.79,4.74,9.89,9.89,0,0,0,2.32,3.6,10,10,0,0,0,3.71,2.28,14.65,14.65,0,0,0,4.9.77,15.93,15.93,0,0,0,3-.26,14.61,14.61,0,0,0,2.59-.68,13.77,13.77,0,0,0,2.06-.92,8.18,8.18,0,0,0,1.47-1l.19-.18-2.12-4.18-.33.28a8.34,8.34,0,0,1-2.51,1.36,11.82,11.82,0,0,1-4,.57,9.7,9.7,0,0,1-2.28-.26,5.93,5.93,0,0,1-1.86-.81,4.23,4.23,0,0,1-1.27-1.39,4.18,4.18,0,0,1-.5-1.75h15l.06-.28a20.52,20.52,0,0,0,.43-4A9.75,9.75,0,0,0,375.88,174.1Zm-11.06.54a6.5,6.5,0,0,1,1.94-.29,4.33,4.33,0,0,1,3.25,1.11,4.18,4.18,0,0,1,1.05,2.72h-9.55a3.84,3.84,0,0,1,.49-1.39,4.35,4.35,0,0,1,1.19-1.33A5.21,5.21,0,0,1,364.82,174.64Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M401.33,174a7,7,0,0,0-1.61-2.75,5.79,5.79,0,0,0-2.46-1.46,10.19,10.19,0,0,0-3-.44,9,9,0,0,0-7,3.07l-.57-2.48h-7.94v5.29h3v16.33h6.13V177.67a4.13,4.13,0,0,1,1.59-2,4.48,4.48,0,0,1,2.62-.83,3.6,3.6,0,0,1,2.67,1,4.72,4.72,0,0,1,1,3.43v12.24h6.14V178.15A13.05,13.05,0,0,0,401.33,174Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M425.62,183.62l-1.41,1.17a9.47,9.47,0,0,1-1.29.89,7,7,0,0,1-3.53.92,3.83,3.83,0,0,1-3.12-1.32,7,7,0,0,1-1.14-4.45v-5.64h11V169.9h-11v-6.43L409,165.21v4.69h-4.14v5.29H409v5.64c0,3.87.83,6.74,2.46,8.54s4.09,2.73,7.31,2.73a12.69,12.69,0,0,0,2.7-.3,14.51,14.51,0,0,0,2.64-.84,15.43,15.43,0,0,0,2.35-1.24,8.09,8.09,0,0,0,1.85-1.59l.17-.2Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M449,186.22c0-.27-.06-.54-.1-.81-.05-.71-.08-1.36-.08-1.94,0-.78.05-1.66.14-2.65s.16-2.25.16-3.67a10.5,10.5,0,0,0-.44-3.12,6,6,0,0,0-1.45-2.44,6.56,6.56,0,0,0-2.6-1.57,11.86,11.86,0,0,0-3.81-.54,22.68,22.68,0,0,0-5.29.55,23.45,23.45,0,0,0-3.93,1.32l-.28.12,1.49,4.95.36-.17a22.8,22.8,0,0,1,2.83-1.07,12,12,0,0,1,3.71-.53,4.18,4.18,0,0,1,2.5.62,2.33,2.33,0,0,1,.77,2v.8a2.53,2.53,0,0,1,0,.47l-1.71-.15c-.56,0-1.1-.08-1.61-.08a17.54,17.54,0,0,0-3.92.41,9.29,9.29,0,0,0-3.07,1.26,5.82,5.82,0,0,0-2,2.22,7.07,7.07,0,0,0-.68,3.19,6.12,6.12,0,0,0,1.94,4.7,7.43,7.43,0,0,0,5.19,1.76,8.26,8.26,0,0,0,4.33-1,7.94,7.94,0,0,0,2.17-1.9l.49,2.56h7.66v-5.3Zm-12.43-2.67a2.4,2.4,0,0,1,.88-.71,4.48,4.48,0,0,1,1.31-.43,7.73,7.73,0,0,1,1.51-.15,13.48,13.48,0,0,1,1.73.11c.38,0,.69.09.94.13v2.13a4.16,4.16,0,0,1-4.08,2.05,2.92,2.92,0,0,1-2-.55,2.07,2.07,0,0,1-.57-1.57A1.68,1.68,0,0,1,436.52,183.55Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<polygon
|
||||||
|
points="249.76 48.18 241.03 39.45 230.96 49.53 230.96 52.96 233.6 50.29 240.61 57.31 238.01 59.91 239.67 61.58 253.68 47.56 253.68 44.27 249.76 48.18"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M314.12,162.94a5.56,5.56,0,1,1-5.55-5.55A5.55,5.55,0,0,1,314.12,162.94Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M275.61,204.76l.56,0,.9-.06,1.06-.05,1,0a6.75,6.75,0,0,1,2.74.49,4.2,4.2,0,0,1,1.72,1.32,5.15,5.15,0,0,1,.88,2,11.58,11.58,0,0,1,.25,2.5,11.24,11.24,0,0,1-.25,2.4,5.62,5.62,0,0,1-.9,2.09,4.62,4.62,0,0,1-1.79,1.48,6.81,6.81,0,0,1-2.95.56l-.73,0-.91,0-.89-.06a4.37,4.37,0,0,1-.65-.06Zm3.57,2c-.22,0-.44,0-.66,0l-.48.05v8.34l.22,0h.29l.29,0h.24a3.13,3.13,0,0,0,1.64-.38,2.41,2.41,0,0,0,.92-1,4.31,4.31,0,0,0,.39-1.4,13.77,13.77,0,0,0,.09-1.57,13.6,13.6,0,0,0-.08-1.4,4.16,4.16,0,0,0-.38-1.34,2.46,2.46,0,0,0-.88-1A2.85,2.85,0,0,0,279.18,206.76Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M286.81,208.92a11,11,0,0,1,1.65-.55,9.5,9.5,0,0,1,2.21-.23,5.19,5.19,0,0,1,1.58.22,2.63,2.63,0,0,1,1.05.64,2.38,2.38,0,0,1,.57,1,4.26,4.26,0,0,1,.18,1.28c0,.61,0,1.13-.06,1.55s-.06.81-.06,1.14,0,.53,0,.84a3.93,3.93,0,0,1,.06.48h1.15v2h-3L292,216h-.08a3.31,3.31,0,0,1-1,1,3.46,3.46,0,0,1-1.77.4,3,3,0,0,1-2.1-.71,2.45,2.45,0,0,1-.78-1.89,2.86,2.86,0,0,1,.27-1.29,2.28,2.28,0,0,1,.8-.89,4,4,0,0,1,1.25-.52,7.54,7.54,0,0,1,1.64-.16l.67,0c.24,0,.52,0,.87.07a2.07,2.07,0,0,0,0-.35v-.34a1.13,1.13,0,0,0-.39-1,1.86,1.86,0,0,0-1.15-.29,5.11,5.11,0,0,0-1.62.23,9.05,9.05,0,0,0-1.22.46Zm3,6.53a2.12,2.12,0,0,0,1.27-.31,2.1,2.1,0,0,0,.61-.67v-1.06a4.92,4.92,0,0,0-.52-.08,6.06,6.06,0,0,0-.76-.05,3.05,3.05,0,0,0-.67.07,2.22,2.22,0,0,0-.6.19,1.31,1.31,0,0,0-.42.35.86.86,0,0,0-.16.51,1,1,0,0,0,.29.78A1.39,1.39,0,0,0,289.84,215.45Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M302.86,214.76a.64.64,0,0,0-.44-.57,6.8,6.8,0,0,0-1.07-.39q-.64-.18-1.41-.36a6.44,6.44,0,0,1-1.4-.52,3.49,3.49,0,0,1-1.08-.84,1.93,1.93,0,0,1-.44-1.3,2.2,2.2,0,0,1,.3-1.16,2.59,2.59,0,0,1,.8-.85,3.82,3.82,0,0,1,1.21-.52,6.09,6.09,0,0,1,1.52-.18,7.73,7.73,0,0,1,1.53.13,7.07,7.07,0,0,1,1.15.3,4.28,4.28,0,0,1,.83.4l.6.4-1,1.58-.61-.33-.73-.31a6.89,6.89,0,0,0-.81-.23,3.81,3.81,0,0,0-.82-.09,3.15,3.15,0,0,0-1.21.2.63.63,0,0,0-.46.6c0,.21.14.39.43.52a6.8,6.8,0,0,0,1.08.36l1.4.37a8.06,8.06,0,0,1,1.41.5,3.52,3.52,0,0,1,1.08.81,1.89,1.89,0,0,1,.43,1.28,2.57,2.57,0,0,1-1,2.13,4.69,4.69,0,0,1-2.92.77,7,7,0,0,1-2.64-.45,6,6,0,0,1-1.79-1.06l1.07-1.67a4.73,4.73,0,0,0,.61.43,5.31,5.31,0,0,0,.86.44,7.61,7.61,0,0,0,1,.33,4.55,4.55,0,0,0,1.08.13,2.26,2.26,0,0,0,1-.19A.68.68,0,0,0,302.86,214.76Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M311.81,213.14h1.95v1.76l.11,0a5.28,5.28,0,0,0,.9.29,4.3,4.3,0,0,0,1,.12,2.61,2.61,0,0,0,1.54-.4,1.27,1.27,0,0,0,.55-1.08,1.22,1.22,0,0,0-.45-1,4.42,4.42,0,0,0-1.12-.66c-.44-.19-.93-.37-1.46-.57a7.09,7.09,0,0,1-1.46-.72,4.15,4.15,0,0,1-1.12-1.09,3,3,0,0,1-.45-1.71,3.35,3.35,0,0,1,.31-1.46,3.4,3.4,0,0,1,.87-1.15,4.1,4.1,0,0,1,1.35-.75,5.29,5.29,0,0,1,1.74-.27,11.64,11.64,0,0,1,2.13.2,6.05,6.05,0,0,1,1.68.53v3.62h-2v-2l-.11,0c-.26-.07-.54-.12-.84-.17a6.8,6.8,0,0,0-.9-.06,2.27,2.27,0,0,0-1.34.34,1,1,0,0,0-.49.91,1.19,1.19,0,0,0,.45,1,5.35,5.35,0,0,0,1.12.67c.45.2.93.4,1.46.61a7.15,7.15,0,0,1,1.46.74,4,4,0,0,1,1.12,1.09,2.86,2.86,0,0,1,.45,1.65,3.92,3.92,0,0,1-.35,1.69,3.41,3.41,0,0,1-1,1.2,4.33,4.33,0,0,1-1.51.73,7.68,7.68,0,0,1-3.14.14,7.59,7.59,0,0,1-1.07-.27,8.12,8.12,0,0,1-.86-.34l-.6-.3Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M330.86,216.36a5.24,5.24,0,0,1-1.71.83,7.74,7.74,0,0,1-2,.27,5.92,5.92,0,0,1-2.05-.33,4.16,4.16,0,0,1-1.51-1,4.12,4.12,0,0,1-.94-1.48,5.6,5.6,0,0,1-.32-1.92,5,5,0,0,1,.38-2,3.93,3.93,0,0,1,1-1.48,4.55,4.55,0,0,1,1.57-.92,6.21,6.21,0,0,1,2-.31,7.43,7.43,0,0,1,1.87.23,9.76,9.76,0,0,1,1.34.45v3.13h-2v-1.64a5.22,5.22,0,0,0-1.1-.12,3.35,3.35,0,0,0-1,.15,2.32,2.32,0,0,0-.87.48,2.6,2.6,0,0,0-.63.82,3.12,3.12,0,0,0,0,2.25,2.51,2.51,0,0,0,.55.83,2.42,2.42,0,0,0,.88.56,3.13,3.13,0,0,0,1.17.21,4.15,4.15,0,0,0,1.66-.29,7.06,7.06,0,0,0,1-.49Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M332.69,215.26h3.13v-5h-3.13v-2h5.44v6.94h3.29v2h-8.73Zm2.7-9.36a1.26,1.26,0,0,1,.41-.94,1.58,1.58,0,0,1,1.14-.39,1.74,1.74,0,0,1,1.19.39,1.19,1.19,0,0,1,.44.94,1.16,1.16,0,0,1-.44.94,1.84,1.84,0,0,1-1.19.36,1.66,1.66,0,0,1-1.14-.36A1.23,1.23,0,0,1,335.39,205.9Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M351.63,216.27a3.11,3.11,0,0,1-.6.42,5.25,5.25,0,0,1-.85.38,6.28,6.28,0,0,1-1.08.29,7,7,0,0,1-1.24.1,5.9,5.9,0,0,1-2-.32,4,4,0,0,1-1.52-.93,4.14,4.14,0,0,1-.95-1.48,6,6,0,0,1,0-4,4.07,4.07,0,0,1,1-1.46,4.36,4.36,0,0,1,1.52-.89,6,6,0,0,1,1.88-.29,5.78,5.78,0,0,1,1.4.18,3.92,3.92,0,0,1,1.33.61,3.23,3.23,0,0,1,1,1.17,3.87,3.87,0,0,1,.39,1.85,8.31,8.31,0,0,1-.18,1.65h-6.41a2,2,0,0,0,.24,1,1.91,1.91,0,0,0,.59.64,2.56,2.56,0,0,0,.84.37,4.57,4.57,0,0,0,1,.11,5.35,5.35,0,0,0,1.75-.24,3.81,3.81,0,0,0,1.12-.61Zm-3.84-6.35a2.73,2.73,0,0,0-.87.13,2.34,2.34,0,0,0-.74.37,2,2,0,0,0-.54.61,1.71,1.71,0,0,0-.25.81h4.37a1.76,1.76,0,0,0-2-1.92Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M352.84,208.32H356l.27,1.17h.07a3.42,3.42,0,0,1,1.17-1,3.68,3.68,0,0,1,1.84-.43,4.11,4.11,0,0,1,1.23.18,2.26,2.26,0,0,1,1,.59,2.84,2.84,0,0,1,.65,1.11,5.39,5.39,0,0,1,.24,1.73v5.53H360.1v-5.05a2.14,2.14,0,0,0-.49-1.56,1.7,1.7,0,0,0-1.24-.48,2,2,0,0,0-1.2.38,1.9,1.9,0,0,0-.74.95v5.76h-2.31v-6.93h-1.28Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M372.83,216.36a5.09,5.09,0,0,1-1.7.83,7.74,7.74,0,0,1-2.05.27,5.88,5.88,0,0,1-2.05-.33,4.29,4.29,0,0,1-1.52-1,4,4,0,0,1-.93-1.48,5.6,5.6,0,0,1-.32-1.92,5,5,0,0,1,.37-2,4.17,4.17,0,0,1,1-1.48,4.6,4.6,0,0,1,1.58-.92,6.21,6.21,0,0,1,2-.31,7.43,7.43,0,0,1,1.87.23,9.1,9.1,0,0,1,1.33.45v3.13h-1.95v-1.64a5.24,5.24,0,0,0-1.11-.12,3.28,3.28,0,0,0-1,.15,2.32,2.32,0,0,0-.87.48,2.46,2.46,0,0,0-.63.82,3,3,0,0,0,0,2.25,2.36,2.36,0,0,0,.55.83,2.42,2.42,0,0,0,.88.56,3.13,3.13,0,0,0,1.17.21,4.19,4.19,0,0,0,1.66-.29,7.71,7.71,0,0,0,1-.49Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M383.11,216.27a3.34,3.34,0,0,1-.59.42,5.81,5.81,0,0,1-.86.38,6.16,6.16,0,0,1-1.07.29,7.11,7.11,0,0,1-1.25.1,5.85,5.85,0,0,1-2-.32,4,4,0,0,1-1.52-.93,4,4,0,0,1-1-1.48,5.67,5.67,0,0,1-.33-2,5.26,5.26,0,0,1,.38-2.06,4.17,4.17,0,0,1,1-1.46,4.36,4.36,0,0,1,1.52-.89,6,6,0,0,1,1.89-.29,5.7,5.7,0,0,1,1.39.18,3.75,3.75,0,0,1,1.33.61,3.36,3.36,0,0,1,1,1.17,4,4,0,0,1,.38,1.85,8.31,8.31,0,0,1-.18,1.65h-6.4a2,2,0,0,0,.24,1,1.79,1.79,0,0,0,.58.64,2.66,2.66,0,0,0,.84.37,4.6,4.6,0,0,0,1,.11,5.39,5.39,0,0,0,1.75-.24,3.87,3.87,0,0,0,1.11-.61Zm-3.84-6.35a2.73,2.73,0,0,0-.87.13,2.45,2.45,0,0,0-.74.37,2,2,0,0,0-.54.61,1.83,1.83,0,0,0-.25.81h4.38a2,2,0,0,0-.49-1.4A1.94,1.94,0,0,0,379.27,209.92Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M397.17,213h1.95v3.56a4.32,4.32,0,0,1-1.45.64,7.37,7.37,0,0,1-4-.11,5,5,0,0,1-1.85-1.12,5.51,5.51,0,0,1-1.28-2,8.35,8.35,0,0,1-.48-3,7.5,7.5,0,0,1,.54-3.07,5.58,5.58,0,0,1,1.39-2,5.16,5.16,0,0,1,1.91-1.09,6.82,6.82,0,0,1,2.06-.33,7.84,7.84,0,0,1,1.81.18,6.52,6.52,0,0,1,1.21.41v3.7H397v-2a7.11,7.11,0,0,0-1.14-.09,3.27,3.27,0,0,0-1.29.26,2.83,2.83,0,0,0-1,.79,3.77,3.77,0,0,0-.69,1.34,6.48,6.48,0,0,0-.25,1.92,6,6,0,0,0,.23,1.75,4,4,0,0,0,.67,1.36,3,3,0,0,0,1.09.88,3.26,3.26,0,0,0,1.46.31,6.12,6.12,0,0,0,1.14-.1Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M409.26,216.27a3.11,3.11,0,0,1-.6.42,5.25,5.25,0,0,1-.85.38,6.41,6.41,0,0,1-1.07.29,7.19,7.19,0,0,1-1.25.1,5.9,5.9,0,0,1-2-.32,4,4,0,0,1-1.52-.93,4.14,4.14,0,0,1-.95-1.48,5.44,5.44,0,0,1-.33-2,5.26,5.26,0,0,1,.38-2.06,4.17,4.17,0,0,1,1-1.46,4.36,4.36,0,0,1,1.52-.89,6,6,0,0,1,1.88-.29,5.78,5.78,0,0,1,1.4.18,3.92,3.92,0,0,1,1.33.61,3.23,3.23,0,0,1,1,1.17,3.87,3.87,0,0,1,.39,1.85,9.12,9.12,0,0,1-.18,1.65H403a2,2,0,0,0,.24,1,1.79,1.79,0,0,0,.58.64,2.47,2.47,0,0,0,.84.37,4.57,4.57,0,0,0,1,.11,5.35,5.35,0,0,0,1.75-.24,3.81,3.81,0,0,0,1.12-.61Zm-3.84-6.35a2.73,2.73,0,0,0-.87.13,2.34,2.34,0,0,0-.74.37,2,2,0,0,0-.54.61,1.83,1.83,0,0,0-.25.81h4.37a1.75,1.75,0,0,0-2-1.92Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M410.47,208.32h3.11l.27,1.17h.07a3.46,3.46,0,0,1,1.18-1,3.64,3.64,0,0,1,1.83-.43a4.06,4.06,0,0,1,1.23.18,2.26,2.26,0,0,1,1,.59,2.84,2.84,0,0,1,.65,1.11,5.39,5.39,0,0,1,.24,1.73v5.53h-2.31v-5.05a2.1,2.1,0,0,0-.49-1.56,1.7,1.7,0,0,0-1.24-.48,2,2,0,0,0-1.2.38,1.9,1.9,0,0,0-.74.95v5.76h-2.31v-6.93h-1.28Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M422.81,210.28h-1.75v-2h1.75v-2l2.32-.66v2.69h4.66v2h-4.66v2.54a3.07,3.07,0,0,0,.51,2,1.8,1.8,0,0,0,1.44.62,3,3,0,0,0,.87-.12,2.85,2.85,0,0,0,.71-.29,4.07,4.07,0,0,0,.57-.39l.47-.39,1.07,1.6a4.11,4.11,0,0,1-.76.65,7,7,0,0,1-1,.51,5.78,5.78,0,0,1-1.09.35,5.4,5.4,0,0,1-1.12.12,3.84,3.84,0,0,1-3-1.11,5.17,5.17,0,0,1-1-3.53Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M440.74,216.27a3.34,3.34,0,0,1-.59.42,5.81,5.81,0,0,1-.86.38,6.16,6.16,0,0,1-1.07.29,7.11,7.11,0,0,1-1.25.1,5.85,5.85,0,0,1-2-.32,4,4,0,0,1-1.52-.93,4,4,0,0,1-1-1.48,5.67,5.67,0,0,1-.32-2,5.26,5.26,0,0,1,.37-2.06,4.17,4.17,0,0,1,1-1.46,4.36,4.36,0,0,1,1.52-.89,6,6,0,0,1,1.89-.29,5.7,5.7,0,0,1,1.39.18,3.83,3.83,0,0,1,1.33.61,3.36,3.36,0,0,1,1,1.17,4,4,0,0,1,.38,1.85,8.31,8.31,0,0,1-.18,1.65h-6.4a2,2,0,0,0,.24,1,1.81,1.81,0,0,0,.59.64,2.51,2.51,0,0,0,.83.37,4.67,4.67,0,0,0,1,.11,5.39,5.39,0,0,0,1.75-.24,3.87,3.87,0,0,0,1.11-.61Zm-3.84-6.35a2.73,2.73,0,0,0-.87.13,2.45,2.45,0,0,0-.74.37,2,2,0,0,0-.79,1.42h4.38a2,2,0,0,0-.49-1.4A1.94,1.94,0,0,0,436.9,209.92Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M442.43,215.26h2.21v-5h-2.21v-2h4l.26,1.17h.07a5,5,0,0,1,1.14-1,2.84,2.84,0,0,1,1.5-.39,2,2,0,0,1,1.68.78,4.28,4.28,0,0,1,.61,2.61h-2.08a1.72,1.72,0,0,0-.19-.93.64.64,0,0,0-.59-.3,1.74,1.74,0,0,0-1.15.36,3.6,3.6,0,0,0-.74.82v3.79h3.86v2h-8.38Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M93.1,181.53l20.42-19.22c.87-.76,1.61-1.66,2.61-1.8,1.19-.17,3,1.09,3.3,1.28s10.7,6.64,12.57,7.77l15.75,9.71c4.25-1.29,7.2-4.59,7.46-7.92a5.92,5.92,0,0,0-3-5.65l-2.48-1.53h0s-17.5-10.32-22.43-13.18a22.83,22.83,0,0,0-11-3c-4.71.09-8.62,2.32-12.24,5h0l-24,18.2,3.64,4.7a16.1,16.1,0,0,0,6.48,4.83A17.88,17.88,0,0,0,93.1,181.53Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="url(#a)"
|
||||||
|
/>
|
||||||
|
<path d="M104.87,170.45h0" transform="translate(-68.76 -130.29)" fill="url(#b)" />
|
||||||
|
<path
|
||||||
|
d="M93.1,181.53h0l11.77-11.08a3.11,3.11,0,0,1-1.35-.18,4.24,4.24,0,0,1-1.33-1.11l-7.57-8.93-14.57,11-7.26,5.5s-4.11,2.67-4,6.32c.15,4.65,5.34,6.69,5.34,6.69l30.63,13.13a35.93,35.93,0,0,0,11.38,2.53c3.39.08,6.83-1.14,10.49-2.24l24.76-8.46c1.26-.41,3.26-1.65,3.26-3.17,0-1.2-1.57-2.73-3.33-3.51a16.28,16.28,0,0,0-8.57-1.19c-4.7.44-25.95,7.65-25.95,7.65L93.16,184.23a1.28,1.28,0,0,1-.91-1.58A2.68,2.68,0,0,1,93.1,181.53Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="url(#c)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M147.75,179.27c4.25-1.29,7.2-4.59,7.46-7.92a5.92,5.92,0,0,0-3-5.65l-2.48-1.53L132,169.56l-27.13.89h0a3.11,3.11,0,0,1-1.35-.18,4.24,4.24,0,0,1-1.33-1.11l-7.57-8.93-14.57,11,3.64,4.7a16.1,16.1,0,0,0,6.48,4.83,17.88,17.88,0,0,0,2.93.73h0a66.73,66.73,0,0,0,7.06.19C107.65,182,144.08,180.38,147.75,179.27Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#e4097d"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M104.87,170.45a3.26,3.26,0,0,1-1.35-.18,4.24,4.24,0,0,1-1.33-1.11l-7.67-9-3-3.55a1.94,1.94,0,0,1-.41-1.27,2.18,2.18,0,0,1,1-1.83L110,141.9a6.43,6.43,0,0,1,1.81-.87,5.82,5.82,0,0,1,1.86.09l16.49,2.64a24.38,24.38,0,0,0,9.39-.52c1.94-.49,4.31-1.25,5.39-2.91s-.6-2.9-1.11-3.27c-2.08-1.5-4.67-1.92-7.18-2.32l-25.48-4.08a23.84,23.84,0,0,0-10.88.57A19.61,19.61,0,0,0,95,133.6L71.44,149.36a6.34,6.34,0,0,0-1.08,9.38l.21.27,13.12,17a16.1,16.1,0,0,0,6.48,4.83,17.88,17.88,0,0,0,2.93.73Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="url(#d)"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="main-content">
|
||||||
|
<div class="success-message">
|
||||||
|
<div class="success-icon">✓</div>
|
||||||
|
<h1>Verlängerung erfolgreich!</h1>
|
||||||
|
<p class="success-text">Ihre Pädagogische Jahreskarte wurde erfolgreich verlängert.</p>
|
||||||
|
|
||||||
|
<div class="card-info">
|
||||||
|
<h3>Ihre Vorteile</h3>
|
||||||
|
<p>
|
||||||
|
Mit der Jahreskarte erhalten Sie ein Jahr lang freien Eintritt in die Ausstellung,
|
||||||
|
Sonderausstellung oder zu den regulären Science Dome Shows.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="contact-info">
|
||||||
|
<p>
|
||||||
|
Bei Fragen zu unseren Angeboten für Bildungseinrichtungen und Pädagogen sind wir gerne
|
||||||
|
für Sie da.
|
||||||
|
</p>
|
||||||
|
<div class="contact-item">
|
||||||
|
<strong>E-Mail-Adresse</strong>
|
||||||
|
<a href="mailto:schulkommunikation@experimenta.science"
|
||||||
|
>schulkommunikation@experimenta.science</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<p>
|
||||||
|
Die neuen Bedingungen der Pädagogischen Jahreskarten finden Sie
|
||||||
|
<a
|
||||||
|
href="https://www.experimenta.science/paedagogische-jahreskarte/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>hier</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="https://www.experimenta.science/" class="back-button"
|
||||||
|
>Zur experimenta Startseite</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="footer-content">
|
||||||
|
<div class="footer-bottom">
|
||||||
|
<p>© 2025 experimenta gGmbH – Das Science Center. Alle Rechte vorbehalten.</p>
|
||||||
|
<div class="footer-bottom-links">
|
||||||
|
<a href="https://www.experimenta.science/kontakt/">Kontakt</a>
|
||||||
|
<a href="https://www.experimenta.science/impressum/">Impressum</a>
|
||||||
|
<a href="https://www.experimenta.science/datenschutz/">Datenschutz</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
{{#payload.id}}
|
||||||
|
<!-- ID: {{payload.id}}-->{{/payload.id}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
796
docs/design-examples/design-example3-error.html
Normal file
@@ -0,0 +1,796 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Fehler bei der Verlängerung | experimenta</title>
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
background: linear-gradient(135deg, #2e1065 0%, #1a0a3a 50%, #0f051d 100%);
|
||||||
|
background-attachment: fixed;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
color: white;
|
||||||
|
line-height: 1.6;
|
||||||
|
min-height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Simplified Header */
|
||||||
|
.header-wrapper {
|
||||||
|
background: rgba(46, 16, 101, 0.95);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
position: relative;
|
||||||
|
z-index: 100;
|
||||||
|
padding: 30px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: white;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-svg {
|
||||||
|
width: 300px;
|
||||||
|
height: auto;
|
||||||
|
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content */
|
||||||
|
.main-content {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 40px 20px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 60px 40px;
|
||||||
|
margin: 40px 0;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-icon {
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #e53e3e;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 100%;
|
||||||
|
font-size: 4rem;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
animation: pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 36px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: white;
|
||||||
|
font-weight: 300;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-text {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-info {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 40px 30px;
|
||||||
|
margin: 30px 0;
|
||||||
|
text-align: center;
|
||||||
|
border-left: 4px solid #f59d24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-info p {
|
||||||
|
font-size: 18px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item strong {
|
||||||
|
color: #f59d24;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item a {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 400;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
word-break: break-all;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item a:hover {
|
||||||
|
color: #ffb347;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item .phone-number {
|
||||||
|
color: white;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
background: #e6007e;
|
||||||
|
background-image: linear-gradient(to left, #e6007e, #e6007e, #e40521, #e6007e);
|
||||||
|
background-size: 300%;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 10px 30px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background-position 1s,
|
||||||
|
all 0.3s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 30px;
|
||||||
|
text-transform: none;
|
||||||
|
line-height: 1.7em;
|
||||||
|
position: relative;
|
||||||
|
outline: 0;
|
||||||
|
border-width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button:hover {
|
||||||
|
background-position: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Simplified Footer */
|
||||||
|
.footer {
|
||||||
|
background: linear-gradient(135deg, #1a0a3a 0%, #0f051d 100%);
|
||||||
|
margin-top: 80px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 60px 20px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-main {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2.5fr 1fr 1fr 1.5fr;
|
||||||
|
gap: 50px;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section h3 {
|
||||||
|
color: #e91e63;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section p {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links li {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links a {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links a:hover {
|
||||||
|
color: #ff4081;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-logo-svg {
|
||||||
|
width: 200px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partner-section {
|
||||||
|
margin: 30px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partner-logos {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partner-logo {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
min-width: 120px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partner-logo:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links a {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: rgba(233, 30, 99, 0.2);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links a:hover {
|
||||||
|
background: #e91e63;
|
||||||
|
transform: translateY(-3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom {
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
padding-top: 30px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom p {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom-links a {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom-links a:hover {
|
||||||
|
color: #ff4081;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recognition-section {
|
||||||
|
text-align: center;
|
||||||
|
margin: 40px 0;
|
||||||
|
padding: 30px;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recognition-section h4 {
|
||||||
|
color: #e91e63;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body {
|
||||||
|
background-attachment: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
padding: 0 15px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-svg {
|
||||||
|
width: 250px;
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
padding: 40px 15px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
padding: 40px 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-info {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 25px 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-info p,
|
||||||
|
.contact-item a,
|
||||||
|
.contact-item strong,
|
||||||
|
.contact-item .phone-number {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-text {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-main {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom p,
|
||||||
|
.footer-bottom-links a {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom-links {
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-logo-svg {
|
||||||
|
width: 180px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-svg {
|
||||||
|
width: 200px;
|
||||||
|
max-width: 85%;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
padding: 30px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
padding: 30px 15px;
|
||||||
|
margin: 15px 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-info {
|
||||||
|
padding: 20px 15px;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-info p,
|
||||||
|
.contact-item a,
|
||||||
|
.contact-item strong,
|
||||||
|
.contact-item .phone-number {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-text {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
padding: 8px 24px;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom p,
|
||||||
|
.footer-bottom-links a {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom-links {
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<!-- Simplified Header -->
|
||||||
|
<header class="header-wrapper">
|
||||||
|
<div class="header-content">
|
||||||
|
<a href="https://www.experimenta.science/" class="logo">
|
||||||
|
<svg
|
||||||
|
class="logo-svg"
|
||||||
|
viewBox="0 0 382.94 87.17"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<linearGradient
|
||||||
|
id="a"
|
||||||
|
x1="102.63"
|
||||||
|
y1="152.32"
|
||||||
|
x2="135.19"
|
||||||
|
y2="191.11"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop offset="0.16" stop-color="#bf144c" />
|
||||||
|
<stop offset="0.29" stop-color="#ce0f60" />
|
||||||
|
<stop offset="0.47" stop-color="#de0b75" />
|
||||||
|
<stop offset="0.59" stop-color="#e4097d" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="b"
|
||||||
|
x1="104.87"
|
||||||
|
y1="170.45"
|
||||||
|
x2="104.87"
|
||||||
|
y2="170.45"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop offset="0.16" stop-color="#bf144c" />
|
||||||
|
<stop offset="0.29" stop-color="#ab1a4e" />
|
||||||
|
<stop offset="0.43" stop-color="#9f1d4f" />
|
||||||
|
<stop offset="0.57" stop-color="#9b1e4f" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="c"
|
||||||
|
x1="68.79"
|
||||||
|
y1="182.84"
|
||||||
|
x2="154.66"
|
||||||
|
y2="182.84"
|
||||||
|
xlink:href="#b"
|
||||||
|
/>
|
||||||
|
<linearGradient
|
||||||
|
id="d"
|
||||||
|
x1="94.04"
|
||||||
|
y1="182.21"
|
||||||
|
x2="114.5"
|
||||||
|
y2="126"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop offset="0.22" stop-color="#e4097d" />
|
||||||
|
<stop offset="0.32" stop-color="#e4115e" />
|
||||||
|
<stop offset="0.45" stop-color="#e5193d" />
|
||||||
|
<stop offset="0.55" stop-color="#e51e28" />
|
||||||
|
<stop offset="0.62" stop-color="#e52021" />
|
||||||
|
<stop offset="0.9" stop-color="#f7a822" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<polygon
|
||||||
|
points="143.78 50 151.18 39.6 144.43 39.6 139.68 46.33 135.13 39.6 127.79 39.6 135.32 50.08 127.29 61.23 134.09 61.23 139.3 53.78 144.43 61.23 151.59 61.23 143.78 50"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M245.79,175.33a9.2,9.2,0,0,0-1.85-3.39,7.28,7.28,0,0,0-2.9-2,10.29,10.29,0,0,0-3.65-.63c-3.12,0-5.47.95-7,2.82l-.56-2.23h-7.72v5.29h3.13v24.7h6.13v-8.4a8.29,8.29,0,0,0,1.7.42,17.3,17.3,0,0,0,2.6.19,11.8,11.8,0,0,0,4.53-.82,9.29,9.29,0,0,0,3.39-2.39,10.78,10.78,0,0,0,2.11-3.78,15.69,15.69,0,0,0,.74-5A16,16,0,0,0,245.79,175.33Zm-12.76,0a5.26,5.26,0,0,1,2.73-.75,4,4,0,0,1,3.12,1.4,5.85,5.85,0,0,1,1.25,4,10.56,10.56,0,0,1-.4,3.12,5.84,5.84,0,0,1-1.1,2.09,4.11,4.11,0,0,1-1.62,1.18,7.15,7.15,0,0,1-4.21.12,4.71,4.71,0,0,1-1.43-.58v-8.48A3.79,3.79,0,0,1,233,175.35Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M270.8,174.1a8.1,8.1,0,0,0-2.42-2.86,9.3,9.3,0,0,0-3.24-1.5,14.1,14.1,0,0,0-7.91.28,10.48,10.48,0,0,0-3.7,2.14,10,10,0,0,0-2.49,3.58,12.71,12.71,0,0,0-.9,5,13.24,13.24,0,0,0,.79,4.74,9.75,9.75,0,0,0,6,5.88,14.71,14.71,0,0,0,4.91.77,15.93,15.93,0,0,0,3-.26,14.61,14.61,0,0,0,2.59-.68,13.18,13.18,0,0,0,2.05-.92,8,8,0,0,0,1.47-1l.19-.18L269,184.85l-.33.28a8.34,8.34,0,0,1-2.51,1.36,11.84,11.84,0,0,1-4,.57,9.83,9.83,0,0,1-2.28-.26,6,6,0,0,1-1.85-.81,4.16,4.16,0,0,1-1.28-1.39,4.33,4.33,0,0,1-.5-1.75h15l.05-.28a19.77,19.77,0,0,0,.43-4A9.75,9.75,0,0,0,270.8,174.1Zm-11.06.54a6.52,6.52,0,0,1,1.95-.29,4.29,4.29,0,0,1,3.24,1.11,4.13,4.13,0,0,1,1.05,2.72h-9.55a4.05,4.05,0,0,1,.5-1.39,4.45,4.45,0,0,1,1.18-1.33A5.21,5.21,0,0,1,259.74,174.64Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M194.59,174.1a8,8,0,0,0-2.43-2.86,9.26,9.26,0,0,0-3.23-1.5A14.09,14.09,0,0,0,181,170a10.48,10.48,0,0,0-3.7,2.14,10,10,0,0,0-2.49,3.58,12.7,12.7,0,0,0-.91,5,13.23,13.23,0,0,0,.8,4.74,9.75,9.75,0,0,0,6,5.88,14.71,14.71,0,0,0,4.91.77,15.82,15.82,0,0,0,3-.26,14.38,14.38,0,0,0,2.59-.68,12.91,12.91,0,0,0,2.06-.92,8,8,0,0,0,1.47-1l.19-.18-2.11-4.18-.34.28a8.22,8.22,0,0,1-2.51,1.36,11.78,11.78,0,0,1-4,.57,9.7,9.7,0,0,1-2.28-.26,6,6,0,0,1-1.86-.81,4.23,4.23,0,0,1-1.27-1.39,4.33,4.33,0,0,1-.5-1.75h15l.06-.28a20.47,20.47,0,0,0,.42-4A9.75,9.75,0,0,0,194.59,174.1Zm-11.06.54a6.52,6.52,0,0,1,1.95-.29,4.31,4.31,0,0,1,3.24,1.11,4.07,4.07,0,0,1,1,2.72h-9.54a4,4,0,0,1,.49-1.39,4.35,4.35,0,0,1,1.19-1.33A5.21,5.21,0,0,1,183.53,174.64Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M298.09,178.12v-.34c0-3-.49-5.07-1.52-6.37a5.07,5.07,0,0,0-4.22-2,6.89,6.89,0,0,0-3.69,1,11.46,11.46,0,0,0-2.44,2l-.58-2.52h-10v5.29h5.18v11h-5.18v5.3H296v-5.3h-9.08v-8.47a8.94,8.94,0,0,1,1.61-1.76,3.84,3.84,0,0,1,2.48-.76,1.2,1.2,0,0,1,1.11.54,3.85,3.85,0,0,1,.39,2v.34Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M350.79,173.27a5.61,5.61,0,0,0-1.16-2.29,4.05,4.05,0,0,0-1.79-1.21,7.17,7.17,0,0,0-2.24-.33,6.19,6.19,0,0,0-3.12.82,5,5,0,0,0-1.79,1.7,3.66,3.66,0,0,0-1.7-1.81,6.39,6.39,0,0,0-2.92-.71,6,6,0,0,0-3.17.82,5.75,5.75,0,0,0-1.71,1.57l-.53-1.93h-4.78v21.62h6V176.07a1.89,1.89,0,0,1,.72-.94,2,2,0,0,1,1.92-.27,1.05,1.05,0,0,1,.53.44,3.5,3.5,0,0,1,.41,1.11,9.26,9.26,0,0,1,.17,2v13.16h6V175.94a1.5,1.5,0,0,1,.63-.9,2.12,2.12,0,0,1,1.18-.31,1.62,1.62,0,0,1,1.29.63,3.88,3.88,0,0,1,.56,2.42v13.74h6V176.86A14.23,14.23,0,0,0,350.79,173.27Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M375.88,174.1a8,8,0,0,0-2.43-2.86,9.3,9.3,0,0,0-3.24-1.5,14.1,14.1,0,0,0-7.91.28,10.44,10.44,0,0,0-3.69,2.14,10.11,10.11,0,0,0-2.5,3.58,12.71,12.71,0,0,0-.9,5,13.46,13.46,0,0,0,.79,4.74,9.89,9.89,0,0,0,2.32,3.6,10,10,0,0,0,3.71,2.28,14.65,14.65,0,0,0,4.9.77,15.93,15.93,0,0,0,3-.26,14.61,14.61,0,0,0,2.59-.68,13.77,13.77,0,0,0,2.06-.92,8.18,8.18,0,0,0,1.47-1l.19-.18-2.12-4.18-.33.28a8.34,8.34,0,0,1-2.51,1.36,11.82,11.82,0,0,1-4,.57,9.7,9.7,0,0,1-2.28-.26,5.93,5.93,0,0,1-1.86-.81,4.23,4.23,0,0,1-1.27-1.39,4.18,4.18,0,0,1-.5-1.75h15l.06-.28a20.52,20.52,0,0,0,.43-4A9.75,9.75,0,0,0,375.88,174.1Zm-11.06.54a6.5,6.5,0,0,1,1.94-.29,4.33,4.33,0,0,1,3.25,1.11,4.18,4.18,0,0,1,1.05,2.72h-9.55a3.84,3.84,0,0,1,.49-1.39,4.35,4.35,0,0,1,1.19-1.33A5.21,5.21,0,0,1,364.82,174.64Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M401.33,174a7,7,0,0,0-1.61-2.75,5.79,5.79,0,0,0-2.46-1.46,10.19,10.19,0,0,0-3-.44,9,9,0,0,0-7,3.07l-.57-2.48h-7.94v5.29h3v16.33h6.13V177.67a4.13,4.13,0,0,1,1.59-2,4.48,4.48,0,0,1,2.62-.83,3.6,3.6,0,0,1,2.67,1,4.72,4.72,0,0,1,1,3.43v12.24h6.14V178.15A13.05,13.05,0,0,0,401.33,174Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M425.62,183.62l-1.41,1.17a9.47,9.47,0,0,1-1.29.89,7,7,0,0,1-3.53.92,3.83,3.83,0,0,1-3.12-1.32,7,7,0,0,1-1.14-4.45v-5.64h11V169.9h-11v-6.43L409,165.21v4.69h-4.14v5.29H409v5.64c0,3.87.83,6.74,2.46,8.54s4.09,2.73,7.31,2.73a12.69,12.69,0,0,0,2.7-.3,14.51,14.51,0,0,0,2.64-.84,15.43,15.43,0,0,0,2.35-1.24,8.09,8.09,0,0,0,1.85-1.59l.17-.2Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M449,186.22c0-.27-.06-.54-.1-.81-.05-.71-.08-1.36-.08-1.94,0-.78.05-1.66.14-2.65s.16-2.25.16-3.67a10.5,10.5,0,0,0-.44-3.12,6,6,0,0,0-1.45-2.44,6.56,6.56,0,0,0-2.6-1.57,11.86,11.86,0,0,0-3.81-.54,22.68,22.68,0,0,0-5.29.55,23.45,23.45,0,0,0-3.93,1.32l-.28.12,1.49,4.95.36-.17a22.8,22.8,0,0,1,2.83-1.07,12,12,0,0,1,3.71-.53,4.18,4.18,0,0,1,2.5.62,2.33,2.33,0,0,1,.77,2v.8a2.53,2.53,0,0,1,0,.47l-1.71-.15c-.56,0-1.1-.08-1.61-.08a17.54,17.54,0,0,0-3.92.41,9.29,9.29,0,0,0-3.07,1.26,5.82,5.82,0,0,0-2,2.22,7.07,7.07,0,0,0-.68,3.19,6.12,6.12,0,0,0,1.94,4.7,7.43,7.43,0,0,0,5.19,1.76,8.26,8.26,0,0,0,4.33-1,7.94,7.94,0,0,0,2.17-1.9l.49,2.56h7.66v-5.3Zm-12.43-2.67a2.4,2.4,0,0,1,.88-.71,4.48,4.48,0,0,1,1.31-.43,7.73,7.73,0,0,1,1.51-.15,13.48,13.48,0,0,1,1.73.11c.38,0,.69.09.94.13v2.13a4.16,4.16,0,0,1-4.08,2.05,2.92,2.92,0,0,1-2-.55,2.07,2.07,0,0,1-.57-1.57A1.68,1.68,0,0,1,436.52,183.55Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<polygon
|
||||||
|
points="249.76 48.18 241.03 39.45 230.96 49.53 230.96 52.96 233.6 50.29 240.61 57.31 238.01 59.91 239.67 61.58 253.68 47.56 253.68 44.27 249.76 48.18"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M314.12,162.94a5.56,5.56,0,1,1-5.55-5.55A5.55,5.55,0,0,1,314.12,162.94Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M275.61,204.76l.56,0,.9-.06,1.06-.05,1,0a6.75,6.75,0,0,1,2.74.49,4.2,4.2,0,0,1,1.72,1.32,5.15,5.15,0,0,1,.88,2,11.58,11.58,0,0,1,.25,2.5,11.24,11.24,0,0,1-.25,2.4,5.62,5.62,0,0,1-.9,2.09,4.62,4.62,0,0,1-1.79,1.48,6.81,6.81,0,0,1-2.95.56l-.73,0-.91,0-.89-.06a4.37,4.37,0,0,1-.65-.06Zm3.57,2c-.22,0-.44,0-.66,0l-.48.05v8.34l.22,0h.29l.29,0h.24a3.13,3.13,0,0,0,1.64-.38,2.41,2.41,0,0,0,.92-1,4.31,4.31,0,0,0,.39-1.4,13.77,13.77,0,0,0,.09-1.57,13.6,13.6,0,0,0-.08-1.4,4.16,4.16,0,0,0-.38-1.34,2.46,2.46,0,0,0-.88-1A2.85,2.85,0,0,0,279.18,206.76Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M286.81,208.92a11,11,0,0,1,1.65-.55,9.5,9.5,0,0,1,2.21-.23,5.19,5.19,0,0,1,1.58.22,2.63,2.63,0,0,1,1.05.64,2.38,2.38,0,0,1,.57,1,4.26,4.26,0,0,1,.18,1.28c0,.61,0,1.13-.06,1.55s-.06.81-.06,1.14,0,.53,0,.84a3.93,3.93,0,0,1,.06.48h1.15v2h-3L292,216h-.08a3.31,3.31,0,0,1-1,1,3.46,3.46,0,0,1-1.77.4,3,3,0,0,1-2.1-.71,2.45,2.45,0,0,1-.78-1.89,2.86,2.86,0,0,1,.27-1.29,2.28,2.28,0,0,1,.8-.89,4,4,0,0,1,1.25-.52,7.54,7.54,0,0,1,1.64-.16l.67,0c.24,0,.52,0,.87.07a2.07,2.07,0,0,0,0-.35v-.34a1.13,1.13,0,0,0-.39-1,1.86,1.86,0,0,0-1.15-.29,5.11,5.11,0,0,0-1.62.23,9.05,9.05,0,0,0-1.22.46Zm3,6.53a2.12,2.12,0,0,0,1.27-.31,2.1,2.1,0,0,0,.61-.67v-1.06a4.92,4.92,0,0,0-.52-.08,6.06,6.06,0,0,0-.76-.05,3.05,3.05,0,0,0-.67.07,2.22,2.22,0,0,0-.6.19,1.31,1.31,0,0,0-.42.35.86.86,0,0,0-.16.51,1,1,0,0,0,.29.78A1.39,1.39,0,0,0,289.84,215.45Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M302.86,214.76a.64.64,0,0,0-.44-.57,6.8,6.8,0,0,0-1.07-.39q-.64-.18-1.41-.36a6.44,6.44,0,0,1-1.4-.52,3.49,3.49,0,0,1-1.08-.84,1.93,1.93,0,0,1-.44-1.3,2.2,2.2,0,0,1,.3-1.16,2.59,2.59,0,0,1,.8-.85,3.82,3.82,0,0,1,1.21-.52,6.09,6.09,0,0,1,1.52-.18,7.73,7.73,0,0,1,1.53.13,7.07,7.07,0,0,1,1.15.3,4.28,4.28,0,0,1,.83.4l.6.4-1,1.58-.61-.33-.73-.31a6.89,6.89,0,0,0-.81-.23,3.81,3.81,0,0,0-.82-.09,3.15,3.15,0,0,0-1.21.2.63.63,0,0,0-.46.6c0,.21.14.39.43.52a6.8,6.8,0,0,0,1.08.36l1.4.37a8.06,8.06,0,0,1,1.41.5,3.52,3.52,0,0,1,1.08.81,1.89,1.89,0,0,1,.43,1.28,2.57,2.57,0,0,1-1,2.13,4.69,4.69,0,0,1-2.92.77,7,7,0,0,1-2.64-.45,6,6,0,0,1-1.79-1.06l1.07-1.67a4.73,4.73,0,0,0,.61.43,5.31,5.31,0,0,0,.86.44,7.61,7.61,0,0,0,1,.33,4.55,4.55,0,0,0,1.08.13,2.26,2.26,0,0,0,1-.19A.68.68,0,0,0,302.86,214.76Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M311.81,213.14h1.95v1.76l.11,0a5.28,5.28,0,0,0,.9.29,4.3,4.3,0,0,0,1,.12,2.61,2.61,0,0,0,1.54-.4,1.27,1.27,0,0,0,.55-1.08,1.22,1.22,0,0,0-.45-1,4.42,4.42,0,0,0-1.12-.66c-.44-.19-.93-.37-1.46-.57a7.09,7.09,0,0,1-1.46-.72,4.15,4.15,0,0,1-1.12-1.09,3,3,0,0,1-.45-1.71,3.35,3.35,0,0,1,.31-1.46,3.4,3.4,0,0,1,.87-1.15,4.1,4.1,0,0,1,1.35-.75,5.29,5.29,0,0,1,1.74-.27,11.64,11.64,0,0,1,2.13.2,6.05,6.05,0,0,1,1.68.53v3.62h-2v-2l-.11,0c-.26-.07-.54-.12-.84-.17a6.8,6.8,0,0,0-.9-.06,2.27,2.27,0,0,0-1.34.34,1,1,0,0,0-.49.91,1.19,1.19,0,0,0,.45,1,5.35,5.35,0,0,0,1.12.67c.45.2.93.4,1.46.61a7.15,7.15,0,0,1,1.46.74,4,4,0,0,1,1.12,1.09,2.86,2.86,0,0,1,.45,1.65,3.92,3.92,0,0,1-.35,1.69,3.41,3.41,0,0,1-1,1.2,4.33,4.33,0,0,1-1.51.73,7.68,7.68,0,0,1-3.14.14,7.59,7.59,0,0,1-1.07-.27,8.12,8.12,0,0,1-.86-.34l-.6-.3Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M330.86,216.36a5.24,5.24,0,0,1-1.71.83,7.74,7.74,0,0,1-2,.27,5.92,5.92,0,0,1-2.05-.33,4.16,4.16,0,0,1-1.51-1,4.12,4.12,0,0,1-.94-1.48,5.6,5.6,0,0,1-.32-1.92,5,5,0,0,1,.38-2,3.93,3.93,0,0,1,1-1.48,4.55,4.55,0,0,1,1.57-.92,6.21,6.21,0,0,1,2-.31,7.43,7.43,0,0,1,1.87.23,9.76,9.76,0,0,1,1.34.45v3.13h-2v-1.64a5.22,5.22,0,0,0-1.1-.12,3.35,3.35,0,0,0-1,.15,2.32,2.32,0,0,0-.87.48,2.6,2.6,0,0,0-.63.82,3.12,3.12,0,0,0,0,2.25,2.51,2.51,0,0,0,.55.83,2.42,2.42,0,0,0,.88.56,3.13,3.13,0,0,0,1.17.21,4.15,4.15,0,0,0,1.66-.29,7.06,7.06,0,0,0,1-.49Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M332.69,215.26h3.13v-5h-3.13v-2h5.44v6.94h3.29v2h-8.73Zm2.7-9.36a1.26,1.26,0,0,1,.41-.94,1.58,1.58,0,0,1,1.14-.39,1.74,1.74,0,0,1,1.19.39,1.19,1.19,0,0,1,.44.94,1.16,1.16,0,0,1-.44.94,1.84,1.84,0,0,1-1.19.36,1.66,1.66,0,0,1-1.14-.36A1.23,1.23,0,0,1,335.39,205.9Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M351.63,216.27a3.11,3.11,0,0,1-.6.42,5.25,5.25,0,0,1-.85.38,6.28,6.28,0,0,1-1.08.29,7,7,0,0,1-1.24.1,5.9,5.9,0,0,1-2-.32,4,4,0,0,1-1.52-.93,4.14,4.14,0,0,1-.95-1.48,6,6,0,0,1,0-4,4.07,4.07,0,0,1,1-1.46,4.36,4.36,0,0,1,1.52-.89,6,6,0,0,1,1.88-.29,5.78,5.78,0,0,1,1.4.18,3.92,3.92,0,0,1,1.33.61,3.23,3.23,0,0,1,1,1.17,3.87,3.87,0,0,1,.39,1.85,8.31,8.31,0,0,1-.18,1.65h-6.41a2,2,0,0,0,.24,1,1.91,1.91,0,0,0,.59.64,2.56,2.56,0,0,0,.84.37,4.57,4.57,0,0,0,1,.11,5.35,5.35,0,0,0,1.75-.24,3.81,3.81,0,0,0,1.12-.61Zm-3.84-6.35a2.73,2.73,0,0,0-.87.13,2.34,2.34,0,0,0-.74.37,2,2,0,0,0-.54.61,1.71,1.71,0,0,0-.25.81h4.37a1.76,1.76,0,0,0-2-1.92Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M352.84,208.32H356l.27,1.17h.07a3.42,3.42,0,0,1,1.17-1,3.68,3.68,0,0,1,1.84-.43,4.11,4.11,0,0,1,1.23.18,2.26,2.26,0,0,1,1,.59,2.84,2.84,0,0,1,.65,1.11,5.39,5.39,0,0,1,.24,1.73v5.53H360.1v-5.05a2.14,2.14,0,0,0-.49-1.56,1.7,1.7,0,0,0-1.24-.48,2,2,0,0,0-1.2.38,1.9,1.9,0,0,0-.74.95v5.76h-2.31v-6.93h-1.28Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M372.83,216.36a5.09,5.09,0,0,1-1.7.83,7.74,7.74,0,0,1-2.05.27,5.88,5.88,0,0,1-2.05-.33,4.29,4.29,0,0,1-1.52-1,4,4,0,0,1-.93-1.48,5.6,5.6,0,0,1-.32-1.92,5,5,0,0,1,.37-2,4.17,4.17,0,0,1,1-1.48,4.6,4.6,0,0,1,1.58-.92,6.21,6.21,0,0,1,2-.31,7.43,7.43,0,0,1,1.87.23,9.1,9.1,0,0,1,1.33.45v3.13h-1.95v-1.64a5.24,5.24,0,0,0-1.11-.12,3.28,3.28,0,0,0-1,.15,2.32,2.32,0,0,0-.87.48,2.46,2.46,0,0,0-.63.82,3,3,0,0,0,0,2.25,2.36,2.36,0,0,0,.55.83,2.42,2.42,0,0,0,.88.56,3.13,3.13,0,0,0,1.17.21,4.19,4.19,0,0,0,1.66-.29,7.71,7.71,0,0,0,1-.49Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M383.11,216.27a3.34,3.34,0,0,1-.59.42,5.81,5.81,0,0,1-.86.38,6.16,6.16,0,0,1-1.07.29,7.11,7.11,0,0,1-1.25.1,5.85,5.85,0,0,1-2-.32,4,4,0,0,1-1.52-.93,4,4,0,0,1-1-1.48,5.67,5.67,0,0,1-.33-2,5.26,5.26,0,0,1,.38-2.06,4.17,4.17,0,0,1,1-1.46,4.36,4.36,0,0,1,1.52-.89,6,6,0,0,1,1.89-.29,5.7,5.7,0,0,1,1.39.18,3.75,3.75,0,0,1,1.33.61,3.36,3.36,0,0,1,1,1.17,4,4,0,0,1,.38,1.85,8.31,8.31,0,0,1-.18,1.65h-6.4a2,2,0,0,0,.24,1,1.79,1.79,0,0,0,.58.64,2.66,2.66,0,0,0,.84.37,4.6,4.6,0,0,0,1,.11,5.39,5.39,0,0,0,1.75-.24,3.87,3.87,0,0,0,1.11-.61Zm-3.84-6.35a2.73,2.73,0,0,0-.87.13,2.45,2.45,0,0,0-.74.37,2,2,0,0,0-.79,1.42h4.38a2,2,0,0,0-.49-1.4A1.94,1.94,0,0,0,379.27,209.92Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M397.17,213h1.95v3.56a4.32,4.32,0,0,1-1.45.64,7.37,7.37,0,0,1-4-.11,5,5,0,0,1-1.85-1.12,5.51,5.51,0,0,1-1.28-2,8.35,8.35,0,0,1-.48-3,7.5,7.5,0,0,1,.54-3.07,5.58,5.58,0,0,1,1.39-2,5.16,5.16,0,0,1,1.91-1.09,6.82,6.82,0,0,1,2.06-.33,7.84,7.84,0,0,1,1.81.18,6.52,6.52,0,0,1,1.21.41v3.7H397v-2a7.11,7.11,0,0,0-1.14-.09,3.27,3.27,0,0,0-1.29.26,2.83,2.83,0,0,0-1,.79,3.77,3.77,0,0,0-.69,1.34,6.48,6.48,0,0,0-.25,1.92,6,6,0,0,0,.23,1.75,4,4,0,0,0,.67,1.36,3,3,0,0,0,1.09.88,3.26,3.26,0,0,0,1.46.31,6.12,6.12,0,0,0,1.14-.1Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M409.26,216.27a3.11,3.11,0,0,1-.6.42,5.25,5.25,0,0,1-.85.38,6.41,6.41,0,0,1-1.07.29,7.19,7.19,0,0,1-1.25.1,5.9,5.9,0,0,1-2-.32,4,4,0,0,1-1.52-.93,4.14,4.14,0,0,1-.95-1.48,5.44,5.44,0,0,1-.33-2,5.26,5.26,0,0,1,.38-2.06,4.17,4.17,0,0,1,1-1.46,4.36,4.36,0,0,1,1.52-.89,6,6,0,0,1,1.88-.29,5.78,5.78,0,0,1,1.4.18,3.92,3.92,0,0,1,1.33.61,3.23,3.23,0,0,1,1,1.17,3.87,3.87,0,0,1,.39,1.85,9.12,9.12,0,0,1-.18,1.65H403a2,2,0,0,0,.24,1,1.79,1.79,0,0,0,.58.64,2.47,2.47,0,0,0,.84.37,4.57,4.57,0,0,0,1,.11,5.35,5.35,0,0,0,1.75-.24,3.81,3.81,0,0,0,1.12-.61Zm-3.84-6.35a2.73,2.73,0,0,0-.87.13,2.34,2.34,0,0,0-.74.37,2,2,0,0,0-.54.61,1.83,1.83,0,0,0-.25.81h4.37a1.75,1.75,0,0,0-2-1.92Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M410.47,208.32h3.11l.27,1.17h.07a3.46,3.46,0,0,1,1.18-1,3.64,3.64,0,0,1,1.83-.43,4.06,4.06,0,0,1,1.23.18,2.26,2.26,0,0,1,1,.59,2.84,2.84,0,0,1,.65,1.11,5.39,5.39,0,0,1,.24,1.73v5.53h-2.31v-5.05a2.1,2.1,0,0,0-.49-1.56,1.7,1.7,0,0,0-1.24-.48,2,2,0,0,0-1.2.38,1.9,1.9,0,0,0-.74.95v5.76h-2.31v-6.93h-1.28Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M422.81,210.28h-1.75v-2h1.75v-2l2.32-.66v2.69h4.66v2h-4.66v2.54a3.07,3.07,0,0,0,.51,2,1.8,1.8,0,0,0,1.44.62,3,3,0,0,0,.87-.12,2.85,2.85,0,0,0,.71-.29,4.07,4.07,0,0,0,.57-.39l.47-.39,1.07,1.6a4.11,4.11,0,0,1-.76.65,7,7,0,0,1-1,.51,5.78,5.78,0,0,1-1.09.35,5.4,5.4,0,0,1-1.12.12,3.84,3.84,0,0,1-3-1.11,5.17,5.17,0,0,1-1-3.53Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M440.74,216.27a3.34,3.34,0,0,1-.59.42,5.81,5.81,0,0,1-.86.38,6.16,6.16,0,0,1-1.07.29,7.11,7.11,0,0,1-1.25.1,5.85,5.85,0,0,1-2-.32,4,4,0,0,1-1.52-.93,4,4,0,0,1-1-1.48,5.67,5.67,0,0,1-.32-2,5.26,5.26,0,0,1,.37-2.06,4.17,4.17,0,0,1,1-1.46,4.36,4.36,0,0,1,1.52-.89,6,6,0,0,1,1.89-.29,5.7,5.7,0,0,1,1.39.18,3.83,3.83,0,0,1,1.33.61,3.36,3.36,0,0,1,1,1.17,4,4,0,0,1,.38,1.85,8.31,8.31,0,0,1-.18,1.65h-6.4a2,2,0,0,0,.24,1,1.81,1.81,0,0,0,.59.64,2.51,2.51,0,0,0,.83.37,4.67,4.67,0,0,0,1,.11,5.39,5.39,0,0,0,1.75-.24,3.87,3.87,0,0,0,1.11-.61Zm-3.84-6.35a2.73,2.73,0,0,0-.87.13,2.45,2.45,0,0,0-.74.37,2,2,0,0,0-.79,1.42h4.38a2,2,0,0,0-.49-1.4A1.94,1.94,0,0,0,436.9,209.92Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M442.43,215.26h2.21v-5h-2.21v-2h4l.26,1.17h.07a5,5,0,0,1,1.14-1,2.84,2.84,0,0,1,1.5-.39,2,2,0,0,1,1.68.78,4.28,4.28,0,0,1,.61,2.61h-2.08a1.72,1.72,0,0,0-.19-.93.64.64,0,0,0-.59-.3,1.74,1.74,0,0,0-1.15.36,3.6,3.6,0,0,0-.74.82v3.79h3.86v2h-8.38Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M93.1,181.53l20.42-19.22c.87-.76,1.61-1.66,2.61-1.8,1.19-.17,3,1.09,3.3,1.28s10.7,6.64,12.57,7.77l15.75,9.71c4.25-1.29,7.2-4.59,7.46-7.92a5.92,5.92,0,0,0-3-5.65l-2.48-1.53h0s-17.5-10.32-22.43-13.18a22.83,22.83,0,0,0-11-3c-4.71.09-8.62,2.32-12.24,5h0l-24,18.2,3.64,4.7a16.1,16.1,0,0,0,6.48,4.83A17.88,17.88,0,0,0,93.1,181.53Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="url(#a)"
|
||||||
|
/>
|
||||||
|
<path d="M104.87,170.45h0" transform="translate(-68.76 -130.29)" fill="url(#b)" />
|
||||||
|
<path
|
||||||
|
d="M93.1,181.53h0l11.77-11.08a3.11,3.11,0,0,1-1.35-.18,4.24,4.24,0,0,1-1.33-1.11l-7.57-8.93-14.57,11-7.26,5.5s-4.11,2.67-4,6.32c.15,4.65,5.34,6.69,5.34,6.69l30.63,13.13a35.93,35.93,0,0,0,11.38,2.53c3.39.08,6.83-1.14,10.49-2.24l24.76-8.46c1.26-.41,3.26-1.65,3.26-3.17,0-1.2-1.57-2.73-3.33-3.51a16.28,16.28,0,0,0-8.57-1.19c-4.7.44-25.95,7.65-25.95,7.65L93.16,184.23a1.28,1.28,0,0,1-.91-1.58A2.68,2.68,0,0,1,93.1,181.53Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="url(#c)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M147.75,179.27c4.25-1.29,7.2-4.59,7.46-7.92a5.92,5.92,0,0,0-3-5.65l-2.48-1.53L132,169.56l-27.13.89h0a3.11,3.11,0,0,1-1.35-.18,4.24,4.24,0,0,1-1.33-1.11l-7.57-8.93-14.57,11,3.64,4.7a16.1,16.1,0,0,0,6.48,4.83,17.88,17.88,0,0,0,2.93.73h0a66.73,66.73,0,0,0,7.06.19C107.65,182,144.08,180.38,147.75,179.27Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="#e4097d"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M104.87,170.45a3.26,3.26,0,0,1-1.35-.18,4.24,4.24,0,0,1-1.33-1.11l-7.67-9-3-3.55a1.94,1.94,0,0,1-.41-1.27,2.18,2.18,0,0,1,1-1.83L110,141.9a6.43,6.43,0,0,1,1.81-.87,5.82,5.82,0,0,1,1.86.09l16.49,2.64a24.38,24.38,0,0,0,9.39-.52c1.94-.49,4.31-1.25,5.39-2.91s-.6-2.9-1.11-3.27c-2.08-1.5-4.67-1.92-7.18-2.32l-25.48-4.08a23.84,23.84,0,0,0-10.88.57A19.61,19.61,0,0,0,95,133.6L71.44,149.36a6.34,6.34,0,0,0-1.08,9.38l.21.27,13.12,17a16.1,16.1,0,0,0,6.48,4.83,17.88,17.88,0,0,0,2.93.73Z"
|
||||||
|
transform="translate(-68.76 -130.29)"
|
||||||
|
fill="url(#d)"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="main-content">
|
||||||
|
<div class="error-message">
|
||||||
|
<div class="error-icon">✖</div>
|
||||||
|
<h1>Ein Fehler ist aufgetreten</h1>
|
||||||
|
<p class="error-text">
|
||||||
|
Bei der Verlängerung Ihrer Pädagogischen Jahreskarte ist leider ein Fehler aufgetreten.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="contact-info">
|
||||||
|
<p>Bitte setzen Sie sich mit uns in Verbindung, damit wir Ihnen weiterhelfen können.</p>
|
||||||
|
|
||||||
|
<div class="contact-item">
|
||||||
|
<strong>Telefon</strong>
|
||||||
|
<span class="phone-number">+49 (0) 7131 88795 – 0</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="contact-item">
|
||||||
|
<strong>E-Mail-Adresse</strong>
|
||||||
|
<a href="mailto:buchung@experimenta.science">buchung@experimenta.science</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="contact-item">
|
||||||
|
<strong>Öffnungszeiten Besucherservice</strong>
|
||||||
|
<span class="phone-number">Montag bis Freitag von 8 bis 17 Uhr</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="https://www.experimenta.science/" class="back-button"
|
||||||
|
>Zur experimenta Startseite</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="footer-content">
|
||||||
|
<div class="footer-bottom">
|
||||||
|
<p>© 2025 experimenta gGmbH – Das Science Center. Alle Rechte vorbehalten.</p>
|
||||||
|
<div class="footer-bottom-links">
|
||||||
|
<a href="https://www.experimenta.science/kontakt/">Kontakt</a>
|
||||||
|
<a href="https://www.experimenta.science/impressum/">Impressum</a>
|
||||||
|
<a href="https://www.experimenta.science/datenschutz/">Datenschutz</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
docs/styleguides/JM experimenta Styleguide CSS 210413@2x.png
Executable file
|
After Width: | Height: | Size: 9.3 MiB |
21
eslint.config.mjs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// @ts-check
|
||||||
|
import withNuxt from './.nuxt/eslint.config.mjs'
|
||||||
|
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
|
||||||
|
|
||||||
|
export default withNuxt(eslintPluginPrettierRecommended, {
|
||||||
|
rules: {
|
||||||
|
// Enforce consistent code style
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// Vue 3 Composition API best practices
|
||||||
|
'vue/component-api-style': ['error', ['script-setup']],
|
||||||
|
'vue/component-name-in-template-casing': ['error', 'PascalCase'],
|
||||||
|
'vue/require-default-prop': 'off',
|
||||||
|
'vue/multi-word-component-names': 'off',
|
||||||
|
},
|
||||||
|
})
|
||||||
6
locales/de-DE.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"welcome": "Willkommen bei experimenta",
|
||||||
|
"app": {
|
||||||
|
"title": "my.experimenta.science"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
locales/en-US.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"welcome": "Welcome to experimenta",
|
||||||
|
"app": {
|
||||||
|
"title": "my.experimenta.science"
|
||||||
|
}
|
||||||
|
}
|
||||||
55
nuxt.config.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
compatibilityDate: '2025-01-29',
|
||||||
|
|
||||||
|
devtools: { enabled: true },
|
||||||
|
|
||||||
|
// App configuration
|
||||||
|
app: {
|
||||||
|
head: {
|
||||||
|
link: [
|
||||||
|
{
|
||||||
|
rel: 'preconnect',
|
||||||
|
href: 'https://fonts.googleapis.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rel: 'preconnect',
|
||||||
|
href: 'https://fonts.gstatic.com',
|
||||||
|
crossorigin: 'anonymous',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rel: 'stylesheet',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Modules
|
||||||
|
modules: ['@nuxtjs/tailwindcss', 'shadcn-nuxt', '@nuxt/eslint'],
|
||||||
|
|
||||||
|
// shadcn-nuxt configuration
|
||||||
|
shadcn: {
|
||||||
|
prefix: '',
|
||||||
|
componentDir: './components/ui',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Runtime configuration
|
||||||
|
runtimeConfig: {
|
||||||
|
// Server-only config
|
||||||
|
databaseUrl: process.env.DATABASE_URL,
|
||||||
|
redisHost: process.env.REDIS_HOST || 'localhost',
|
||||||
|
redisPort: process.env.REDIS_PORT || '6379',
|
||||||
|
|
||||||
|
// Public (exposed to client)
|
||||||
|
public: {
|
||||||
|
appUrl: process.env.APP_URL || 'http://localhost:3000',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// TypeScript configuration
|
||||||
|
typescript: {
|
||||||
|
strict: true,
|
||||||
|
typeCheck: false, // Disabled for now, will enable in later phases with vue-tsc
|
||||||
|
},
|
||||||
|
})
|
||||||
35
package.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "my.experimenta.science",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "nuxt build",
|
||||||
|
"dev": "nuxt dev",
|
||||||
|
"generate": "nuxt generate",
|
||||||
|
"preview": "nuxt preview",
|
||||||
|
"postinstall": "nuxt prepare",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"typecheck": "nuxt typecheck"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"nuxt": "^4.2.0",
|
||||||
|
"reka-ui": "^2.6.0",
|
||||||
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"vue": "^3.5.22",
|
||||||
|
"vue-router": "^4.6.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nuxt/eslint": "^1.10.0",
|
||||||
|
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||||
|
"@types/node": "^22.10.0",
|
||||||
|
"eslint": "^9.38.0",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"eslint-plugin-prettier": "^5.5.4",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
|
"shadcn-nuxt": "^2.3.2",
|
||||||
|
"typescript": "^5.7.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
9637
pnpm-lock.yaml
generated
Normal file
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
58
public/img/experimenta-logo-white.svg
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<svg viewBox="0 0 382.94 87.17" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="footer-a" x1="102.63" y1="152.32" x2="135.19" y2="191.11" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.16" stop-color="#bf144c" />
|
||||||
|
<stop offset="0.29" stop-color="#ce0f60" />
|
||||||
|
<stop offset="0.47" stop-color="#de0b75" />
|
||||||
|
<stop offset="0.59" stop-color="#e4097d" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="footer-b" x1="104.87" y1="170.45" x2="104.87" y2="170.45" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.16" stop-color="#bf144c" />
|
||||||
|
<stop offset="0.29" stop-color="#ab1a4e" />
|
||||||
|
<stop offset="0.43" stop-color="#9f1d4f" />
|
||||||
|
<stop offset="0.57" stop-color="#9b1e4f" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="footer-c" x1="68.79" y1="182.84" x2="154.66" y2="182.84" xlink:href="#footer-b" />
|
||||||
|
<linearGradient id="footer-d" x1="94.04" y1="182.21" x2="114.5" y2="126" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.22" stop-color="#e4097d" />
|
||||||
|
<stop offset="0.32" stop-color="#e4115e" />
|
||||||
|
<stop offset="0.45" stop-color="#e5193d" />
|
||||||
|
<stop offset="0.55" stop-color="#e51e28" />
|
||||||
|
<stop offset="0.62" stop-color="#e52021" />
|
||||||
|
<stop offset="0.9" stop-color="#f7a822" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<polygon points="143.78 50 151.18 39.6 144.43 39.6 139.68 46.33 135.13 39.6 127.79 39.6 135.32 50.08 127.29 61.23 134.09 61.23 139.3 53.78 144.43 61.23 151.59 61.23 143.78 50" fill="#fff" />
|
||||||
|
<path d="M245.79,175.33a9.2,9.2,0,0,0-1.85-3.39,7.28,7.28,0,0,0-2.9-2,10.29,10.29,0,0,0-3.65-.63c-3.12,0-5.47.95-7,2.82l-.56-2.23h-7.72v5.29h3.13v24.7h6.13v-8.4a8.29,8.29,0,0,0,1.7.42,17.3,17.3,0,0,0,2.6.19,11.8,11.8,0,0,0,4.53-.82,9.29,9.29,0,0,0,3.39-2.39,10.78,10.78,0,0,0,2.11-3.78,15.69,15.69,0,0,0,.74-5A16,16,0,0,0,245.79,175.33Zm-12.76,0a5.26,5.26,0,0,1,2.73-.75,4,4,0,0,1,3.12,1.4,5.85,5.85,0,0,1,1.25,4,10.56,10.56,0,0,1-.4,3.12,5.84,5.84,0,0,1-1.1,2.09,4.11,4.11,0,0,1-1.62,1.18,7.15,7.15,0,0,1-4.21.12,4.71,4.71,0,0,1-1.43-.58v-8.48A3.79,3.79,0,0,1,233,175.35Z" transform="translate(-68.76 -130.29)" fill="#fff" />
|
||||||
|
<path d="M270.8,174.1a8.1,8.1,0,0,0-2.42-2.86,9.3,9.3,0,0,0-3.24-1.5,14.1,14.1,0,0,0-7.91.28,10.48,10.48,0,0,0-3.7,2.14,10,10,0,0,0-2.49,3.58,12.71,12.71,0,0,0-.9,5,13.24,13.24,0,0,0,.79,4.74,9.75,9.75,0,0,0,6,5.88,14.71,14.71,0,0,0,4.91.77,15.93,15.93,0,0,0,3-.26,14.61,14.61,0,0,0,2.59-.68,13.18,13.18,0,0,0,2.05-.92,8,8,0,0,0,1.47-1l.19-.18L269,184.85l-.33.28a8.34,8.34,0,0,1-2.51,1.36,11.84,11.84,0,0,1-4,.57,9.83,9.83,0,0,1-2.28-.26,6,6,0,0,1-1.85-.81,4.16,4.16,0,0,1-1.28-1.39,4.33,4.33,0,0,1-.5-1.75h15l.05-.28a19.77,19.77,0,0,0,.43-4A9.75,9.75,0,0,0,270.8,174.1Zm-11.06.54a6.52,6.52,0,0,1,1.95-.29,4.29,4.29,0,0,1,3.24,1.11,4.13,4.13,0,0,1,1.05,2.72h-9.55a4.05,4.05,0,0,1,.5-1.39,4.45,4.45,0,0,1,1.18-1.33A5.21,5.21,0,0,1,259.74,174.64Z" transform="translate(-68.76 -130.29)" fill="#fff" />
|
||||||
|
<path d="M194.59,174.1a8,8,0,0,0-2.43-2.86,9.26,9.26,0,0,0-3.23-1.5A14.09,14.09,0,0,0,181,170a10.48,10.48,0,0,0-3.7,2.14,10,10,0,0,0-2.49,3.58,12.7,12.7,0,0,0-.91,5,13.23,13.23,0,0,0,.8,4.74,9.75,9.75,0,0,0,6,5.88,14.71,14.71,0,0,0,4.91.77,15.82,15.82,0,0,0,3-.26,14.38,14.38,0,0,0,2.59-.68,12.91,12.91,0,0,0,2.06-.92,8,8,0,0,0,1.47-1l.19-.18-2.11-4.18-.34.28a8.22,8.22,0,0,1-2.51,1.36,11.78,11.78,0,0,1-4,.57,9.7,9.7,0,0,1-2.28-.26,6,6,0,0,1-1.86-.81,4.23,4.23,0,0,1-1.27-1.39,4.33,4.33,0,0,1-.5-1.75h15l.06-.28a20.47,20.47,0,0,0,.42-4A9.75,9.75,0,0,0,194.59,174.1Zm-11.06.54a6.52,6.52,0,0,1,1.95-.29,4.31,4.31,0,0,1,3.24,1.11,4.07,4.07,0,0,1,1,2.72h-9.54a4,4,0,0,1,.49-1.39,4.35,4.35,0,0,1,1.19-1.33A5.21,5.21,0,0,1,183.53,174.64Z" transform="translate(-68.76 -130.29)" fill="#fff" />
|
||||||
|
<path d="M298.09,178.12v-.34c0-3-.49-5.07-1.52-6.37a5.07,5.07,0,0,0-4.22-2,6.89,6.89,0,0,0-3.69,1,11.46,11.46,0,0,0-2.44,2l-.58-2.52h-10v5.29h5.18v11h-5.18v5.3H296v-5.3h-9.08v-8.47a8.94,8.94,0,0,1,1.61-1.76,3.84,3.84,0,0,1,2.48-.76,1.2,1.2,0,0,1,1.11.54,3.85,3.85,0,0,1,.39,2v.34Z" transform="translate(-68.76 -130.29)" fill="#fff" />
|
||||||
|
<path d="M350.79,173.27a5.61,5.61,0,0,0-1.16-2.29,4.05,4.05,0,0,0-1.79-1.21,7.17,7.17,0,0,0-2.24-.33,6.19,6.19,0,0,0-3.12.82,5,5,0,0,0-1.79,1.7,3.66,3.66,0,0,0-1.7-1.81,6.39,6.39,0,0,0-2.92-.71,6,6,0,0,0-3.17.82,5.75,5.75,0,0,0-1.71,1.57l-.53-1.93h-4.78v21.62h6V176.07a1.89,1.89,0,0,1,.72-.94,2,2,0,0,1,1.92-.27,1.05,1.05,0,0,1,.53.44,3.5,3.5,0,0,1,.41,1.11,9.26,9.26,0,0,1,.17,2v13.16h6V175.94a1.5,1.5,0,0,1,.63-.9,2.12,2.12,0,0,1,1.18-.31,1.62,1.62,0,0,1,1.29.63,3.88,3.88,0,0,1,.56,2.42v13.74h6V176.86A14.23,14.23,0,0,0,350.79,173.27Z" transform="translate(-68.76 -130.29)" fill="#fff" />
|
||||||
|
<path d="M375.88,174.1a8,8,0,0,0-2.43-2.86,9.3,9.3,0,0,0-3.24-1.5,14.1,14.1,0,0,0-7.91.28,10.44,10.44,0,0,0-3.69,2.14,10.11,10.11,0,0,0-2.5,3.58,12.71,12.71,0,0,0-.9,5,13.46,13.46,0,0,0,.79,4.74,9.89,9.89,0,0,0,2.32,3.6,10,10,0,0,0,3.71,2.28,14.65,14.65,0,0,0,4.9.77,15.93,15.93,0,0,0,3-.26,14.61,14.61,0,0,0,2.59-.68,13.77,13.77,0,0,0,2.06-.92,8.18,8.18,0,0,0,1.47-1l.19-.18-2.12-4.18-.33.28a8.34,8.34,0,0,1-2.51,1.36,11.82,11.82,0,0,1-4,.57,9.7,9.7,0,0,1-2.28-.26,5.93,5.93,0,0,1-1.86-.81,4.23,4.23,0,0,1-1.27-1.39,4.18,4.18,0,0,1-.5-1.75h15l.06-.28a20.52,20.52,0,0,0,.43-4A9.75,9.75,0,0,0,375.88,174.1Zm-11.06.54a6.5,6.5,0,0,1,1.94-.29,4.33,4.33,0,0,1,3.25,1.11,4.18,4.18,0,0,1,1.05,2.72h-9.55a3.84,3.84,0,0,1,.49-1.39,4.35,4.35,0,0,1,1.19-1.33A5.21,5.21,0,0,1,364.82,174.64Z" transform="translate(-68.76 -130.29)" fill="#fff" />
|
||||||
|
<path d="M401.33,174a7,7,0,0,0-1.61-2.75,5.79,5.79,0,0,0-2.46-1.46,10.19,10.19,0,0,0-3-.44,9,9,0,0,0-7,3.07l-.57-2.48h-7.94v5.29h3v16.33h6.13V177.67a4.13,4.13,0,0,1,1.59-2,4.48,4.48,0,0,1,2.62-.83,3.6,3.6,0,0,1,2.67,1,4.72,4.72,0,0,1,1,3.43v12.24h6.14V178.15A13.05,13.05,0,0,0,401.33,174Z" transform="translate(-68.76 -130.29)" fill="#fff" />
|
||||||
|
<path d="M425.62,183.62l-1.41,1.17a9.47,9.47,0,0,1-1.29.89,7,7,0,0,1-3.53.92,3.83,3.83,0,0,1-3.12-1.32,7,7,0,0,1-1.14-4.45v-5.64h11V169.9h-11v-6.43L409,165.21v4.69h-4.14v5.29H409v5.64c0,3.87.83,6.74,2.46,8.54s4.09,2.73,7.31,2.73a12.69,12.69,0,0,0,2.7-.3,14.51,14.51,0,0,0,2.64-.84,15.43,15.43,0,0,0,2.35-1.24,8.09,8.09,0,0,0,1.85-1.59l.17-.2Z" transform="translate(-68.76 -130.29)" fill="#fff" />
|
||||||
|
<path d="M449,186.22c0-.27-.06-.54-.1-.81-.05-.71-.08-1.36-.08-1.94,0-.78.05-1.66.14-2.65s.16-2.25.16-3.67a10.5,10.5,0,0,0-.44-3.12,6,6,0,0,0-1.45-2.44,6.56,6.56,0,0,0-2.6-1.57,11.86,11.86,0,0,0-3.81-.54,22.68,22.68,0,0,0-5.29.55,23.45,23.45,0,0,0-3.93,1.32l-.28.12,1.49,4.95.36-.17a22.8,22.8,0,0,1,2.83-1.07,12,12,0,0,1,3.71-.53,4.18,4.18,0,0,1,2.5.62,2.33,2.33,0,0,1,.77,2v.8a2.53,2.53,0,0,1,0,.47l-1.71-.15c-.56,0-1.10-.08-1.61-.08a17.54,17.54,0,0,0-3.92.41,9.29,9.29,0,0,0-3.07,1.26,5.82,5.82,0,0,0-2,2.22,7.07,7.07,0,0,0-.68,3.19,6.12,6.12,0,0,0,1.94,4.7,7.43,7.43,0,0,0,5.19,1.76,8.26,8.26,0,0,0,4.33-1,7.94,7.94,0,0,0,2.17-1.9l.49,2.56h7.66v-5.3Zm-12.43-2.67a2.4,2.4,0,0,1,.88-.71,4.48,4.48,0,0,1,1.31-.43,7.73,7.73,0,0,1,1.51-.15,13.48,13.48,0,0,1,1.73.11c.38,0,.69.09.94.13v2.13a4.16,4.16,0,0,1-4.08,2.05,2.92,2.92,0,0,1-2-.55,2.07,2.07,0,0,1-.57-1.57A1.68,1.68,0,0,1,436.52,183.55Z" transform="translate(-68.76 -130.29)" fill="#fff" />
|
||||||
|
<polygon points="249.76 48.18 241.03 39.45 230.96 49.53 230.96 52.96 233.6 50.29 240.61 57.31 238.01 59.91 239.67 61.58 253.68 47.56 253.68 44.27 249.76 48.18" fill="#fff" />
|
||||||
|
<path d="M314.12,162.94a5.56,5.56,0,1,1-5.55-5.55A5.55,5.55,0,0,1,314.12,162.94Z" transform="translate(-68.76 -130.29)" fill="#fff" />
|
||||||
|
<path d="M275.61,204.76l.56,0,.9-.06,1.06-.05,1,0a6.75,6.75,0,0,1,2.74.49,4.2,4.2,0,0,1,1.72,1.32,5.15,5.15,0,0,1,.88,2,11.58,11.58,0,0,1,.25,2.5,11.24,11.24,0,0,1-.25,2.4,5.62,5.62,0,0,1-.9,2.09,4.62,4.62,0,0,1-1.79,1.48,6.81,6.81,0,0,1-2.95.56l-.73,0-.91,0-.89-.06a4.37,4.37,0,0,1-.65-.06Zm3.57,2c-.22,0-.44,0-.66,0l-.48.05v8.34l.22,0h.29l.29,0h.24a3.13,3.13,0,0,0,1.64-.38,2.41,2.41,0,0,0,.92-1,4.31,4.31,0,0,0,.39-1.4,13.77,13.77,0,0,0,.09-1.57,13.6,13.6,0,0,0-.08-1.4,4.16,4.16,0,0,0-.38-1.34,2.46,2.46,0,0,0-.88-1A2.85,2.85,0,0,0,279.18,206.76Z" transform="translate(-68.76 -130.29)" fill="#fff" />
|
||||||
|
<path d="M286.81,208.92a11,11,0,0,1,1.65-.55,9.5,9.5,0,0,1,2.21-.23,5.19,5.19,0,0,1,1.58.22,2.63,2.63,0,0,1,1.05.64,2.38,2.38,0,0,1,.57,1,4.26,4.26,0,0,1,.18,1.28c0,.61,0,1.13-.06,1.55s-.06.81-.06,1.14,0,.53,0,.84a3.93,3.93,0,0,1,.06.48h1.15v2h-3L292,216h-.08a3.31,3.31,0,0,1-1,1,3.46,3.46,0,0,1-1.77.4,3,3,0,0,1-2.1-.71,2.45,2.45,0,0,1-.78-1.89,2.86,2.86,0,0,1,.27-1.29,2.28,2.28,0,0,1,.8-.89,4,4,0,0,1,1.25-.52,7.54,7.54,0,0,1,1.64-.16l.67,0c.24,0,.52,0,.87.07a2.07,2.07,0,0,0,0-.35v-.34a1.13,1.13,0,0,0-.39-1,1.86,1.86,0,0,0-1.15-.29,5.11,5.11,0,0,0-1.62.23,9.05,9.05,0,0,0-1.22.46Zm3,6.53a2.12,2.12,0,0,0,1.27-.31,2.1,2.1,0,0,0,.61-.67v-1.06a4.92,4.92,0,0,0-.52-.08,6.06,6.06,0,0,0-.76-.05,3.05,3.05,0,0,0-.67.07,2.22,2.22,0,0,0-.6.19,1.31,1.31,0,0,0-.42.35.86.86,0,0,0-.16.51,1,1,0,0,0,.29.78A1.39,1.39,0,0,0,289.84,215.45Z" transform="translate(-68.76 -130.29)" fill="#fff" />
|
||||||
|
<path d="M302.86,214.76a.64.64,0,0,0-.44-.57,6.8,6.8,0,0,0-1.07-.39q-.64-.18-1.41-.36a6.44,6.44,0,0,1-1.4-.52,3.49,3.49,0,0,1-1.08-.84,1.93,1.93,0,0,1-.44-1.3,2.2,2.2,0,0,1,.3-1.16,2.59,2.59,0,0,1,.8-.85,3.82,3.82,0,0,1,1.21-.52,6.09,6.09,0,0,1,1.52-.18,7.73,7.73,0,0,1,1.53.13,7.07,7.07,0,0,1,1.15.3,4.28,4.28,0,0,1,.83.4l.6.4-1,1.58-.61-.33-.73-.31a6.89,6.89,0,0,0-.81-.23,3.81,3.81,0,0,0-.82-.09,3.15,3.15,0,0,0-1.21.2.63.63,0,0,0-.46.60c0,.21.14.39.43.52a6.8,6.8,0,0,0,1.08.36l1.4.37a8.06,8.06,0,0,1,1.41.5,3.52,3.52,0,0,1,1.08.81,1.89,1.89,0,0,1,.43,1.28,2.57,2.57,0,0,1-1,2.13,4.69,4.69,0,0,1-2.92.77,7,7,0,0,1-2.64-.45,6,6,0,0,1-1.79-1.06l1.07-1.67a4.73,4.73,0,0,0,.61.43,5.31,5.31,0,0,0,.86.44,7.61,7.61,0,0,0,1,.33,4.55,4.55,0,0,0,1.08.13,2.26,2.26,0,0,0,1-.19A.68.68,0,0,0,302.86,214.76Z" transform="translate(-68.76 -130.29)" fill="#fff" />
|
||||||
|
<path d="M311.81,213.14h1.95v1.76l.11,0a5.28,5.28,0,0,0,.9.29,4.3,4.3,0,0,0,1,.12,2.61,2.61,0,0,0,1.54-.4,1.27,1.27,0,0,0,.55-1.08,1.22,1.22,0,0,0-.45-1,4.42,4.42,0,0,0-1.12-.66c-.44-.19-.93-.37-1.46-.57a7.09,7.09,0,0,1-1.46-.72,4.15,4.15,0,0,1-1.12-1.09,3,3,0,0,1-.45-1.71,3.35,3.35,0,0,1,.31-1.46,3.4,3.4,0,0,1,.87-1.15,4.1,4.1,0,0,1,1.35-.75,5.29,5.29,0,0,1,1.74-.27,11.64,11.64,0,0,1,2.13.2,6.05,6.05,0,0,1,1.68.53v3.62h-2v-2l-.11,0c-.26-.07-.54-.12-.84-.17a6.8,6.8,0,0,0-.9-.06,2.27,2.27,0,0,0-1.34.34,1,1,0,0,0-.49.91,1.19,1.19,0,0,0,.45,1,5.35,5.35,0,0,0,1.12.67c.45.2.93.4,1.46.61a7.15,7.15,0,0,1,1.46.74,4,4,0,0,1,1.12,1.09,2.86,2.86,0,0,1,.45,1.65,3.92,3.92,0,0,1-.35,1.69,3.41,3.41,0,0,1-1,1.2,4.33,4.33,0,0,1-1.51.73,7.68,7.68,0,0,1-3.14.14,7.59,7.59,0,0,1-1.07-.27,8.12,8.12,0,0,1-.86-.34l-.6-.3Z" transform="translate(-68.76 -130.29)" fill="#fff" />
|
||||||
|
<path d="M330.86,216.36a5.24,5.24,0,0,1-1.71.83,7.74,7.74,0,0,1-2,.27,5.92,5.92,0,0,1-2.05-.33,4.16,4.16,0,0,1-1.51-1,4.12,4.12,0,0,1-.94-1.48,5.6,5.6,0,0,1-.32-1.92,5,5,0,0,1,.38-2,3.93,3.93,0,0,1,1-1.48,4.55,4.55,0,0,1,1.57-.92,6.21,6.21,0,0,1,2-.31,7.43,7.43,0,0,1,1.87.23,9.76,9.76,0,0,1,1.34.45v3.13h-2v-1.64a5.22,5.22,0,0,0-1.1-.12,3.35,3.35,0,0,0-1,.15,2.32,2.32,0,0,0-.87.48,2.6,2.6,0,0,0-.63.82,3.12,3.12,0,0,0,0,2.25,2.51,2.51,0,0,0,.55.83,2.42,2.42,0,0,0,.88.56,3.13,3.13,0,0,0,1.17.21,4.15,4.15,0,0,0,1.66-.29,7.06,7.06,0,0,0,1-.49Z" transform="translate(-68.76 -130.29)" fill="#fff" />
|
||||||
|
<path d="M332.69,215.26h3.13v-5h-3.13v-2h5.44v6.94h3.29v2h-8.73Zm2.7-9.36a1.26,1.26,0,0,1,.41-.94,1.58,1.58,0,0,1,1.14-.39,1.74,1.74,0,0,1,1.19.39,1.19,1.19,0,0,1,.44.94,1.16,1.16,0,0,1-.44.94,1.84,1.84,0,0,1-1.19.36,1.66,1.66,0,0,1-1.14-.36A1.23,1.23,0,0,1,335.39,205.9Z" transform="translate(-68.76 -130.29)" fill="#fff" />
|
||||||
|
<path d="M351.63,216.27a3.11,3.11,0,0,1-.6.42,5.25,5.25,0,0,1-.85.38,6.28,6.28,0,0,1-1.08.29,7,7,0,0,1-1.24.1,5.9,5.9,0,0,1-2-.32,4,4,0,0,1-1.52-.93,4.14,4.14,0,0,1-.95-1.48,6,6,0,0,1,0-4,4.07,4.07,0,0,1,1-1.46,4.36,4.36,0,0,1,1.52-.89,6,6,0,0,1,1.88-.29,5.78,5.78,0,0,1,1.4.18,3.92,3.92,0,0,1,1.33.61,3.23,3.23,0,0,1,1,1.17,3.87,3.87,0,0,1,.39,1.85,8.31,8.31,0,0,1-.18,1.65h-6.41a2,2,0,0,0,.24,1,1.91,1.91,0,0,0,.59.64,2.56,2.56,0,0,0,.84.37,4.57,4.57,0,0,0,1,.11,5.35,5.35,0,0,0,1.75-.24,3.81,3.81,0,0,0,1.12-.61Zm-3.84-6.35a2.73,2.73,0,0,0-.87.13,2.34,2.34,0,0,0-.74.37,2,2,0,0,0-.54.61,1.71,1.71,0,0,0-.25.81h4.37a1.76,1.76,0,0,0-2-1.92Z" transform="translate(-68.76 -130.29)" fill="#fff" />
|
||||||
|
<path d="M352.84,208.32H356l.27,1.17h.07a3.42,3.42,0,0,1,1.17-1,3.68,3.68,0,0,1,1.84-.43,4.11,4.11,0,0,1,1.23.18,2.26,2.26,0,0,1,1,.59,2.84,2.84,0,0,1,.65,1.11,5.39,5.39,0,0,1,.24,1.73v5.53H360.1v-5.05a2.14,2.14,0,0,0-.49-1.56,1.7,1.7,0,0,0-1.24-.48,2,2,0,0,0-1.2.38,1.9,1.9,0,0,0-.74.95v5.76h-2.31v-6.93h-1.28Z" transform="translate(-68.76 -130.29)" fill="#fff" />
|
||||||
|
<path d="M372.83,216.36a5.09,5.09,0,0,1-1.7.83,7.74,7.74,0,0,1-2.05.27,5.88,5.88,0,0,1-2.05-.33,4.29,4.29,0,0,1-1.52-1,4,4,0,0,1-.93-1.48,5.6,5.6,0,0,1-.32-1.92,5,5,0,0,1,.37-2,4.17,4.17,0,0,1,1-1.48,4.6,4.6,0,0,1,1.58-.92,6.21,6.21,0,0,1,2-.31,7.43,7.43,0,0,1,1.87.23,9.1,9.1,0,0,1,1.33.45v3.13h-1.95v-1.64a5.24,5.24,0,0,0-1.11-.12,3.28,3.28,0,0,0-1,.15,2.32,2.32,0,0,0-.87.48,2.46,2.46,0,0,0-.63.82,3,3,0,0,0,0,2.25,2.36,2.36,0,0,0,.55.83,2.42,2.42,0,0,0,.88.56,3.13,3.13,0,0,0,1.17.21,4.19,4.19,0,0,0,1.66-.29,7.71,7.71,0,0,0,1-.49Z" transform="translate(-68.76 -130.29)" fill="#fff" />
|
||||||
|
<path d="M383.11,216.27a3.34,3.34,0,0,1-.59.42,5.81,5.81,0,0,1-.86.38,6.16,6.16,0,0,1-1.07.29,7.11,7.11,0,0,1-1.25.1,5.85,5.85,0,0,1-2-.32,4,4,0,0,1-1.52-.93,4,4,0,0,1-1-1.48,5.67,5.67,0,0,1-.33-2,5.26,5.26,0,0,1,.38-2.06,4.17,4.17,0,0,1,1-1.46,4.36,4.36,0,0,1,1.52-.89,6,6,0,0,1,1.89-.29,5.7,5.7,0,0,1,1.39.18,3.75,3.75,0,0,1,1.33.61,3.36,3.36,0,0,1,1,1.17,4,4,0,0,1,.38,1.85,8.31,8.31,0,0,1-.18,1.65h-6.4a2,2,0,0,0,.24,1,1.79,1.79,0,0,0,.58.64,2.66,2.66,0,0,0,.84.37,4.6,4.6,0,0,0,1,.11,5.39,5.39,0,0,0,1.75-.24,3.87,3.87,0,0,0,1.11-.61Zm-3.84-6.35a2.73,2.73,0,0,0-.87.13,2.45,2.45,0,0,0-.74.37,2,2,0,0,0-.54.61,1.83,1.83,0,0,0-.25.81h4.38a2,2,0,0,0-.49-1.4A1.94,1.94,0,0,0,379.27,209.92Z" transform="translate(-68.76 -130.29)" fill="#fff" />
|
||||||
|
<path d="M397.17,213h1.95v3.56a4.32,4.32,0,0,1-1.45.64,7.37,7.37,0,0,1-4-.11,5,5,0,0,1-1.85-1.12,5.51,5.51,0,0,1-1.28-2,8.35,8.35,0,0,1-.48-3,7.5,7.5,0,0,1,.54-3.07,5.58,5.58,0,0,1,1.39-2,5.16,5.16,0,0,1,1.91-1.09,6.82,6.82,0,0,1,2.06-.33,7.84,7.84,0,0,1,1.81.18,6.52,6.52,0,0,1,1.21.41v3.7H397v-2a7.11,7.11,0,0,0-1.14-.09,3.27,3.27,0,0,0-1.29.26,2.83,2.83,0,0,0-1,.79,3.77,3.77,0,0,0-.69,1.34,6.48,6.48,0,0,0-.25,1.92,6,6,0,0,0,.23,1.75,4,4,0,0,0,.67,1.36,3,3,0,0,0,1.09.88,3.26,3.26,0,0,0,1.46.31,6.12,6.12,0,0,0,1.14-.1Z" transform="translate(-68.76 -130.29)" fill="#fff" />
|
||||||
|
<path d="M409.26,216.27a3.11,3.11,0,0,1-.6.42,5.25,5.25,0,0,1-.85.38,6.41,6.41,0,0,1-1.07.29,7.19,7.19,0,0,1-1.25.1,5.9,5.9,0,0,1-2-.32,4,4,0,0,1-1.52-.93,4.14,4.14,0,0,1-.95-1.48,5.44,5.44,0,0,1-.33-2,5.26,5.26,0,0,1,.38-2.06,4.17,4.17,0,0,1,1-1.46,4.36,4.36,0,0,1,1.52-.89,6,6,0,0,1,1.88-.29,5.78,5.78,0,0,1,1.4.18,3.92,3.92,0,0,1,1.33.61,3.23,3.23,0,0,1,1,1.17,3.87,3.87,0,0,1,.39,1.85,9.12,9.12,0,0,1-.18,1.65H403a2,2,0,0,0,.24,1,1.79,1.79,0,0,0,.58.64,2.47,2.47,0,0,0,.84.37,4.57,4.57,0,0,0,1,.11,5.35,5.35,0,0,0,1.75-.24,3.81,3.81,0,0,0,1.12-.61Zm-3.84-6.35a2.73,2.73,0,0,0-.87.13,2.34,2.34,0,0,0-.74.37,2,2,0,0,0-.54.61,1.83,1.83,0,0,0-.25.81h4.37a1.75,1.75,0,0,0-2-1.92Z" transform="translate(-68.76 -130.29)" fill="#fff" />
|
||||||
|
<path d="M410.47,208.32h3.11l.27,1.17h.07a3.46,3.46,0,0,1,1.18-1,3.64,3.64,0,0,1,1.83-.43,4.06,4.06,0,0,1,1.23.18,2.26,2.26,0,0,1,1,.59,2.84,2.84,0,0,1,.65,1.11,5.39,5.39,0,0,1,.24,1.73v5.53h-2.31v-5.05a2.1,2.1,0,0,0-.49-1.56,1.7,1.7,0,0,0-1.24-.48,2,2,0,0,0-1.2.38,1.9,1.9,0,0,0-.74.95v5.76h-2.31v-6.93h-1.28Z" transform="translate(-68.76 -130.29)" fill="#fff" />
|
||||||
|
<path d="M422.81,210.28h-1.75v-2h1.75v-2l2.32-.66v2.69h4.66v2h-4.66v2.54a3.07,3.07,0,0,0,.51,2,1.8,1.8,0,0,0,1.44.62,3,3,0,0,0,.87-.12,2.85,2.85,0,0,0,.71-.29,4.07,4.07,0,0,0,.57-.39l.47-.39,1.07,1.6a4.11,4.11,0,0,1-.76.65,7,7,0,0,1-1,.51,5.78,5.78,0,0,1-1.09.35,5.4,5.4,0,0,1-1.12.12,3.84,3.84,0,0,1-3-1.11,5.17,5.17,0,0,1-1-3.53Z" transform="translate(-68.76 -130.29)" fill="#fff" />
|
||||||
|
<path d="M440.74,216.27a3.34,3.34,0,0,1-.59.42,5.81,5.81,0,0,1-.86.38,6.16,6.16,0,0,1-1.07.29,7.11,7.11,0,0,1-1.25.1,5.85,5.85,0,0,1-2-.32,4,4,0,0,1-1.52-.93,4,4,0,0,1-1-1.48,5.67,5.67,0,0,1-.32-2,5.26,5.26,0,0,1,.37-2.06,4.17,4.17,0,0,1,1-1.46,4.36,4.36,0,0,1,1.52-.89,6,6,0,0,1,1.89-.29,5.7,5.7,0,0,1,1.39.18,3.83,3.83,0,0,1,1.33.61,3.36,3.36,0,0,1,1,1.17,4,4,0,0,1,.38,1.85,8.31,8.31,0,0,1-.18,1.65h-6.4a2,2,0,0,0,.24,1,1.81,1.81,0,0,0,.59.64,2.51,2.51,0,0,0,.83.37,4.67,4.67,0,0,0,1,.11,5.39,5.39,0,0,0,1.75-.24,3.87,3.87,0,0,0,1.11-.61Zm-3.84-6.35a2.73,2.73,0,0,0-.87.13,2.45,2.45,0,0,0-.74.37,2,2,0,0,0-.79,1.42h4.38a2,2,0,0,0-.49-1.4A1.94,1.94,0,0,0,436.9,209.92Z" transform="translate(-68.76 -130.29)" fill="#fff" />
|
||||||
|
<path d="M442.43,215.26h2.21v-5h-2.21v-2h4l.26,1.17h.07a5,5,0,0,1,1.14-1,2.84,2.84,0,0,1,1.5-.39,2,2,0,0,1,1.68.78,4.28,4.28,0,0,1,.61,2.61h-2.08a1.72,1.72,0,0,0-.19-.93.64.64,0,0,0-.59-.3,1.74,1.74,0,0,0-1.15.36,3.6,3.6,0,0,0-.74.82v3.79h3.86v2h-8.38Z" transform="translate(-68.76 -130.29)" fill="#fff" />
|
||||||
|
<path d="M93.1,181.53l20.42-19.22c.87-.76,1.61-1.66,2.61-1.8,1.19-.17,3,1.09,3.3,1.28s10.7,6.64,12.57,7.77l15.75,9.71c4.25-1.29,7.2-4.59,7.46-7.92a5.92,5.92,0,0,0-3-5.65l-2.48-1.53h0s-17.5-10.32-22.43-13.18a22.83,22.83,0,0,0-11-3c-4.71.09-8.62,2.32-12.24,5h0l-24,18.2,3.64,4.7a16.1,16.1,0,0,0,6.48,4.83A17.88,17.88,0,0,0,93.1,181.53Z" transform="translate(-68.76 -130.29)" fill="url(#footer-a)" />
|
||||||
|
<path d="M104.87,170.45h0" transform="translate(-68.76 -130.29)" fill="url(#footer-b)" />
|
||||||
|
<path d="M93.1,181.53h0l11.77-11.08a3.11,3.11,0,0,1-1.35-.18,4.24,4.24,0,0,1-1.33-1.11l-7.57-8.93-14.57,11-7.26,5.5s-4.11,2.67-4,6.32c.15,4.65,5.34,6.69,5.34,6.69l30.63,13.13a35.93,35.93,0,0,0,11.38,2.53c3.39.08,6.83-1.14,10.49-2.24l24.76-8.46c1.26-.41,3.26-1.65,3.26-3.17,0-1.2-1.57-2.73-3.33-3.51a16.28,16.28,0,0,0-8.57-1.19c-4.7.44-25.95,7.65-25.95,7.65L93.16,184.23a1.28,1.28,0,0,1-.91-1.58A2.68,2.68,0,0,1,93.1,181.53Z" transform="translate(-68.76 -130.29)" fill="url(#footer-c)" />
|
||||||
|
<path d="M147.75,179.27c4.25-1.29,7.2-4.59,7.46-7.92a5.92,5.92,0,0,0-3-5.65l-2.48-1.53L132,169.56l-27.13.89h0a3.11,3.11,0,0,1-1.35-.18,4.24,4.24,0,0,1-1.33-1.11l-7.57-8.93-14.57,11,3.64,4.7a16.1,16.1,0,0,0,6.48,4.83,17.88,17.88,0,0,0,2.93.73h0a66.73,66.73,0,0,0,7.06.19C107.65,182,144.08,180.38,147.75,179.27Z" transform="translate(-68.76 -130.29)" fill="#e4097d" />
|
||||||
|
<path d="M104.87,170.45a3.26,3.26,0,0,1-1.35-.18,4.24,4.24,0,0,1-1.33-1.11l-7.67-9-3-3.55a1.94,1.94,0,0,1-.41-1.27,2.18,2.18,0,0,1,1-1.83L110,141.9a6.43,6.43,0,0,1,1.81-.87,5.82,5.82,0,0,1,1.86.09l16.49,2.64a24.38,24.38,0,0,0,9.39-.52c1.94-.49,4.31-1.25,5.39-2.91s-.6-2.9-1.11-3.27c-2.08-1.5-4.67-1.92-7.18-2.32l-25.48-4.08a23.84,23.84,0,0,0-10.88.57A19.61,19.61,0,0,0,95,133.6L71.44,149.36a6.34,6.34,0,0,0-1.08,9.38l.21.27,13.12,17a16.1,16.1,0,0,0,6.48,4.83,17.88,17.88,0,0,0,2.93.73Z" transform="translate(-68.76 -130.29)" fill="url(#footer-d)" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 19 KiB |
2
public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
User-Agent: *
|
||||||
|
Disallow:
|
||||||
202
tailwind.config.ts
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
import type { Config } from 'tailwindcss'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
'./components/**/*.{js,vue,ts}',
|
||||||
|
'./layouts/**/*.vue',
|
||||||
|
'./pages/**/*.vue',
|
||||||
|
'./plugins/**/*.{js,ts}',
|
||||||
|
'./app.vue',
|
||||||
|
'./error.vue',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
// experimenta Color Palette
|
||||||
|
colors: {
|
||||||
|
// shadcn-ui color variables
|
||||||
|
border: 'hsl(var(--border))',
|
||||||
|
input: 'hsl(var(--input))',
|
||||||
|
ring: 'hsl(var(--ring))',
|
||||||
|
background: 'hsl(var(--background))',
|
||||||
|
foreground: 'hsl(var(--foreground))',
|
||||||
|
primary: {
|
||||||
|
DEFAULT: 'hsl(var(--primary))',
|
||||||
|
foreground: 'hsl(var(--primary-foreground))',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: 'hsl(var(--secondary))',
|
||||||
|
foreground: 'hsl(var(--secondary-foreground))',
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: 'hsl(var(--destructive))',
|
||||||
|
foreground: 'hsl(var(--destructive-foreground))',
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: 'hsl(var(--muted))',
|
||||||
|
foreground: 'hsl(var(--muted-foreground))',
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: 'hsl(var(--accent))',
|
||||||
|
foreground: 'hsl(var(--accent-foreground))',
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: 'hsl(var(--popover))',
|
||||||
|
foreground: 'hsl(var(--popover-foreground))',
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: 'hsl(var(--card))',
|
||||||
|
foreground: 'hsl(var(--card-foreground))',
|
||||||
|
},
|
||||||
|
|
||||||
|
// experimenta Brand Colors (kept separate for custom use)
|
||||||
|
'experimenta-primary': {
|
||||||
|
DEFAULT: '#e6007e',
|
||||||
|
hover: '#c2006a',
|
||||||
|
light: '#ff4081',
|
||||||
|
},
|
||||||
|
'experimenta-secondary': {
|
||||||
|
DEFAULT: '#e91e63',
|
||||||
|
dark: '#c2185b',
|
||||||
|
},
|
||||||
|
'experimenta-accent': {
|
||||||
|
DEFAULT: '#f59d24',
|
||||||
|
hover: '#ffb347',
|
||||||
|
},
|
||||||
|
red: '#E40521',
|
||||||
|
|
||||||
|
// Purple Variants (Background)
|
||||||
|
purple: {
|
||||||
|
dark: '#2e1065',
|
||||||
|
deeper: '#1a0a3a',
|
||||||
|
darkest: '#0f051d',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Semantic Colors
|
||||||
|
success: {
|
||||||
|
DEFAULT: '#46c74a',
|
||||||
|
dark: '#3ba83e',
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
DEFAULT: '#e53e3e',
|
||||||
|
dark: '#c53030',
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
DEFAULT: '#f59d24',
|
||||||
|
dark: '#dd8a1e',
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
DEFAULT: '#4299e1',
|
||||||
|
dark: '#3182ce',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Font Sizes (Mobile-First with responsive variants)
|
||||||
|
fontSize: {
|
||||||
|
// H1
|
||||||
|
'4xl': ['36px', { lineHeight: '1.2', letterSpacing: '-1px' }],
|
||||||
|
// H2
|
||||||
|
'3xl': ['30px', { lineHeight: '1.3', letterSpacing: '-0.5px' }],
|
||||||
|
// H3
|
||||||
|
'2xl': ['24px', { lineHeight: '1.4' }],
|
||||||
|
// H4
|
||||||
|
xl: ['20px', { lineHeight: '1.4' }],
|
||||||
|
// H5
|
||||||
|
lg: ['18px', { lineHeight: '1.5', letterSpacing: '0.5px' }],
|
||||||
|
// Body
|
||||||
|
base: ['16px', { lineHeight: '1.6' }],
|
||||||
|
// Small
|
||||||
|
sm: ['14px', { lineHeight: '1.6' }],
|
||||||
|
// Tiny
|
||||||
|
xs: ['12px', { lineHeight: '1.5' }],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Spacing System (8px Grid)
|
||||||
|
spacing: {
|
||||||
|
'1': '4px',
|
||||||
|
'2': '8px',
|
||||||
|
'3': '12px',
|
||||||
|
'4': '16px',
|
||||||
|
'5': '20px',
|
||||||
|
'6': '24px',
|
||||||
|
'8': '30px',
|
||||||
|
'10': '40px',
|
||||||
|
'15': '60px',
|
||||||
|
'20': '80px',
|
||||||
|
'25': '100px', // For status icons
|
||||||
|
},
|
||||||
|
|
||||||
|
// Border Radius
|
||||||
|
borderRadius: {
|
||||||
|
lg: 'var(--radius)',
|
||||||
|
md: 'calc(var(--radius) - 2px)',
|
||||||
|
sm: 'calc(var(--radius) - 4px)',
|
||||||
|
xl: '20px',
|
||||||
|
'2xl': '25px',
|
||||||
|
full: '100%',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Max Widths (Containers)
|
||||||
|
maxWidth: {
|
||||||
|
'container-main': '800px',
|
||||||
|
'container-wide': '1200px',
|
||||||
|
'container-full': '1760px',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Background Gradients
|
||||||
|
backgroundImage: {
|
||||||
|
'gradient-primary': 'linear-gradient(135deg, #2e1065 0%, #1a0a3a 50%, #0f051d 100%)',
|
||||||
|
'gradient-footer': 'linear-gradient(135deg, #1a0a3a 0%, #0f051d 100%)',
|
||||||
|
'gradient-glass':
|
||||||
|
'linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05))',
|
||||||
|
'gradient-button': 'linear-gradient(to left, #e6007e, #e6007e, #E40521, #e6007e)',
|
||||||
|
'gradient-success': 'linear-gradient(90deg, #46c74a 0%, #66d96a 50%, #46c74a 100%)',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Background Sizes
|
||||||
|
backgroundSize: {
|
||||||
|
'size-300': '300%',
|
||||||
|
'size-200': '200%',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Background Positions
|
||||||
|
backgroundPosition: {
|
||||||
|
left: 'left',
|
||||||
|
right: 'right',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Box Shadows
|
||||||
|
boxShadow: {
|
||||||
|
glass: '0 20px 40px rgba(0, 0, 0, 0.3)',
|
||||||
|
'inner-light': 'inset 0 2px 4px rgba(0, 0, 0, 0.2)',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Backdrop Blur
|
||||||
|
backdropBlur: {
|
||||||
|
xl: '15px',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Animations
|
||||||
|
keyframes: {
|
||||||
|
pulse: {
|
||||||
|
'0%, 100%': { transform: 'scale(1)' },
|
||||||
|
'50%': { transform: 'scale(1.1)' },
|
||||||
|
},
|
||||||
|
shimmer: {
|
||||||
|
'0%': { backgroundPosition: '-200% 0' },
|
||||||
|
'100%': { backgroundPosition: '200% 0' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
pulse: 'pulse 2s ease-in-out infinite',
|
||||||
|
shimmer: 'shimmer 3s ease-in-out infinite',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Transitions
|
||||||
|
transitionDuration: {
|
||||||
|
'300': '300ms',
|
||||||
|
'1000': '1000ms',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
} satisfies Config
|
||||||
414
tasks/00-PROGRESS.md
Normal file
@@ -0,0 +1,414 @@
|
|||||||
|
# 📊 MVP Implementation Progress
|
||||||
|
|
||||||
|
## my.experimenta.science
|
||||||
|
|
||||||
|
**Last Updated:** 2025-10-29
|
||||||
|
**Overall Progress:** 9/137 tasks (6.6%)
|
||||||
|
**Current Phase:** ✅ Phase 1 - Foundation (Completed)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Quick Status
|
||||||
|
|
||||||
|
| Phase | Status | Progress | Started | Completed |
|
||||||
|
| --------------------------- | ------- | ---------- | ---------- | ---------- |
|
||||||
|
| **01** Foundation | ✅ Done | 9/10 (90%) | 2025-10-29 | 2025-10-29 |
|
||||||
|
| **02** Database | ⏳ Todo | 0/12 (0%) | - | - |
|
||||||
|
| **03** Authentication | ⏳ Todo | 0/18 (0%) | - | - |
|
||||||
|
| **04** Products | ⏳ Todo | 0/10 (0%) | - | - |
|
||||||
|
| **05** Cart | ⏳ Todo | 0/12 (0%) | - | - |
|
||||||
|
| **06** Checkout | ⏳ Todo | 0/15 (0%) | - | - |
|
||||||
|
| **07** Payment | ⏳ Todo | 0/12 (0%) | - | - |
|
||||||
|
| **08** Order Processing | ⏳ Todo | 0/15 (0%) | - | - |
|
||||||
|
| **09** ERP Integration | ⏳ Todo | 0/10 (0%) | - | - |
|
||||||
|
| **10** i18n | ⏳ Todo | 0/8 (0%) | - | - |
|
||||||
|
| **11** Testing & Deployment | ⏳ Todo | 0/15 (0%) | - | - |
|
||||||
|
|
||||||
|
**Legend:** ⏳ Todo | 🔄 In Progress | ✅ Done | 🚫 Blocked | ⏭️ Skipped
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Current Work
|
||||||
|
|
||||||
|
**Phase:** Phase 1 - Foundation ✅ **COMPLETED**
|
||||||
|
|
||||||
|
**Tasks Completed (9/10):**
|
||||||
|
|
||||||
|
- ✅ Initialize Nuxt 4 project with pnpm (v4.2.0)
|
||||||
|
- ✅ Copy .env.example to .env and configure
|
||||||
|
- ✅ Install shadcn-nuxt module (v2.3.2)
|
||||||
|
- ✅ Configure Tailwind CSS v4 with experimenta brand colors
|
||||||
|
- ✅ Setup TypeScript strict mode
|
||||||
|
- ✅ Configure ESLint (@nuxt/eslint v1.10.0)
|
||||||
|
- ✅ Configure Prettier (v3.6.2)
|
||||||
|
- ✅ Create basic folder structure
|
||||||
|
- ✅ Configure nuxt.config.ts
|
||||||
|
- ✅ Create basic layout components (app.vue, layouts, Header, Footer)
|
||||||
|
- ✅ Test development server
|
||||||
|
- ⏭️ Setup Git hooks with husky (Skipped - deferred to Phase 11)
|
||||||
|
|
||||||
|
**Pending:**
|
||||||
|
|
||||||
|
- ⚠️ Start Docker services (PostgreSQL + Redis) - **Manual action required**
|
||||||
|
|
||||||
|
**Next Steps:**
|
||||||
|
|
||||||
|
1. **Manual Action Required:** Start Docker Desktop and run:
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.dev.yml up -d
|
||||||
|
```
|
||||||
|
2. **Begin Phase 2 - Database Setup:**
|
||||||
|
- Read `tasks/02-database.md`
|
||||||
|
- Install Drizzle ORM and PostgreSQL driver
|
||||||
|
- Create database schemas
|
||||||
|
- Generate and apply migrations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 Timeline
|
||||||
|
|
||||||
|
### Week 1 (Target)
|
||||||
|
|
||||||
|
- [x] Phase 1: Foundation ✅ **COMPLETED 2025-10-29**
|
||||||
|
- [ ] Phase 2: Database
|
||||||
|
- [ ] Phase 3: Authentication
|
||||||
|
|
||||||
|
### Week 2 (Target)
|
||||||
|
|
||||||
|
- [ ] Phase 4: Products
|
||||||
|
- [ ] Phase 5: Cart
|
||||||
|
- [ ] Phase 6: Checkout
|
||||||
|
|
||||||
|
### Week 3 (Target)
|
||||||
|
|
||||||
|
- [ ] Phase 7: Payment
|
||||||
|
- [ ] Phase 8: Order Processing
|
||||||
|
- [ ] Phase 9: ERP Integration
|
||||||
|
|
||||||
|
### Week 4 (Target)
|
||||||
|
|
||||||
|
- [ ] Phase 10: i18n
|
||||||
|
- [ ] Phase 11: Testing & Deployment
|
||||||
|
- [ ] MVP Launch 🎉
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚧 Blockers
|
||||||
|
|
||||||
|
**Phase 2 (Database):** Docker services (PostgreSQL + Redis) need to be started before database setup can begin.
|
||||||
|
|
||||||
|
- **Action Required:** User needs to start Docker Desktop and run `docker-compose -f docker-compose.dev.yml up -d`
|
||||||
|
- **Impact:** Blocks Phase 2 start
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Decisions Needed
|
||||||
|
|
||||||
|
**None currently.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Completed Milestones
|
||||||
|
|
||||||
|
- [x] Planning & Documentation (PRD, Architecture, Tech Stack)
|
||||||
|
- [x] Docker Development Setup (docker-compose.dev.yml)
|
||||||
|
- [x] Task Management System Setup
|
||||||
|
- [x] **Phase 1 - Foundation (2025-10-29)**
|
||||||
|
- Nuxt 4 project initialized
|
||||||
|
- shadcn-nuxt and Tailwind CSS configured
|
||||||
|
- TypeScript strict mode enabled
|
||||||
|
- ESLint and Prettier configured
|
||||||
|
- Basic project structure created
|
||||||
|
- Development server tested successfully
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Phase Details
|
||||||
|
|
||||||
|
### Phase 1: Foundation (Nuxt 4 Setup)
|
||||||
|
|
||||||
|
**Status:** ✅ Done | **Progress:** 9/10 (90%)
|
||||||
|
|
||||||
|
Tasks:
|
||||||
|
|
||||||
|
- [x] Initialize Nuxt 4 project with pnpm
|
||||||
|
- [x] Install shadcn-nuxt module
|
||||||
|
- [x] Configure Tailwind CSS v4
|
||||||
|
- [x] Setup TypeScript strict mode
|
||||||
|
- [x] Configure ESLint + Prettier
|
||||||
|
- [x] Setup Git hooks (⏭️ Skipped - deferred to Phase 11)
|
||||||
|
- [x] Create basic folder structure
|
||||||
|
- [x] Configure nuxt.config.ts
|
||||||
|
- [x] Create basic layout components
|
||||||
|
- [x] Test development server
|
||||||
|
|
||||||
|
**Note:** Docker services not started (requires manual action). All other tasks completed successfully.
|
||||||
|
|
||||||
|
[Details: tasks/01-foundation.md](./01-foundation.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2: Database (Drizzle ORM)
|
||||||
|
|
||||||
|
**Status:** ⏳ Todo | **Progress:** 0/12 (0%)
|
||||||
|
|
||||||
|
Tasks:
|
||||||
|
|
||||||
|
- [ ] Install Drizzle ORM & PostgreSQL driver
|
||||||
|
- [ ] Configure drizzle.config.ts
|
||||||
|
- [ ] Create users table schema
|
||||||
|
- [ ] Create products table schema
|
||||||
|
- [ ] Create carts & cart_items schema
|
||||||
|
- [ ] Create orders & order_items schema
|
||||||
|
- [ ] Generate initial migration
|
||||||
|
- [ ] Apply migrations to dev DB
|
||||||
|
- [ ] Create database connection utility
|
||||||
|
- [ ] Test CRUD operations
|
||||||
|
- [ ] Setup Drizzle Studio
|
||||||
|
- [ ] Document schema decisions
|
||||||
|
|
||||||
|
[Details: tasks/02-database.md](./02-database.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: Authentication (Cidaas OAuth2)
|
||||||
|
|
||||||
|
**Status:** ⏳ Todo | **Progress:** 0/18 (0%)
|
||||||
|
|
||||||
|
Tasks:
|
||||||
|
|
||||||
|
- [ ] Install nuxt-auth-utils + jose
|
||||||
|
- [ ] Create PKCE generator utility
|
||||||
|
- [ ] Create Cidaas API client
|
||||||
|
- [ ] Create JWT validation utility
|
||||||
|
- [ ] Implement /api/auth/login endpoint
|
||||||
|
- [ ] Implement /api/auth/callback endpoint
|
||||||
|
- [ ] Implement /api/auth/register endpoint
|
||||||
|
- [ ] Implement /api/auth/logout endpoint
|
||||||
|
- [ ] Implement /api/auth/me endpoint
|
||||||
|
- [ ] Create useAuth composable
|
||||||
|
- [ ] Create LoginForm component
|
||||||
|
- [ ] Create RegisterForm component
|
||||||
|
- [ ] Create auth page with tabs
|
||||||
|
- [ ] Create auth middleware
|
||||||
|
- [ ] Create rate-limit middleware
|
||||||
|
- [ ] Test OAuth2 flow end-to-end
|
||||||
|
- [ ] Test session management
|
||||||
|
- [ ] Document authentication flow
|
||||||
|
|
||||||
|
[Details: tasks/03-authentication.md](./03-authentication.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 4: Products (Display & List)
|
||||||
|
|
||||||
|
**Status:** ⏳ Todo | **Progress:** 0/10 (0%)
|
||||||
|
|
||||||
|
Tasks:
|
||||||
|
|
||||||
|
- [ ] Create /api/products/index.get.ts endpoint
|
||||||
|
- [ ] Create /api/products/[id].get.ts endpoint
|
||||||
|
- [ ] Create ProductCard component
|
||||||
|
- [ ] Create ProductList component
|
||||||
|
- [ ] Create ProductDetail page
|
||||||
|
- [ ] Create products index page
|
||||||
|
- [ ] Add product images handling
|
||||||
|
- [ ] Test product display
|
||||||
|
- [ ] Optimize product queries
|
||||||
|
- [ ] Document product schema
|
||||||
|
|
||||||
|
[Details: tasks/04-products.md](./04-products.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 5: Cart (Shopping Cart)
|
||||||
|
|
||||||
|
**Status:** ⏳ Todo | **Progress:** 0/12 (0%)
|
||||||
|
|
||||||
|
Tasks:
|
||||||
|
|
||||||
|
- [ ] Create /api/cart/index.get.ts endpoint
|
||||||
|
- [ ] Create /api/cart/items.post.ts endpoint
|
||||||
|
- [ ] Create /api/cart/items/[id].patch.ts endpoint
|
||||||
|
- [ ] Create /api/cart/items/[id].delete.ts endpoint
|
||||||
|
- [ ] Create useCart composable
|
||||||
|
- [ ] Create CartItem component
|
||||||
|
- [ ] Create CartSummary component
|
||||||
|
- [ ] Create cart page
|
||||||
|
- [ ] Test cart operations
|
||||||
|
- [ ] Add cart persistence
|
||||||
|
- [ ] Optimize cart queries
|
||||||
|
- [ ] Document cart logic
|
||||||
|
|
||||||
|
[Details: tasks/05-cart.md](./05-cart.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 6: Checkout (Forms & Flow)
|
||||||
|
|
||||||
|
**Status:** ⏳ Todo | **Progress:** 0/15 (0%)
|
||||||
|
|
||||||
|
Tasks:
|
||||||
|
|
||||||
|
- [ ] Create checkout schema (Zod)
|
||||||
|
- [ ] Create CheckoutForm component
|
||||||
|
- [ ] Create AddressForm component
|
||||||
|
- [ ] Implement address pre-fill from user profile
|
||||||
|
- [ ] Create /api/checkout/validate endpoint
|
||||||
|
- [ ] Create checkout page
|
||||||
|
- [ ] Implement save address to profile
|
||||||
|
- [ ] Add form validation (VeeValidate)
|
||||||
|
- [ ] Test checkout flow
|
||||||
|
- [ ] Test address save/load
|
||||||
|
- [ ] Add error handling
|
||||||
|
- [ ] Optimize checkout UX
|
||||||
|
- [ ] Add loading states
|
||||||
|
- [ ] Test mobile checkout
|
||||||
|
- [ ] Document checkout logic
|
||||||
|
|
||||||
|
[Details: tasks/06-checkout.md](./06-checkout.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 7: Payment (PayPal Integration)
|
||||||
|
|
||||||
|
**Status:** ⏳ Todo | **Progress:** 0/12 (0%)
|
||||||
|
|
||||||
|
Tasks:
|
||||||
|
|
||||||
|
- [ ] Install PayPal SDK
|
||||||
|
- [ ] Configure PayPal credentials
|
||||||
|
- [ ] Create /api/payment/paypal/create.post.ts endpoint
|
||||||
|
- [ ] Create /api/payment/paypal/capture.post.ts endpoint
|
||||||
|
- [ ] Create /api/payment/paypal/webhook.post.ts endpoint
|
||||||
|
- [ ] Integrate PayPal button on checkout
|
||||||
|
- [ ] Implement payment success flow
|
||||||
|
- [ ] Implement payment error handling
|
||||||
|
- [ ] Test PayPal sandbox
|
||||||
|
- [ ] Add payment status tracking
|
||||||
|
- [ ] Document PayPal integration
|
||||||
|
- [ ] Test webhook handling
|
||||||
|
|
||||||
|
[Details: tasks/07-payment.md](./07-payment.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 8: Order Processing (BullMQ + X-API)
|
||||||
|
|
||||||
|
**Status:** ⏳ Todo | **Progress:** 0/15 (0%)
|
||||||
|
|
||||||
|
Tasks:
|
||||||
|
|
||||||
|
- [ ] Install BullMQ + ioredis
|
||||||
|
- [ ] Configure Redis connection
|
||||||
|
- [ ] Create order queue
|
||||||
|
- [ ] Create order worker
|
||||||
|
- [ ] Create X-API client utility
|
||||||
|
- [ ] Implement transformOrderToXAPI function
|
||||||
|
- [ ] Implement submitOrderToXAPI with retry
|
||||||
|
- [ ] Create /api/orders/index.post.ts endpoint
|
||||||
|
- [ ] Create /api/orders/[id].get.ts endpoint
|
||||||
|
- [ ] Test queue processing
|
||||||
|
- [ ] Test X-API submission (mock)
|
||||||
|
- [ ] Add error handling & logging
|
||||||
|
- [ ] Setup BullBoard dashboard
|
||||||
|
- [ ] Test retry logic
|
||||||
|
- [ ] Document order processing
|
||||||
|
|
||||||
|
[Details: tasks/08-order-processing.md](./08-order-processing.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 9: ERP Integration (NAV Product Sync)
|
||||||
|
|
||||||
|
**Status:** ⏳ Todo | **Progress:** 0/10 (0%)
|
||||||
|
|
||||||
|
Tasks:
|
||||||
|
|
||||||
|
- [ ] Create NAV ERP product schema (Zod)
|
||||||
|
- [ ] Create /api/erp/products.post.ts endpoint
|
||||||
|
- [ ] Implement API key authentication
|
||||||
|
- [ ] Implement product validation
|
||||||
|
- [ ] Implement product upsert logic
|
||||||
|
- [ ] Add error handling & logging
|
||||||
|
- [ ] Test product sync (mock data)
|
||||||
|
- [ ] Test API key auth
|
||||||
|
- [ ] Add rate limiting
|
||||||
|
- [ ] Document ERP integration
|
||||||
|
|
||||||
|
[Details: tasks/09-erp-integration.md](./09-erp-integration.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 10: i18n (Internationalization)
|
||||||
|
|
||||||
|
**Status:** ⏳ Todo | **Progress:** 0/8 (0%)
|
||||||
|
|
||||||
|
Tasks:
|
||||||
|
|
||||||
|
- [ ] Install @nuxtjs/i18n
|
||||||
|
- [ ] Configure i18n module
|
||||||
|
- [ ] Create locale files (de-DE.json, en-US.json)
|
||||||
|
- [ ] Translate all UI strings
|
||||||
|
- [ ] Create language switcher component
|
||||||
|
- [ ] Test route localization
|
||||||
|
- [ ] Test currency/date formatting
|
||||||
|
- [ ] Document i18n structure
|
||||||
|
|
||||||
|
[Details: tasks/10-i18n.md](./10-i18n.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 11: Testing & Deployment
|
||||||
|
|
||||||
|
**Status:** ⏳ Todo | **Progress:** 0/15 (0%)
|
||||||
|
|
||||||
|
Tasks:
|
||||||
|
|
||||||
|
- [ ] Setup Vitest for unit tests
|
||||||
|
- [ ] Write tests for auth utilities
|
||||||
|
- [ ] Write tests for API endpoints
|
||||||
|
- [ ] Setup Playwright for E2E
|
||||||
|
- [ ] Write E2E test: user registration
|
||||||
|
- [ ] Write E2E test: complete checkout flow
|
||||||
|
- [ ] Create Dockerfile (production)
|
||||||
|
- [ ] Create docker-compose.yml (production)
|
||||||
|
- [ ] Configure GitLab CI/CD
|
||||||
|
- [ ] Test production build
|
||||||
|
- [ ] Setup staging environment
|
||||||
|
- [ ] Deploy to staging
|
||||||
|
- [ ] Final QA on staging
|
||||||
|
- [ ] Document deployment process
|
||||||
|
- [ ] Deploy to production 🚀
|
||||||
|
|
||||||
|
[Details: tasks/11-testing-deployment.md](./11-testing-deployment.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 Progress Over Time
|
||||||
|
|
||||||
|
| Date | Overall Progress | Phase | Notes |
|
||||||
|
| ---------- | ---------------- | ------------- | ------------------------------------------------------------------------------------------- |
|
||||||
|
| 2025-01-29 | 0% | Planning | Task system created |
|
||||||
|
| 2025-10-29 | 6.6% | Phase 1 - MVP | ✅ Foundation completed: Nuxt 4, shadcn-nuxt, Tailwind CSS, ESLint, Prettier all configured |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 Next Steps
|
||||||
|
|
||||||
|
1. ⚠️ **Manual Action Required:** Start Docker services
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start Docker Desktop, then run:
|
||||||
|
docker-compose -f docker-compose.dev.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Start Phase 2: Database Setup**
|
||||||
|
- Read `tasks/02-database.md` for detailed tasks
|
||||||
|
- Install Drizzle ORM and PostgreSQL driver
|
||||||
|
- Create database schemas
|
||||||
|
- Generate and apply migrations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Let's build this! 🚀**
|
||||||
219
tasks/01-foundation.md
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
# Phase 1: Foundation (Nuxt 4 Setup)
|
||||||
|
|
||||||
|
**Status:** ✅ Done
|
||||||
|
**Progress:** 9/10 tasks (90%)
|
||||||
|
**Started:** 2025-10-29
|
||||||
|
**Completed:** 2025-10-29
|
||||||
|
**Assigned to:** -
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Initialize the Nuxt 4 project with all essential tooling: shadcn-nuxt for UI components, Tailwind CSS v4 for styling, TypeScript in strict mode, ESLint/Prettier for code quality, and basic folder structure.
|
||||||
|
|
||||||
|
**Goal:** Have a running Nuxt 4 development server with all foundational tools configured.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- ✅ Docker development environment (docker-compose.dev.yml) - Already created
|
||||||
|
- ✅ .env.example - Already created
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
### Project Initialization
|
||||||
|
|
||||||
|
- [x] Initialize Nuxt 4 project with pnpm
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dlx nuxi@latest init .
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
**Completed:** Nuxt 4.2.0 installed with all core dependencies
|
||||||
|
|
||||||
|
- [ ] Start Docker services (PostgreSQL + Redis)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.dev.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Docker daemon not running - user needs to start Docker Desktop
|
||||||
|
|
||||||
|
- [x] Copy .env.example to .env and configure basic values
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env: Set DATABASE_URL, REDIS_HOST, NUXT_SESSION_PASSWORD
|
||||||
|
```
|
||||||
|
**Completed:** .env file created from template
|
||||||
|
|
||||||
|
### UI Framework
|
||||||
|
|
||||||
|
- [x] Install shadcn-nuxt module
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add -D shadcn-nuxt
|
||||||
|
npx shadcn-nuxt@latest init
|
||||||
|
```
|
||||||
|
|
||||||
|
**Completed:** shadcn-nuxt v2.3.2 installed, Button component added and tested
|
||||||
|
|
||||||
|
- [x] Configure Tailwind CSS v4
|
||||||
|
- Verify Tailwind is installed via shadcn-nuxt ✓
|
||||||
|
- Customize `tailwind.config.ts` with experimenta colors/fonts ✓
|
||||||
|
- Test Tailwind classes in a component ✓
|
||||||
|
|
||||||
|
**Completed:** Tailwind CSS v4 configured with CSS variables, experimenta brand colors preserved
|
||||||
|
|
||||||
|
### TypeScript Configuration
|
||||||
|
|
||||||
|
- [x] Setup TypeScript strict mode
|
||||||
|
- Edit `tsconfig.json`: Set `"strict": true` ✓
|
||||||
|
- Add `"noUncheckedIndexedAccess": true` ✓
|
||||||
|
- Add `"noImplicitOverride": true` ✓
|
||||||
|
|
||||||
|
**Completed:** TypeScript strict mode already configured in Nuxt 4, verified all options are set
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
|
||||||
|
- [x] Configure ESLint
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add -D @nuxt/eslint eslint
|
||||||
|
```
|
||||||
|
|
||||||
|
- Create `eslint.config.mjs` with @nuxt/eslint flat config ✓
|
||||||
|
- Add lint script to package.json: `"lint": "eslint ."` ✓
|
||||||
|
|
||||||
|
**Completed:** @nuxt/eslint v1.10.0 installed, configured with Prettier integration
|
||||||
|
|
||||||
|
- [x] Configure Prettier
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add -D prettier eslint-config-prettier eslint-plugin-prettier
|
||||||
|
```
|
||||||
|
|
||||||
|
- Create `.prettierrc.json` with rules ✓
|
||||||
|
- Add format script: `"format": "prettier --write ."` ✓
|
||||||
|
- Create `.prettierignore` ✓
|
||||||
|
|
||||||
|
**Completed:** Prettier v3.6.2 installed, 47 files formatted successfully
|
||||||
|
|
||||||
|
- [x] Setup Git hooks with husky (optional for now, can defer to Phase 11)
|
||||||
|
- ⏭️ **Skipped** - Will be added in Phase 11 (Testing & Deployment)
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
- [x] Create basic folder structure
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p server/api/{auth,products,cart,orders,payment,erp}
|
||||||
|
mkdir -p server/database
|
||||||
|
mkdir -p server/utils
|
||||||
|
mkdir -p server/middleware
|
||||||
|
mkdir -p components/{Auth,Product,Cart,Checkout,Common}
|
||||||
|
mkdir -p composables
|
||||||
|
mkdir -p middleware
|
||||||
|
mkdir -p locales
|
||||||
|
```
|
||||||
|
|
||||||
|
**Completed:** All directories created, i18n locale files added (de-DE.json, en-US.json)
|
||||||
|
|
||||||
|
- [x] Configure nuxt.config.ts
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
compatibilityDate: '2025-01-29',
|
||||||
|
devtools: { enabled: true },
|
||||||
|
modules: ['shadcn-nuxt'],
|
||||||
|
typescript: {
|
||||||
|
strict: true,
|
||||||
|
typeCheck: true,
|
||||||
|
},
|
||||||
|
runtimeConfig: {
|
||||||
|
// Server-only
|
||||||
|
databaseUrl: process.env.DATABASE_URL,
|
||||||
|
redisHost: process.env.REDIS_HOST,
|
||||||
|
redisPort: process.env.REDIS_PORT,
|
||||||
|
// Public (exposed to client)
|
||||||
|
public: {
|
||||||
|
appUrl: process.env.APP_URL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Completed:** nuxt.config.ts configured with all runtime config, modules, i18n, TypeScript strict mode
|
||||||
|
|
||||||
|
- [x] Create basic layout components
|
||||||
|
- `app/app.vue`: Root app file with <NuxtPage /> ✓
|
||||||
|
- `app/layouts/default.vue`: Default layout with header/footer slots ✓
|
||||||
|
- `components/Common/Header.vue`: Placeholder header ✓
|
||||||
|
- `components/Common/Footer.vue`: Placeholder footer ✓
|
||||||
|
|
||||||
|
**Completed:** All layout components created with TypeScript and Tailwind CSS
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
|
||||||
|
- [x] Test development server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
- Open http://localhost:3000 ✓
|
||||||
|
- Verify Nuxt welcome page or custom layout loads ✓
|
||||||
|
- Verify no TypeScript errors ✓
|
||||||
|
- Verify Tailwind classes work ✓
|
||||||
|
- Verify hot reload works ✓
|
||||||
|
|
||||||
|
**Completed:** Dev server tested successfully, shadcn Button components render correctly
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] Nuxt 4 project is initialized
|
||||||
|
- [x] Development server runs without errors on http://localhost:3000
|
||||||
|
- [x] shadcn-nuxt is installed and configured
|
||||||
|
- [x] Tailwind CSS v4 is working (test with utility classes)
|
||||||
|
- [x] TypeScript strict mode is enabled and passes type checking
|
||||||
|
- [x] ESLint and Prettier are configured
|
||||||
|
- [x] Basic folder structure exists
|
||||||
|
- [x] nuxt.config.ts is configured with runtime config
|
||||||
|
- [x] Basic layout (app.vue, layouts/default.vue) exists
|
||||||
|
- [x] Hot reload works
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Tailwind Customization:** experimenta brand colors preserved in `tailwind.config.ts` as `experimenta-primary`, `experimenta-accent`, etc. shadcn uses CSS variables for theming.
|
||||||
|
- **Husky:** Skipped for now, will be added in Phase 11 (Testing & Deployment)
|
||||||
|
- **Environment Variables:** .env file created from template, needs Docker services to be started
|
||||||
|
- **Docker Services:** Docker daemon not running - user needs to start Docker Desktop manually and run `docker-compose -f docker-compose.dev.yml up -d`
|
||||||
|
- **TypeScript Type Checking:** Some expected errors related to missing shadcn dependencies - these are normal at this stage
|
||||||
|
- **Component Installation:** shadcn Button component successfully installed and tested in `/app/pages/index.vue`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blockers
|
||||||
|
|
||||||
|
- ⚠️ **Docker Services:** Docker daemon not running. User needs to manually:
|
||||||
|
1. Start Docker Desktop
|
||||||
|
2. Run: `docker-compose -f docker-compose.dev.yml up -d`
|
||||||
|
|
||||||
|
This is required for Phase 2 (Database) but not blocking Phase 1 completion.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [TECH_STACK.md: Nuxt 4](../docs/TECH_STACK.md#1-frontend-framework)
|
||||||
|
- [TECH_STACK.md: shadcn-nuxt](../docs/TECH_STACK.md#2-ui-framework)
|
||||||
|
- [TECH_STACK.md: Tailwind CSS](../docs/TECH_STACK.md#3-styling)
|
||||||
|
- [README.md: Local Development](../README.md#lokale-entwicklung)
|
||||||
188
tasks/02-database.md
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
# Phase 2: Database (Drizzle ORM)
|
||||||
|
|
||||||
|
**Status:** ⏳ Todo
|
||||||
|
**Progress:** 0/12 tasks (0%)
|
||||||
|
**Started:** -
|
||||||
|
**Completed:** -
|
||||||
|
**Assigned to:** -
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Setup Drizzle ORM, define complete database schema for all tables (users, products, carts, cart_items, orders, order_items), generate and apply migrations, and create database utilities.
|
||||||
|
|
||||||
|
**Goal:** Fully functional database with all MVP tables ready for use.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- ✅ Phase 1: Foundation must be completed
|
||||||
|
- ✅ PostgreSQL running in Docker (docker-compose.dev.yml)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
### Drizzle Setup
|
||||||
|
|
||||||
|
- [ ] Install Drizzle ORM & PostgreSQL driver
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add drizzle-orm postgres
|
||||||
|
pnpm add -D drizzle-kit
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] Configure drizzle.config.ts
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineConfig } from 'drizzle-kit'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
schema: './server/database/schema.ts',
|
||||||
|
out: './server/database/migrations',
|
||||||
|
dialect: 'postgresql',
|
||||||
|
dbCredentials: {
|
||||||
|
url: process.env.DATABASE_URL!,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] Add database scripts to package.json
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"db:generate": "drizzle-kit generate",
|
||||||
|
"db:migrate": "drizzle-kit migrate",
|
||||||
|
"db:studio": "drizzle-kit studio",
|
||||||
|
"db:push": "drizzle-kit push"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schema Definition
|
||||||
|
|
||||||
|
- [ ] Create users table schema
|
||||||
|
- File: `server/database/schema.ts`
|
||||||
|
- Fields: id (UUID), experimenta_id (unique), email, first_name, last_name, salutation, date_of_birth, street, post_code, city, country_code, phone, created_at, updated_at
|
||||||
|
- See: [ARCHITECTURE.md Section 4.1](../docs/ARCHITECTURE.md#41-datenbank-schema)
|
||||||
|
|
||||||
|
- [ ] Create products table schema
|
||||||
|
- Fields: id (UUID), nav_product_id (unique), name, description, price (decimal), stock_quantity, category, active, created_at, updated_at
|
||||||
|
- Indexes: nav_product_id, active, category
|
||||||
|
|
||||||
|
- [ ] Create carts table schema
|
||||||
|
- Fields: id (UUID), user_id (FK to users, nullable), session_id, created_at, updated_at
|
||||||
|
- Relations: hasMany cart_items
|
||||||
|
|
||||||
|
- [ ] Create cart_items table schema
|
||||||
|
- Fields: id (UUID), cart_id (FK to carts), product_id (FK to products), quantity, added_at
|
||||||
|
- Relations: belongsTo cart, belongsTo product
|
||||||
|
|
||||||
|
- [ ] Create orders table schema
|
||||||
|
- Fields: id (UUID), order_number (unique), user_id (FK to users), total_amount, status, billing_address (JSON), payment_id, payment_completed_at, created_at, updated_at
|
||||||
|
- Relations: hasMany order_items
|
||||||
|
- Indexes: order_number, user_id, status
|
||||||
|
|
||||||
|
- [ ] Create order_items table schema
|
||||||
|
- Fields: id (UUID), order_id (FK to orders), product_id (FK to products), product_snapshot (JSON), quantity, price_snapshot, created_at
|
||||||
|
- Relations: belongsTo order, belongsTo product
|
||||||
|
|
||||||
|
### Migrations
|
||||||
|
|
||||||
|
- [ ] Generate initial migration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm db:generate
|
||||||
|
```
|
||||||
|
|
||||||
|
- Verify migration files in `server/database/migrations/`
|
||||||
|
|
||||||
|
- [ ] Apply migrations to dev database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm db:migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
- Verify tables exist in PostgreSQL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec -it experimenta-db-dev psql -U dev -d experimenta_dev -c "\dt"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Utilities
|
||||||
|
|
||||||
|
- [ ] Create database connection utility
|
||||||
|
- File: `server/utils/db.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { drizzle } from 'drizzle-orm/postgres-js'
|
||||||
|
import postgres from 'postgres'
|
||||||
|
import * as schema from '../database/schema'
|
||||||
|
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const client = postgres(config.databaseUrl)
|
||||||
|
export const db = drizzle(client, { schema })
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] Test CRUD operations
|
||||||
|
- Create test endpoint: `server/api/test/db.get.ts`
|
||||||
|
- Test insert, select, update, delete on users table
|
||||||
|
- Verify relations work (e.g., fetch cart with items)
|
||||||
|
- Remove test endpoint after verification
|
||||||
|
|
||||||
|
### Tools
|
||||||
|
|
||||||
|
- [ ] Setup Drizzle Studio
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm db:studio
|
||||||
|
```
|
||||||
|
|
||||||
|
- Open http://localhost:4983
|
||||||
|
- Verify all tables are visible
|
||||||
|
- Test data manipulation via Studio
|
||||||
|
|
||||||
|
- [ ] Document schema decisions
|
||||||
|
- Add comments to schema.ts explaining design choices
|
||||||
|
- Document why JSONB for billing_address
|
||||||
|
- Document why UUID vs serial IDs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] Drizzle ORM is installed and configured
|
||||||
|
- [x] All 6 tables are defined in schema.ts (users, products, carts, cart_items, orders, order_items)
|
||||||
|
- [x] Relations between tables are defined correctly
|
||||||
|
- [x] Initial migration is generated and applied
|
||||||
|
- [x] All tables exist in PostgreSQL database
|
||||||
|
- [x] Database connection utility (db.ts) is working
|
||||||
|
- [x] CRUD operations work as expected
|
||||||
|
- [x] Drizzle Studio can connect and display tables
|
||||||
|
- [x] Schema is documented with comments
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **UUID vs Serial:** Using UUIDs for better distributed systems support and security
|
||||||
|
- **JSONB for billing_address:** Flexible address storage, avoids complex normalized address tables
|
||||||
|
- **Decimal for prices:** Using decimal(10,2) for accurate money calculations
|
||||||
|
- **created_at/updated_at:** Timestamps for audit trail
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blockers
|
||||||
|
|
||||||
|
- None currently
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [ARCHITECTURE.md: Database Schema](../docs/ARCHITECTURE.md#41-datenbank-schema)
|
||||||
|
- [TECH_STACK.md: Drizzle ORM](../docs/TECH_STACK.md#5-orm)
|
||||||
|
- [TECH_STACK.md: PostgreSQL](../docs/TECH_STACK.md#4-datenbank)
|
||||||
|
- [CLAUDE.md: Database Schema](../CLAUDE.md#database-schema)
|
||||||
228
tasks/03-authentication.md
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
# Phase 3: Authentication (Cidaas OAuth2/OIDC)
|
||||||
|
|
||||||
|
**Status:** ⏳ Todo
|
||||||
|
**Progress:** 0/18 tasks (0%)
|
||||||
|
**Started:** -
|
||||||
|
**Completed:** -
|
||||||
|
**Assigned to:** -
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Implement complete Cidaas OAuth2/OIDC authentication with custom UI: login, registration, logout, session management, JWT validation, and rate limiting.
|
||||||
|
|
||||||
|
**Goal:** Fully functional authentication system with custom experimenta-branded login/registration UI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- ✅ Phase 1: Foundation must be completed
|
||||||
|
- ✅ Phase 2: Database must be completed (users table needed)
|
||||||
|
- ⚠️ **Required:** Cidaas credentials (CLIENT_ID, CLIENT_SECRET, BASE_URL)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
### Dependencies Installation
|
||||||
|
|
||||||
|
- [ ] Install nuxt-auth-utils + jose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add nuxt-auth-utils jose
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] Configure Cidaas environment variables in .env
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CIDAAS_BASE_URL=https://experimenta.cidaas.de
|
||||||
|
CIDAAS_CLIENT_ID=xxx
|
||||||
|
CIDAAS_CLIENT_SECRET=xxx
|
||||||
|
CIDAAS_REDIRECT_URI=http://localhost:3000/api/auth/callback
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] Add Cidaas config to nuxt.config.ts runtimeConfig
|
||||||
|
```typescript
|
||||||
|
runtimeConfig: {
|
||||||
|
cidaas: {
|
||||||
|
baseUrl: process.env.CIDAAS_BASE_URL,
|
||||||
|
clientId: process.env.CIDAAS_CLIENT_ID,
|
||||||
|
clientSecret: process.env.CIDAAS_CLIENT_SECRET,
|
||||||
|
redirectUri: process.env.CIDAAS_REDIRECT_URI,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server Utilities
|
||||||
|
|
||||||
|
- [ ] Create PKCE generator utility
|
||||||
|
- File: `server/utils/pkce.ts`
|
||||||
|
- Functions: `generatePKCE()` → returns { verifier, challenge }
|
||||||
|
- Implementation: See [CIDAAS_INTEGRATION.md](../docs/CIDAAS_INTEGRATION.md#5-server-utilities)
|
||||||
|
|
||||||
|
- [ ] Create Cidaas API client utility
|
||||||
|
- File: `server/utils/cidaas.ts`
|
||||||
|
- Functions:
|
||||||
|
- `exchangeCodeForToken(code, verifier)` → tokens
|
||||||
|
- `fetchUserInfo(accessToken)` → user data
|
||||||
|
- `registerUser(userData)` → registration result
|
||||||
|
- See: [CIDAAS_INTEGRATION.md](../docs/CIDAAS_INTEGRATION.md#5-server-utilities)
|
||||||
|
|
||||||
|
- [ ] Create JWT validation utility
|
||||||
|
- File: `server/utils/jwt.ts`
|
||||||
|
- Function: `verifyIdToken(idToken)` → payload
|
||||||
|
- Uses: jose library with JWKS
|
||||||
|
- See: [CLAUDE.md: JWT Validation Pattern](../CLAUDE.md#jwt-validation-pattern)
|
||||||
|
|
||||||
|
### Auth API Endpoints
|
||||||
|
|
||||||
|
- [ ] Create /api/auth/login.post.ts endpoint
|
||||||
|
- Generates PKCE challenge & state
|
||||||
|
- Stores in HTTP-only cookies (5min TTL)
|
||||||
|
- Returns Cidaas authorization URL
|
||||||
|
- See: [CLAUDE.md: OAuth2 Login Flow](../CLAUDE.md#oauth2-login-flow-pattern)
|
||||||
|
|
||||||
|
- [ ] Create /api/auth/callback.get.ts endpoint
|
||||||
|
- Validates state (CSRF protection)
|
||||||
|
- Exchanges code for tokens (with PKCE)
|
||||||
|
- Validates ID token (JWT)
|
||||||
|
- Fetches user info from Cidaas
|
||||||
|
- Creates/updates user in local DB
|
||||||
|
- Creates encrypted session (nuxt-auth-utils)
|
||||||
|
- Redirects to homepage
|
||||||
|
- See: [CLAUDE.md: OAuth2 Callback](../CLAUDE.md#oauth2-callback-pattern)
|
||||||
|
|
||||||
|
- [ ] Create /api/auth/register.post.ts endpoint
|
||||||
|
- Validates registration data (Zod schema)
|
||||||
|
- Calls Cidaas registration API
|
||||||
|
- Returns success/error
|
||||||
|
- See: [CLAUDE.md: User Registration](../CLAUDE.md#user-registration-pattern)
|
||||||
|
|
||||||
|
- [ ] Create /api/auth/logout.post.ts endpoint
|
||||||
|
- Clears session via clearUserSession()
|
||||||
|
- Optional: Single Sign-Out at Cidaas
|
||||||
|
- Returns success
|
||||||
|
|
||||||
|
- [ ] Create /api/auth/me.get.ts endpoint
|
||||||
|
- Protected endpoint (requires session)
|
||||||
|
- Returns current user data
|
||||||
|
- Uses: requireUserSession()
|
||||||
|
|
||||||
|
### Client-Side Composables
|
||||||
|
|
||||||
|
- [ ] Create useAuth composable
|
||||||
|
- File: `composables/useAuth.ts`
|
||||||
|
- Functions:
|
||||||
|
- `login(email)` → redirects to Cidaas
|
||||||
|
- `logout()` → clears session, redirects
|
||||||
|
- `register(data)` → calls registration API
|
||||||
|
- Uses: useUserSession from nuxt-auth-utils
|
||||||
|
- Returns: { user, loggedIn, login, logout, register }
|
||||||
|
- See: [CLAUDE.md: OAuth2 Login Flow](../CLAUDE.md#oauth2-login-flow-pattern)
|
||||||
|
|
||||||
|
### UI Components
|
||||||
|
|
||||||
|
- [ ] Create LoginForm component
|
||||||
|
- File: `components/Auth/LoginForm.vue`
|
||||||
|
- Fields: Email input
|
||||||
|
- Button: "Login with Cidaas"
|
||||||
|
- Calls: `login(email)` from useAuth
|
||||||
|
- See: [CIDAAS_INTEGRATION.md: UI Components](../docs/CIDAAS_INTEGRATION.md#8-ui-components)
|
||||||
|
|
||||||
|
- [ ] Create RegisterForm component
|
||||||
|
- File: `components/Auth/RegisterForm.vue`
|
||||||
|
- Fields: Email, Password, Confirm Password, First Name, Last Name
|
||||||
|
- Validation: VeeValidate + Zod
|
||||||
|
- Calls: `register(data)` from useAuth
|
||||||
|
- See: [CIDAAS_INTEGRATION.md: UI Components](../docs/CIDAAS_INTEGRATION.md#8-ui-components)
|
||||||
|
|
||||||
|
- [ ] Create auth page with tabs
|
||||||
|
- File: `pages/auth.vue`
|
||||||
|
- Tabs: Login | Register (shadcn-nuxt Tabs component)
|
||||||
|
- Embeds: LoginForm + RegisterForm
|
||||||
|
- Styling: experimenta branding
|
||||||
|
- See: [CIDAAS_INTEGRATION.md: UI Components](../docs/CIDAAS_INTEGRATION.md#8-ui-components)
|
||||||
|
|
||||||
|
### Middleware
|
||||||
|
|
||||||
|
- [ ] Create auth middleware
|
||||||
|
- File: `middleware/auth.ts`
|
||||||
|
- Redirects to /auth if not logged in
|
||||||
|
- Stores intended destination for post-login redirect
|
||||||
|
- See: [CLAUDE.md: Protected Route Middleware](../CLAUDE.md#protected-route-middleware-pattern)
|
||||||
|
|
||||||
|
- [ ] Create rate-limit middleware
|
||||||
|
- File: `server/middleware/rate-limit.ts`
|
||||||
|
- Limits:
|
||||||
|
- /api/auth/login: 5 attempts / 15min per IP
|
||||||
|
- /api/auth/register: 3 attempts / 1hour per IP
|
||||||
|
- Returns 429 on exceed
|
||||||
|
- See: [CLAUDE.md: Rate Limiting](../CLAUDE.md#rate-limiting-pattern)
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- [ ] Test OAuth2 flow end-to-end
|
||||||
|
- Start at /auth page
|
||||||
|
- Click "Login"
|
||||||
|
- Redirect to Cidaas (if credentials configured)
|
||||||
|
- Complete login
|
||||||
|
- Verify callback works
|
||||||
|
- Verify user created in DB
|
||||||
|
- Verify session works
|
||||||
|
|
||||||
|
- [ ] Test session management
|
||||||
|
- Verify session persists across page reloads
|
||||||
|
- Verify session expires after 30 days (or config)
|
||||||
|
- Test logout clears session
|
||||||
|
|
||||||
|
- [ ] Document authentication flow
|
||||||
|
- Add detailed flow diagram to docs/CIDAAS_INTEGRATION.md (already exists)
|
||||||
|
- Document any deviations from plan
|
||||||
|
- Document Cidaas-specific quirks encountered
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] nuxt-auth-utils and jose are installed
|
||||||
|
- [x] All utilities (PKCE, Cidaas client, JWT) are implemented
|
||||||
|
- [x] All 5 auth endpoints work correctly
|
||||||
|
- [x] useAuth composable is functional
|
||||||
|
- [x] LoginForm and RegisterForm components are styled and functional
|
||||||
|
- [x] /auth page shows tabs with both forms
|
||||||
|
- [x] auth middleware protects routes correctly
|
||||||
|
- [x] rate-limit middleware works and returns 429 when exceeded
|
||||||
|
- [x] OAuth2 flow works end-to-end (login → callback → session)
|
||||||
|
- [x] Session management works (persist, expire, clear)
|
||||||
|
- [x] User is created/updated in local DB on first login
|
||||||
|
- [x] JWT tokens are validated correctly
|
||||||
|
- [x] PKCE flow prevents authorization code interception
|
||||||
|
- [x] State parameter prevents CSRF attacks
|
||||||
|
- [x] Authentication is fully documented
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Cidaas Credentials:** You'll need to request CLIENT_ID and CLIENT_SECRET from experimenta admin
|
||||||
|
- **Redirect URI:** Must be registered in Cidaas Admin Panel: `http://localhost:3000/api/auth/callback` (dev), `https://my.experimenta.science/api/auth/callback` (prod)
|
||||||
|
- **Session Duration:** Configured to 30 days (can be adjusted in nuxt-auth-utils config)
|
||||||
|
- **Custom UI:** We're NOT using Cidaas hosted pages - fully custom experimenta-branded UI
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blockers
|
||||||
|
|
||||||
|
- ⚠️ **Cidaas Credentials Missing:** Cannot test OAuth2 flow without CLIENT_ID/SECRET
|
||||||
|
- **Workaround:** Implement everything, test with mock/manual verification until credentials available
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [docs/CIDAAS_INTEGRATION.md](../docs/CIDAAS_INTEGRATION.md) - Complete implementation guide
|
||||||
|
- [docs/ARCHITECTURE.md: Section 3.6](../docs/ARCHITECTURE.md#36-authentication--authorization-cidaas-oauth2oidc)
|
||||||
|
- [CLAUDE.md: Authentication Patterns](../CLAUDE.md#authentication-patterns)
|
||||||
|
- [docs/PRD.md: US-001, US-002](../docs/PRD.md#51-authentifizierung--benutzerverwaltung)
|
||||||
162
tasks/04-products.md
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
# Phase 4: Products (Display & List)
|
||||||
|
|
||||||
|
**Status:** ⏳ Todo
|
||||||
|
**Progress:** 0/10 tasks (0%)
|
||||||
|
**Started:** -
|
||||||
|
**Completed:** -
|
||||||
|
**Assigned to:** -
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Implement product display functionality: API endpoints for fetching products, product list/detail pages, and UI components for displaying Makerspace annual passes.
|
||||||
|
|
||||||
|
**Goal:** Users can browse and view Makerspace-Jahreskarten products.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- ✅ Phase 1: Foundation must be completed
|
||||||
|
- ✅ Phase 2: Database must be completed (products table needed)
|
||||||
|
- ⚠️ **Optional:** Phase 3: Authentication (for future protected features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
- [ ] Create /api/products/index.get.ts endpoint
|
||||||
|
- Query products from DB (Drizzle)
|
||||||
|
- Filter by: active=true, category (optional)
|
||||||
|
- Sort by: name, price
|
||||||
|
- Return: Array of products
|
||||||
|
- Example:
|
||||||
|
```typescript
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const { category } = getQuery(event)
|
||||||
|
const products = await db.query.products.findMany({
|
||||||
|
where: and(
|
||||||
|
eq(products.active, true),
|
||||||
|
category ? eq(products.category, category) : undefined
|
||||||
|
),
|
||||||
|
orderBy: products.name,
|
||||||
|
})
|
||||||
|
return products
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] Create /api/products/[id].get.ts endpoint
|
||||||
|
- Validate ID (Zod: UUID)
|
||||||
|
- Query product by ID
|
||||||
|
- Return 404 if not found
|
||||||
|
- Return: Product object
|
||||||
|
- See: [CLAUDE.md: API Route Example](../CLAUDE.md#api-route-example)
|
||||||
|
|
||||||
|
### UI Components
|
||||||
|
|
||||||
|
- [ ] Create ProductCard component
|
||||||
|
- File: `components/Product/ProductCard.vue`
|
||||||
|
- Props: product (object)
|
||||||
|
- Display: Image, name, price, short description
|
||||||
|
- Button: "In den Warenkorb" (Add to Cart)
|
||||||
|
- Styling: shadcn-nuxt Card component + Tailwind
|
||||||
|
- Mobile-first design
|
||||||
|
|
||||||
|
- [ ] Create ProductList component
|
||||||
|
- File: `components/Product/ProductList.vue`
|
||||||
|
- Props: products (array)
|
||||||
|
- Grid layout: 1 col (mobile), 2 cols (tablet), 3 cols (desktop)
|
||||||
|
- Uses: ProductCard for each product
|
||||||
|
- Empty state: "Keine Produkte verfügbar"
|
||||||
|
|
||||||
|
- [ ] Create ProductDetail component
|
||||||
|
- File: `components/Product/ProductDetail.vue`
|
||||||
|
- Props: product (object)
|
||||||
|
- Display: Large image, full description, price, stock status
|
||||||
|
- Button: "In den Warenkorb"
|
||||||
|
- Breadcrumb: Home > Produkte > [Product Name]
|
||||||
|
|
||||||
|
### Pages
|
||||||
|
|
||||||
|
- [ ] Create products index page
|
||||||
|
- File: `pages/produkte/index.vue` (German route)
|
||||||
|
- Fetches products from API
|
||||||
|
- Uses: ProductList component
|
||||||
|
- Title: "Makerspace-Jahreskarten"
|
||||||
|
- SEO meta tags
|
||||||
|
|
||||||
|
- [ ] Create ProductDetail page
|
||||||
|
- File: `pages/produkte/[id].vue`
|
||||||
|
- Fetches product by ID from API
|
||||||
|
- Uses: ProductDetail component
|
||||||
|
- 404 page if product not found
|
||||||
|
- SEO meta tags with product data
|
||||||
|
|
||||||
|
### Asset Handling
|
||||||
|
|
||||||
|
- [ ] Add product images handling
|
||||||
|
- Create `/public/images/products/` folder
|
||||||
|
- Add placeholder image for products without image
|
||||||
|
- Document image requirements (size, format)
|
||||||
|
- Optimize images (use Nuxt Image module if needed)
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- [ ] Test product display
|
||||||
|
- Seed database with sample products (manual or seed script)
|
||||||
|
- Visit /produkte page
|
||||||
|
- Verify products display correctly
|
||||||
|
- Verify responsive design (mobile, tablet, desktop)
|
||||||
|
- Click product to view detail page
|
||||||
|
- Verify product detail displays correctly
|
||||||
|
|
||||||
|
- [ ] Optimize product queries
|
||||||
|
- Add indexes to products table if needed (active, category)
|
||||||
|
- Test query performance with 100+ products
|
||||||
|
- Add pagination if needed (future enhancement)
|
||||||
|
|
||||||
|
- [ ] Document product schema
|
||||||
|
- Document product data structure in code comments
|
||||||
|
- Document how images are stored/referenced
|
||||||
|
- Document category values (e.g., "makerspace-jahreskarte")
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] /api/products endpoint returns all active products
|
||||||
|
- [x] /api/products/[id] endpoint returns product by ID
|
||||||
|
- [x] ProductCard component displays product correctly
|
||||||
|
- [x] ProductList component shows products in grid layout
|
||||||
|
- [x] ProductDetail component shows full product info
|
||||||
|
- [x] /produkte page lists all products
|
||||||
|
- [x] /produkte/[id] page shows product detail
|
||||||
|
- [x] Images display correctly (or placeholder if missing)
|
||||||
|
- [x] Responsive design works on mobile/tablet/desktop
|
||||||
|
- [x] Product data structure is documented
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **MVP Scope:** Only Makerspace-Jahreskarten products for now
|
||||||
|
- **Images:** Can use placeholders initially, real images added later
|
||||||
|
- **Pagination:** Not needed for MVP (< 10 products expected)
|
||||||
|
- **Filters:** Category filter can be added later if needed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blockers
|
||||||
|
|
||||||
|
- None currently (can use mock products if NAV ERP sync not ready)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [docs/PRD.md: F-004](../docs/PRD.md#f-004-produktanzeige)
|
||||||
|
- [docs/ARCHITECTURE.md: Products Table](../docs/ARCHITECTURE.md#products)
|
||||||
|
- [CLAUDE.md: API Route Example](../CLAUDE.md#api-route-example)
|
||||||
156
tasks/05-cart.md
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# Phase 5: Cart (Shopping Cart)
|
||||||
|
|
||||||
|
**Status:** ⏳ Todo
|
||||||
|
**Progress:** 0/12 tasks (0%)
|
||||||
|
**Started:** -
|
||||||
|
**Completed:** -
|
||||||
|
**Assigned to:** -
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Implement shopping cart functionality: API endpoints for cart operations, cart composable, and UI components for cart display and management.
|
||||||
|
|
||||||
|
**Goal:** Users can add products to cart, update quantities, and remove items.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- ✅ Phase 2: Database must be completed (carts, cart_items tables needed)
|
||||||
|
- ✅ Phase 3: Authentication should be completed (for user-specific carts)
|
||||||
|
- ✅ Phase 4: Products must be completed (products needed in cart)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
- [ ] Create /api/cart/index.get.ts endpoint
|
||||||
|
- Get current user's cart (or session cart for guests)
|
||||||
|
- Include cart items with product details (join)
|
||||||
|
- Calculate total price
|
||||||
|
- Return: { cart, items: [{product, quantity, subtotal}], total }
|
||||||
|
|
||||||
|
- [ ] Create /api/cart/items.post.ts endpoint
|
||||||
|
- Add item to cart (body: {productId, quantity})
|
||||||
|
- Validate product exists and has stock
|
||||||
|
- Create cart if doesn't exist
|
||||||
|
- Upsert cart_item (update quantity if already exists)
|
||||||
|
- Return: Updated cart
|
||||||
|
|
||||||
|
- [ ] Create /api/cart/items/[id].patch.ts endpoint
|
||||||
|
- Update cart item quantity (body: {quantity})
|
||||||
|
- Validate quantity > 0
|
||||||
|
- Validate stock availability
|
||||||
|
- Return: Updated cart item
|
||||||
|
|
||||||
|
- [ ] Create /api/cart/items/[id].delete.ts endpoint
|
||||||
|
- Remove item from cart
|
||||||
|
- Delete cart_item record
|
||||||
|
- Return: 204 No Content
|
||||||
|
|
||||||
|
### Composables
|
||||||
|
|
||||||
|
- [ ] Create useCart composable
|
||||||
|
- File: `composables/useCart.ts`
|
||||||
|
- State: cart (ref), items (computed), total (computed), itemCount (computed)
|
||||||
|
- Functions:
|
||||||
|
- `fetchCart()` - Load cart from API
|
||||||
|
- `addItem(productId, quantity)` - Add to cart
|
||||||
|
- `updateItem(itemId, quantity)` - Update quantity
|
||||||
|
- `removeItem(itemId)` - Remove from cart
|
||||||
|
- `clearCart()` - Empty cart
|
||||||
|
- Auto-fetch on mount
|
||||||
|
- See similar pattern: [CLAUDE.md: useAuth](../CLAUDE.md#oauth2-login-flow-pattern)
|
||||||
|
|
||||||
|
### UI Components
|
||||||
|
|
||||||
|
- [ ] Create CartItem component
|
||||||
|
- File: `components/Cart/CartItem.vue`
|
||||||
|
- Props: item (object with product, quantity, subtotal)
|
||||||
|
- Display: Product image, name, price, quantity input, subtotal
|
||||||
|
- Actions: Update quantity, Remove button
|
||||||
|
- Emits: @update, @remove
|
||||||
|
|
||||||
|
- [ ] Create CartSummary component
|
||||||
|
- File: `components/Cart/CartSummary.vue`
|
||||||
|
- Props: items (array), total (number)
|
||||||
|
- Display: Items count, subtotal, VAT, total
|
||||||
|
- Button: "Zur Kasse" (to checkout)
|
||||||
|
- Styling: shadcn-nuxt Card
|
||||||
|
|
||||||
|
### Pages
|
||||||
|
|
||||||
|
- [ ] Create cart page
|
||||||
|
- File: `pages/warenkorb.vue` (German route)
|
||||||
|
- Uses: useCart composable
|
||||||
|
- Shows: List of CartItem components + CartSummary
|
||||||
|
- Empty state: "Ihr Warenkorb ist leer" with link to /produkte
|
||||||
|
- Loading state while fetching
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- [ ] Test cart operations
|
||||||
|
- Add product to cart from product page
|
||||||
|
- Verify cart count updates (header badge)
|
||||||
|
- Visit /warenkorb page
|
||||||
|
- Update quantity via input
|
||||||
|
- Remove item via button
|
||||||
|
- Verify total updates correctly
|
||||||
|
|
||||||
|
- [ ] Add cart persistence
|
||||||
|
- For logged-in users: cart stored in DB (user_id)
|
||||||
|
- For guests: cart stored in DB (session_id)
|
||||||
|
- Test cart persists across page reloads
|
||||||
|
- Test cart merges when guest logs in (optional, can defer)
|
||||||
|
|
||||||
|
- [ ] Optimize cart queries
|
||||||
|
- Ensure product details are fetched efficiently (join, not N+1)
|
||||||
|
- Test with 10+ items in cart
|
||||||
|
- Add indexes if needed
|
||||||
|
|
||||||
|
- [ ] Document cart logic
|
||||||
|
- Document cart/session relationship
|
||||||
|
- Document cart item uniqueness (cart_id + product_id)
|
||||||
|
- Document cart cleanup strategy (old carts)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] All 4 cart API endpoints work correctly
|
||||||
|
- [x] useCart composable manages cart state
|
||||||
|
- [x] CartItem component displays and allows editing
|
||||||
|
- [x] CartSummary component shows total correctly
|
||||||
|
- [x] /warenkorb page shows cart with all items
|
||||||
|
- [x] Can add products to cart from product pages
|
||||||
|
- [x] Can update item quantities in cart
|
||||||
|
- [x] Can remove items from cart
|
||||||
|
- [x] Cart total calculates correctly
|
||||||
|
- [x] Cart persists across page reloads
|
||||||
|
- [x] Empty cart shows helpful message
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Guest Carts:** Use session_id for guest carts (cookie-based)
|
||||||
|
- **Cart Merge:** When guest logs in, merge guest cart with user cart (optional for MVP)
|
||||||
|
- **Stock Validation:** Ensure quantity doesn't exceed stock when adding/updating
|
||||||
|
- **VAT:** 7% VAT for annual passes (hardcoded for MVP)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blockers
|
||||||
|
|
||||||
|
- None currently
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [docs/PRD.md: F-005](../docs/PRD.md#f-005-warenkorb)
|
||||||
|
- [docs/ARCHITECTURE.md: Carts Tables](../docs/ARCHITECTURE.md#carts)
|
||||||
171
tasks/06-checkout.md
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
# Phase 6: Checkout (Forms & Flow)
|
||||||
|
|
||||||
|
**Status:** ⏳ Todo
|
||||||
|
**Progress:** 0/15 tasks (0%)
|
||||||
|
**Started:** -
|
||||||
|
**Completed:** -
|
||||||
|
**Assigned to:** -
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Implement checkout flow: billing address form, validation, address pre-fill from user profile, save address to profile option.
|
||||||
|
|
||||||
|
**Goal:** Users can enter billing information and proceed to payment.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- ✅ Phase 2: Database (users table with address fields)
|
||||||
|
- ✅ Phase 3: Authentication (user session needed)
|
||||||
|
- ✅ Phase 5: Cart (checkout requires items in cart)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
### Schema & Validation
|
||||||
|
|
||||||
|
- [ ] Create checkout schema (Zod)
|
||||||
|
- File: `server/utils/schemas/checkout.ts`
|
||||||
|
- Fields: salutation, firstName, lastName, dateOfBirth, street, postCode, city, countryCode
|
||||||
|
- Validation rules: required fields, date format, postal code format
|
||||||
|
- Export: `checkoutSchema`, `CheckoutData` type
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
- [ ] Create /api/checkout/validate.post.ts endpoint
|
||||||
|
- Validates checkout data (Zod)
|
||||||
|
- Checks if user is logged in
|
||||||
|
- Checks if cart has items
|
||||||
|
- Returns: validation result or errors
|
||||||
|
|
||||||
|
### UI Components
|
||||||
|
|
||||||
|
- [ ] Create CheckoutForm component
|
||||||
|
- File: `components/Checkout/CheckoutForm.vue`
|
||||||
|
- Uses: VeeValidate + Zod schema
|
||||||
|
- Fields: All billing address fields
|
||||||
|
- Checkbox: "Adresse für zukünftige Bestellungen speichern"
|
||||||
|
- Pre-checked if user has no saved address
|
||||||
|
- Button: "Weiter zur Zahlung"
|
||||||
|
- See: [CLAUDE.md: Checkout Pattern](../CLAUDE.md#checkout-with-saved-address-pattern)
|
||||||
|
|
||||||
|
- [ ] Create AddressForm component (reusable)
|
||||||
|
- File: `components/Checkout/AddressForm.vue`
|
||||||
|
- Props: modelValue (address object), errors
|
||||||
|
- Emits: @update:modelValue
|
||||||
|
- Fields: Salutation dropdown, Name fields, Address fields
|
||||||
|
- Can be reused in profile settings later
|
||||||
|
|
||||||
|
### Core Functionality
|
||||||
|
|
||||||
|
- [ ] Implement address pre-fill from user profile
|
||||||
|
- In CheckoutForm: fetch user data from useAuth
|
||||||
|
- If user has saved address (user.street exists): pre-fill all fields
|
||||||
|
- If no saved address: show empty form
|
||||||
|
|
||||||
|
- [ ] Implement save address to profile
|
||||||
|
- After successful checkout: if checkbox checked, save address to user record
|
||||||
|
- Update users table: salutation, dateOfBirth, street, postCode, city, countryCode
|
||||||
|
- API endpoint: PATCH /api/user/profile (or include in order creation)
|
||||||
|
|
||||||
|
### Pages
|
||||||
|
|
||||||
|
- [ ] Create checkout page
|
||||||
|
- File: `pages/kasse.vue` (German route)
|
||||||
|
- Middleware: `auth` (requires login)
|
||||||
|
- Shows: CheckoutForm component
|
||||||
|
- Shows: Order summary (cart items + total)
|
||||||
|
- Redirects to /warenkorb if cart is empty
|
||||||
|
|
||||||
|
### Validation & Error Handling
|
||||||
|
|
||||||
|
- [ ] Add form validation (VeeValidate)
|
||||||
|
- Install VeeValidate + @vee-validate/zod
|
||||||
|
- Configure VeeValidate with Zod integration
|
||||||
|
- Show field-level errors
|
||||||
|
- Show form-level errors (e.g., "Cart is empty")
|
||||||
|
|
||||||
|
- [ ] Add error handling
|
||||||
|
- Handle validation errors gracefully
|
||||||
|
- Show user-friendly error messages
|
||||||
|
- Disable submit button while submitting
|
||||||
|
- Show loading spinner during submission
|
||||||
|
|
||||||
|
- [ ] Add loading states
|
||||||
|
- Loading: fetching user profile
|
||||||
|
- Loading: validating checkout data
|
||||||
|
- Loading: processing payment (next phase)
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- [ ] Test checkout flow
|
||||||
|
- Login as user with saved address → verify pre-fill
|
||||||
|
- Login as new user → verify empty form
|
||||||
|
- Fill form and submit → verify validation
|
||||||
|
- Submit with invalid data → verify error messages
|
||||||
|
- Submit with valid data → proceed to payment (next phase)
|
||||||
|
|
||||||
|
- [ ] Test address save/load
|
||||||
|
- Submit checkout with "save address" checked
|
||||||
|
- Verify user record updated in DB
|
||||||
|
- Start new checkout → verify address pre-filled
|
||||||
|
|
||||||
|
- [ ] Test mobile checkout
|
||||||
|
- Test form on mobile device/emulator
|
||||||
|
- Verify fields are easy to tap and type
|
||||||
|
- Verify keyboard shows correct type (e.g., numeric for postal code)
|
||||||
|
|
||||||
|
- [ ] Optimize checkout UX
|
||||||
|
- Autofocus first field
|
||||||
|
- Tab order is logical
|
||||||
|
- Error messages are clear and helpful
|
||||||
|
- Button placement is accessible
|
||||||
|
|
||||||
|
- [ ] Document checkout logic
|
||||||
|
- Document address save/load flow
|
||||||
|
- Document validation rules
|
||||||
|
- Document error handling strategy
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] Checkout schema is defined with Zod
|
||||||
|
- [x] CheckoutForm component is functional and styled
|
||||||
|
- [x] AddressForm component is reusable
|
||||||
|
- [x] Address pre-fills from user profile if available
|
||||||
|
- [x] "Save address" checkbox works correctly
|
||||||
|
- [x] /kasse page is protected (requires auth)
|
||||||
|
- [x] Form validation works (VeeValidate + Zod)
|
||||||
|
- [x] Field-level and form-level errors display correctly
|
||||||
|
- [x] Loading states show during async operations
|
||||||
|
- [x] Mobile checkout UX is optimized
|
||||||
|
- [x] Address is saved to user profile after successful checkout
|
||||||
|
- [x] Checkout flow is documented
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Required Fields:** All address fields are required at checkout (even though optional in DB)
|
||||||
|
- **Date of Birth:** Required for annual pass registration
|
||||||
|
- **Salutation:** Dropdown with values: "Herr", "Frau", "Keine Angabe" (maps to HERR, FRAU, K_ANGABE in X-API)
|
||||||
|
- **Country Code:** Default to "DE", allow selection for international customers
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blockers
|
||||||
|
|
||||||
|
- None currently
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [docs/PRD.md: F-006](../docs/PRD.md#f-006-checkout-prozess)
|
||||||
|
- [docs/ARCHITECTURE.md: Users Table](../docs/ARCHITECTURE.md#users)
|
||||||
|
- [CLAUDE.md: Checkout Pattern](../CLAUDE.md#checkout-with-saved-address-pattern)
|
||||||
186
tasks/07-payment.md
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
# Phase 7: Payment (PayPal Integration)
|
||||||
|
|
||||||
|
**Status:** ⏳ Todo
|
||||||
|
**Progress:** 0/12 tasks (0%)
|
||||||
|
**Started:** -
|
||||||
|
**Completed:** -
|
||||||
|
**Assigned to:** -
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Integrate PayPal payment gateway: create order, capture payment, handle webhooks, and manage payment success/failure flows.
|
||||||
|
|
||||||
|
**Goal:** Users can pay for their orders securely via PayPal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- ✅ Phase 5: Cart (payment requires cart data)
|
||||||
|
- ✅ Phase 6: Checkout (billing info needed for order)
|
||||||
|
- ⚠️ **Required:** PayPal credentials (CLIENT_ID, CLIENT_SECRET)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
- [ ] Install PayPal SDK
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @paypal/checkout-server-sdk
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] Configure PayPal credentials in .env
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PAYPAL_CLIENT_ID=xxx
|
||||||
|
PAYPAL_CLIENT_SECRET=xxx
|
||||||
|
PAYPAL_MODE=sandbox # or 'live' for production
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] Add PayPal config to nuxt.config.ts
|
||||||
|
```typescript
|
||||||
|
runtimeConfig: {
|
||||||
|
paypal: {
|
||||||
|
clientId: process.env.PAYPAL_CLIENT_ID,
|
||||||
|
clientSecret: process.env.PAYPAL_CLIENT_SECRET,
|
||||||
|
mode: process.env.PAYPAL_MODE || 'sandbox',
|
||||||
|
},
|
||||||
|
public: {
|
||||||
|
paypalClientId: process.env.PAYPAL_CLIENT_ID, // For client-side SDK
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
- [ ] Create /api/payment/paypal/create.post.ts endpoint
|
||||||
|
- Body: { cartId, billingAddress }
|
||||||
|
- Create PayPal order via SDK
|
||||||
|
- Amount: cart total in EUR
|
||||||
|
- Description: "Makerspace-Jahreskarte"
|
||||||
|
- Return: { orderId, approvalUrl }
|
||||||
|
- See: PayPal Orders API v2
|
||||||
|
|
||||||
|
- [ ] Create /api/payment/paypal/capture.post.ts endpoint
|
||||||
|
- Body: { orderId, paypalOrderId }
|
||||||
|
- Capture PayPal payment
|
||||||
|
- If successful:
|
||||||
|
- Create order in local DB (orders, order_items)
|
||||||
|
- Queue order for X-API submission (BullMQ)
|
||||||
|
- Clear user's cart
|
||||||
|
- Return: { success: true, orderNumber }
|
||||||
|
- If failed:
|
||||||
|
- Return: { success: false, error }
|
||||||
|
|
||||||
|
- [ ] Create /api/payment/paypal/webhook.post.ts endpoint
|
||||||
|
- Verify webhook signature (PayPal SDK)
|
||||||
|
- Handle events:
|
||||||
|
- CHECKOUT.ORDER.APPROVED
|
||||||
|
- PAYMENT.CAPTURE.COMPLETED
|
||||||
|
- PAYMENT.CAPTURE.DENIED
|
||||||
|
- Update order status in DB based on event
|
||||||
|
- Log all webhook events
|
||||||
|
- Return: 200 OK (acknowledge receipt)
|
||||||
|
|
||||||
|
### Client-Side Integration
|
||||||
|
|
||||||
|
- [ ] Integrate PayPal button on checkout
|
||||||
|
- File: `components/Checkout/PayPalButton.vue`
|
||||||
|
- Load PayPal JavaScript SDK
|
||||||
|
- Render PayPal button
|
||||||
|
- On click: Call /api/payment/paypal/create
|
||||||
|
- On approve: Call /api/payment/paypal/capture
|
||||||
|
- On error: Show error message
|
||||||
|
- See: PayPal Checkout Integration guide
|
||||||
|
|
||||||
|
### Payment Flows
|
||||||
|
|
||||||
|
- [ ] Implement payment success flow
|
||||||
|
- After successful capture:
|
||||||
|
- Redirect to /bestätigung/[orderNumber] (order confirmation page)
|
||||||
|
- Show success message + order details
|
||||||
|
- Show estimated delivery/activation time
|
||||||
|
- Send confirmation email (optional for MVP)
|
||||||
|
|
||||||
|
- [ ] Implement payment error handling
|
||||||
|
- On capture failure:
|
||||||
|
- Show user-friendly error message
|
||||||
|
- Keep cart intact (don't clear)
|
||||||
|
- Log error for debugging
|
||||||
|
- Offer retry or alternative payment method (future)
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- [ ] Test PayPal sandbox
|
||||||
|
- Create sandbox account on PayPal Developer Portal
|
||||||
|
- Use sandbox credentials in .env
|
||||||
|
- Test complete flow: create → approve → capture
|
||||||
|
- Test with PayPal test cards
|
||||||
|
- Verify order is created in local DB
|
||||||
|
|
||||||
|
- [ ] Add payment status tracking
|
||||||
|
- Order status field: 'pending', 'paid', 'failed', 'completed'
|
||||||
|
- Update status after PayPal capture
|
||||||
|
- Display status in user's order history
|
||||||
|
|
||||||
|
- [ ] Document PayPal integration
|
||||||
|
- Document PayPal API flow
|
||||||
|
- Document webhook events and handling
|
||||||
|
- Document error scenarios and recovery
|
||||||
|
- Document sandbox vs production setup
|
||||||
|
|
||||||
|
- [ ] Test webhook handling
|
||||||
|
- Use PayPal webhook simulator in sandbox
|
||||||
|
- Send test events to webhook endpoint
|
||||||
|
- Verify events are processed correctly
|
||||||
|
- Verify order status updates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] PayPal SDK is installed and configured
|
||||||
|
- [x] /api/payment/paypal/create endpoint works
|
||||||
|
- [x] /api/payment/paypal/capture endpoint works
|
||||||
|
- [x] /api/payment/paypal/webhook endpoint works
|
||||||
|
- [x] PayPal button renders on checkout page
|
||||||
|
- [x] Can create PayPal order successfully
|
||||||
|
- [x] Can capture payment successfully
|
||||||
|
- [x] Order is created in DB after successful payment
|
||||||
|
- [x] Cart is cleared after successful payment
|
||||||
|
- [x] User is redirected to confirmation page
|
||||||
|
- [x] Payment errors are handled gracefully
|
||||||
|
- [x] Webhook signature verification works
|
||||||
|
- [x] Webhook events update order status
|
||||||
|
- [x] Payment flow is documented
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Sandbox Testing:** Use PayPal sandbox for development
|
||||||
|
- **Webhook URL:** Must be publicly accessible (use ngrok for local testing)
|
||||||
|
- **Currency:** EUR for all transactions
|
||||||
|
- **Amount Precision:** PayPal requires 2 decimal places (e.g., "19.99")
|
||||||
|
- **Order Number:** Generate unique order number (e.g., "EXP-2025-0001")
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blockers
|
||||||
|
|
||||||
|
- ⚠️ **PayPal Credentials:** Need sandbox credentials to test integration
|
||||||
|
- ⚠️ **Webhook Testing:** Need public URL for webhook endpoint (ngrok)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [docs/PRD.md: F-007](../docs/PRD.md#f-007-paypal-integration)
|
||||||
|
- [docs/TECH_STACK.md: PayPal](../docs/TECH_STACK.md#7-payment-gateway)
|
||||||
|
- [PayPal Orders API](https://developer.paypal.com/docs/api/orders/v2/)
|
||||||
|
- [PayPal Webhooks](https://developer.paypal.com/api/rest/webhooks/)
|
||||||
251
tasks/08-order-processing.md
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
# Phase 8: Order Processing (BullMQ + X-API)
|
||||||
|
|
||||||
|
**Status:** ⏳ Todo
|
||||||
|
**Progress:** 0/15 tasks (0%)
|
||||||
|
**Started:** -
|
||||||
|
**Completed:** -
|
||||||
|
**Assigned to:** -
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Implement asynchronous order processing: BullMQ queue for order submission to X-API, worker for processing orders, retry logic, and BullBoard dashboard for monitoring.
|
||||||
|
|
||||||
|
**Goal:** Orders are reliably submitted to X-API (NAV ERP) after successful payment.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- ✅ Phase 2: Database (orders table needed)
|
||||||
|
- ✅ Phase 7: Payment (orders created after payment)
|
||||||
|
- ✅ Docker Redis running (from docker-compose.dev.yml)
|
||||||
|
- ⚠️ **Required:** X-API credentials (USERNAME, PASSWORD, BASE_URL)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
### BullMQ Setup
|
||||||
|
|
||||||
|
- [ ] Install BullMQ + ioredis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add bullmq ioredis
|
||||||
|
pnpm add -D @bull-board/api @bull-board/nuxt
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] Configure Redis connection
|
||||||
|
- File: `server/utils/redis.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Redis } from 'ioredis'
|
||||||
|
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
export const redis = new Redis({
|
||||||
|
host: config.redisHost,
|
||||||
|
port: config.redisPort,
|
||||||
|
password: config.redisPassword,
|
||||||
|
maxRetriesPerRequest: null,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] Create order queue
|
||||||
|
- File: `server/queues/orderQueue.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Queue } from 'bullmq'
|
||||||
|
import { redis } from '../utils/redis'
|
||||||
|
|
||||||
|
export const orderQueue = new Queue('x-api-orders', {
|
||||||
|
connection: redis,
|
||||||
|
defaultJobOptions: {
|
||||||
|
attempts: 5,
|
||||||
|
backoff: { type: 'exponential', delay: 2000 },
|
||||||
|
removeOnComplete: 1000,
|
||||||
|
removeOnFail: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] Create order worker
|
||||||
|
- File: `server/workers/orderWorker.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Worker } from 'bullmq'
|
||||||
|
import { redis } from '../utils/redis'
|
||||||
|
|
||||||
|
export const orderWorker = new Worker(
|
||||||
|
'x-api-orders',
|
||||||
|
async (job) => {
|
||||||
|
const { orderId } = job.data
|
||||||
|
|
||||||
|
// 1. Fetch order from DB with items and user
|
||||||
|
const order = await db.query.orders.findFirst({
|
||||||
|
where: eq(orders.id, orderId),
|
||||||
|
with: { items: true, user: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2. Transform to X-API format
|
||||||
|
const payload = transformOrderToXAPI(order, order.user)
|
||||||
|
|
||||||
|
// 3. Submit to X-API
|
||||||
|
const result = await submitOrderToXAPI(payload)
|
||||||
|
|
||||||
|
// 4. Update order status
|
||||||
|
await db
|
||||||
|
.update(orders)
|
||||||
|
.set({ status: 'completed', xapiResponse: result })
|
||||||
|
.where(eq(orders.id, orderId))
|
||||||
|
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
{
|
||||||
|
connection: redis,
|
||||||
|
concurrency: 5,
|
||||||
|
limiter: { max: 10, duration: 1000 },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### X-API Client
|
||||||
|
|
||||||
|
- [ ] Create X-API client utility
|
||||||
|
- File: `server/utils/xapi/client.ts`
|
||||||
|
- Functions:
|
||||||
|
- `submitOrderToXAPI(payload)` - Submit order with retry logic & Basic Auth
|
||||||
|
- See: [CLAUDE.md: X-API Order Transformation](../CLAUDE.md#x-api-order-transformation-pattern)
|
||||||
|
|
||||||
|
- [ ] Implement transformOrderToXAPI function
|
||||||
|
- File: `server/utils/xapi/transformer.ts`
|
||||||
|
- Transform order from DB schema to X-API schema
|
||||||
|
- Critical transformations:
|
||||||
|
- Prices: EUR (Decimal) → Cents (Integer): `Math.round(price * 100)`
|
||||||
|
- Dates: JavaScript Date → ISO 8601 UTC: `.toISOString()`
|
||||||
|
- Line numbers: 10000, 20000, 30000... (multiples of 10000)
|
||||||
|
- Salutation: 'male' → 'HERR', 'female' → 'FRAU', other → 'K_ANGABE'
|
||||||
|
- See: [docs/ARCHITECTURE.md: X-API Format](../docs/ARCHITECTURE.md#34-x-api-order-transformation)
|
||||||
|
|
||||||
|
- [ ] Implement submitOrderToXAPI with retry
|
||||||
|
- Exponential backoff: 1s, 3s, 9s
|
||||||
|
- Max 3 retries
|
||||||
|
- HTTP Basic Auth header
|
||||||
|
- Timeout: 30 seconds
|
||||||
|
- Log all attempts
|
||||||
|
- See: [CLAUDE.md: X-API Pattern](../CLAUDE.md#x-api-order-transformation-pattern)
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
- [ ] Create /api/orders/index.post.ts endpoint
|
||||||
|
- Protected (requires auth)
|
||||||
|
- Body: { billingAddress, paymentId }
|
||||||
|
- Create order record in DB
|
||||||
|
- Create order_items from cart
|
||||||
|
- Queue order for X-API submission
|
||||||
|
- Return: { orderId, orderNumber }
|
||||||
|
|
||||||
|
- [ ] Create /api/orders/[id].get.ts endpoint
|
||||||
|
- Protected (requires auth)
|
||||||
|
- Fetch order by ID (only user's own orders)
|
||||||
|
- Include order items with product details
|
||||||
|
- Return: Order object
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- [ ] Test queue processing
|
||||||
|
- Add job to queue manually: `orderQueue.add('submit-order', { orderId: '...' })`
|
||||||
|
- Verify worker picks up job
|
||||||
|
- Verify job completes successfully
|
||||||
|
- Check BullBoard dashboard
|
||||||
|
|
||||||
|
- [ ] Test X-API submission (mock)
|
||||||
|
- Create mock X-API endpoint for testing
|
||||||
|
- Submit order via queue
|
||||||
|
- Verify transformation is correct
|
||||||
|
- Verify Basic Auth header is present
|
||||||
|
|
||||||
|
- [ ] Add error handling & logging
|
||||||
|
- Log all queue events (active, completed, failed, stalled)
|
||||||
|
- Log X-API requests/responses
|
||||||
|
- Handle X-API errors gracefully
|
||||||
|
- Update order status on failure
|
||||||
|
|
||||||
|
### Monitoring
|
||||||
|
|
||||||
|
- [ ] Setup BullBoard dashboard
|
||||||
|
- File: `server/api/admin/queues.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createBullBoard } from '@bull-board/api'
|
||||||
|
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter'
|
||||||
|
import { NuxtAdapter } from '@bull-board/nuxt'
|
||||||
|
|
||||||
|
const serverAdapter = new NuxtAdapter()
|
||||||
|
serverAdapter.setBasePath('/admin/queues')
|
||||||
|
|
||||||
|
createBullBoard({
|
||||||
|
queues: [new BullMQAdapter(orderQueue)],
|
||||||
|
serverAdapter,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default fromNodeMiddleware(serverAdapter.registerPlugin())
|
||||||
|
```
|
||||||
|
|
||||||
|
- Access at: http://localhost:3000/admin/queues
|
||||||
|
|
||||||
|
- [ ] Test retry logic
|
||||||
|
- Simulate X-API failure (wrong credentials or mock 500 error)
|
||||||
|
- Verify job retries with exponential backoff
|
||||||
|
- Verify job moves to failed after 5 attempts
|
||||||
|
- Check retry logs
|
||||||
|
|
||||||
|
- [ ] Document order processing
|
||||||
|
- Document queue flow: payment → queue → worker → X-API
|
||||||
|
- Document retry strategy
|
||||||
|
- Document error handling and recovery
|
||||||
|
- Document how to monitor queues
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] BullMQ is installed and configured
|
||||||
|
- [x] Redis connection is working
|
||||||
|
- [x] Order queue is created
|
||||||
|
- [x] Order worker processes jobs correctly
|
||||||
|
- [x] transformOrderToXAPI transforms orders correctly
|
||||||
|
- [x] submitOrderToXAPI submits with Basic Auth and retry logic
|
||||||
|
- [x] /api/orders endpoints create orders and queue jobs
|
||||||
|
- [x] Queue processing works end-to-end
|
||||||
|
- [x] X-API submissions succeed (or fail gracefully)
|
||||||
|
- [x] Error handling and logging are comprehensive
|
||||||
|
- [x] BullBoard dashboard is accessible and functional
|
||||||
|
- [x] Retry logic works as expected
|
||||||
|
- [x] Order processing is documented
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Async Processing:** Orders are queued immediately, processed in background
|
||||||
|
- **Job Timeout:** 60 seconds per job (X-API timeout + overhead)
|
||||||
|
- **Concurrency:** 5 jobs processed simultaneously
|
||||||
|
- **Rate Limit:** 10 requests/second to X-API
|
||||||
|
- **Failed Jobs:** Kept in Redis for manual inspection (not auto-deleted)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blockers
|
||||||
|
|
||||||
|
- ⚠️ **X-API Credentials:** Cannot test real submission without credentials
|
||||||
|
- **Workaround:** Use mock X-API endpoint for testing, document real integration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [docs/ARCHITECTURE.md: Queue Architecture](../docs/ARCHITECTURE.md#35-queue-architektur-bullmq--redis)
|
||||||
|
- [docs/ARCHITECTURE.md: X-API Format](../docs/ARCHITECTURE.md#34-x-api-order-transformation)
|
||||||
|
- [docs/TECH_STACK.md: BullMQ](../docs/TECH_STACK.md#52-queue-system-bullmq)
|
||||||
|
- [CLAUDE.md: X-API Pattern](../CLAUDE.md#x-api-order-transformation-pattern)
|
||||||
147
tasks/09-erp-integration.md
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
# Phase 9: ERP Integration (NAV Product Sync)
|
||||||
|
|
||||||
|
**Status:** ⏳ Todo
|
||||||
|
**Progress:** 0/10 tasks (0%)
|
||||||
|
**Started:** -
|
||||||
|
**Completed:** -
|
||||||
|
**Assigned to:** -
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Implement NAV ERP product sync API endpoint: receive product data pushed from NAV ERP, validate, and upsert into local database.
|
||||||
|
|
||||||
|
**Goal:** NAV ERP can push products to our API, keeping product catalog up-to-date.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- ✅ Phase 2: Database (products table needed)
|
||||||
|
- ⚠️ **Required:** API key for NAV ERP authentication
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
### Schema & Validation
|
||||||
|
|
||||||
|
- [ ] Create NAV ERP product schema (Zod)
|
||||||
|
- File: `server/utils/schemas/navProduct.ts`
|
||||||
|
- Fields: navProductId, name, description, price, stockQuantity, category, active
|
||||||
|
- Validation rules: required fields, price > 0, stock >= 0
|
||||||
|
- Export: `navProductSchema`, `NavProductData` type
|
||||||
|
|
||||||
|
### API Endpoint
|
||||||
|
|
||||||
|
- [ ] Create /api/erp/products.post.ts endpoint
|
||||||
|
- Body: { products: NavProductData[] } (array of products)
|
||||||
|
- Validate API key from header: `Authorization: Bearer <API_KEY>`
|
||||||
|
- Validate product data with Zod
|
||||||
|
- Upsert products in DB (insert if new, update if exists)
|
||||||
|
- Return: { success: true, upserted: count, errors: [] }
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
- [ ] Implement API key authentication
|
||||||
|
- Middleware: `server/middleware/erpAuth.ts`
|
||||||
|
- Check Authorization header
|
||||||
|
- Validate API key against NAV_ERP_API_KEY env var
|
||||||
|
- Return 401 if invalid/missing
|
||||||
|
- Only apply to /api/erp/\* routes
|
||||||
|
|
||||||
|
### Business Logic
|
||||||
|
|
||||||
|
- [ ] Implement product validation
|
||||||
|
- Validate required fields
|
||||||
|
- Validate data types and formats
|
||||||
|
- Validate price is positive
|
||||||
|
- Validate stock quantity is non-negative
|
||||||
|
- Return detailed errors for invalid products
|
||||||
|
|
||||||
|
- [ ] Implement product upsert logic
|
||||||
|
- Check if product exists by navProductId (unique key)
|
||||||
|
- If exists: Update name, description, price, stock, category, active, updated_at
|
||||||
|
- If not exists: Insert new product with all fields
|
||||||
|
- Use Drizzle's `.onConflictDoUpdate()` or manual check
|
||||||
|
- Return count of upserted products
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
- [ ] Add error handling & logging
|
||||||
|
- Log all incoming requests (timestamp, product count)
|
||||||
|
- Log validation errors with details
|
||||||
|
- Log DB errors
|
||||||
|
- Return structured errors to NAV ERP
|
||||||
|
- Example: `{ success: false, errors: [{ product: '...', message: '...' }] }`
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- [ ] Test product sync (mock data)
|
||||||
|
- Create sample NAV product data (JSON)
|
||||||
|
- POST to /api/erp/products with valid API key
|
||||||
|
- Verify products are created in DB
|
||||||
|
- POST again with updated data
|
||||||
|
- Verify products are updated in DB
|
||||||
|
- Test with invalid data → verify validation errors
|
||||||
|
|
||||||
|
- [ ] Test API key auth
|
||||||
|
- Test without Authorization header → expect 401
|
||||||
|
- Test with invalid API key → expect 401
|
||||||
|
- Test with valid API key → expect 200
|
||||||
|
|
||||||
|
- [ ] Add rate limiting
|
||||||
|
- Limit NAV ERP endpoint to prevent abuse
|
||||||
|
- Example: 100 requests / hour per API key
|
||||||
|
- Use `server/middleware/rate-limit.ts` (extend from Phase 3)
|
||||||
|
- Return 429 if limit exceeded
|
||||||
|
|
||||||
|
- [ ] Document ERP integration
|
||||||
|
- Document API endpoint spec (request/response format)
|
||||||
|
- Document authentication method (API key in header)
|
||||||
|
- Document product data schema
|
||||||
|
- Document error codes and messages
|
||||||
|
- Document rate limits
|
||||||
|
- Create example curl commands for NAV team
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] NAV product schema is defined with Zod
|
||||||
|
- [x] /api/erp/products endpoint is implemented
|
||||||
|
- [x] API key authentication works correctly
|
||||||
|
- [x] Product validation works (Zod schema)
|
||||||
|
- [x] Product upsert logic works (insert new, update existing)
|
||||||
|
- [x] Error handling returns structured errors
|
||||||
|
- [x] Logging captures all requests and errors
|
||||||
|
- [x] Can sync products successfully with mock data
|
||||||
|
- [x] API key auth prevents unauthorized access
|
||||||
|
- [x] Rate limiting protects endpoint from abuse
|
||||||
|
- [x] ERP integration is documented for NAV team
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Push Model:** NAV ERP pushes to us (we don't pull)
|
||||||
|
- **Batch Sync:** NAV can send multiple products in one request
|
||||||
|
- **Idempotent:** Repeated syncs with same data should be safe (upsert)
|
||||||
|
- **API Key Storage:** Store NAV_ERP_API_KEY in .env (dev/prod)
|
||||||
|
- **NAV Contact:** Coordinate with NAV team for API key and sync schedule
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blockers
|
||||||
|
|
||||||
|
- ⚠️ **API Key:** Need to generate/agree on API key with NAV team
|
||||||
|
- ⚠️ **NAV Schema:** Need exact product schema from NAV team (may differ from assumption)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [docs/PRD.md: F-011](../docs/PRD.md#f-011-nav-erp-push-endpunkt)
|
||||||
|
- [docs/ARCHITECTURE.md: NAV ERP Integration](../docs/ARCHITECTURE.md#33-nav-erp-product-sync)
|
||||||
|
- [CLAUDE.md: Important Constraints](../CLAUDE.md#important-constraints)
|
||||||
180
tasks/10-i18n.md
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
# Phase 10: i18n (Internationalization)
|
||||||
|
|
||||||
|
**Status:** ⏳ Todo
|
||||||
|
**Progress:** 0/8 tasks (0%)
|
||||||
|
**Started:** -
|
||||||
|
**Completed:** -
|
||||||
|
**Assigned to:** -
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Implement internationalization (i18n) with @nuxtjs/i18n: support German (default) and English, translate all UI strings, and create language switcher.
|
||||||
|
|
||||||
|
**Goal:** App is fully bilingual (German + English) with proper routing and formatting.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- ✅ Phase 1-9: Most UI components should be created by now
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
- [ ] Install @nuxtjs/i18n
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @nuxtjs/i18n
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] Configure i18n module
|
||||||
|
- File: `nuxt.config.ts`
|
||||||
|
```typescript
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
modules: ['@nuxtjs/i18n'],
|
||||||
|
i18n: {
|
||||||
|
locales: [
|
||||||
|
{ code: 'de', iso: 'de-DE', name: 'Deutsch', file: 'de-DE.json' },
|
||||||
|
{ code: 'en', iso: 'en-US', name: 'English', file: 'en-US.json' },
|
||||||
|
],
|
||||||
|
defaultLocale: 'de',
|
||||||
|
strategy: 'prefix_except_default',
|
||||||
|
langDir: 'locales',
|
||||||
|
lazy: true,
|
||||||
|
detectBrowserLanguage: {
|
||||||
|
useCookie: true,
|
||||||
|
cookieKey: 'i18n_redirected',
|
||||||
|
redirectOn: 'root',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Translation Files
|
||||||
|
|
||||||
|
- [ ] Create locale files (de-DE.json, en-US.json)
|
||||||
|
- Create: `locales/de-DE.json` (German)
|
||||||
|
- Create: `locales/en-US.json` (English)
|
||||||
|
- Structure:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"header": { "home": "Startseite", "products": "Produkte", ... },
|
||||||
|
"footer": { ... },
|
||||||
|
"buttons": { "submit": "Absenden", "cancel": "Abbrechen", ... }
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"login": { "title": "Anmelden", "email": "E-Mail", ... },
|
||||||
|
"register": { ... }
|
||||||
|
},
|
||||||
|
"products": { ... },
|
||||||
|
"cart": { ... },
|
||||||
|
"checkout": { ... },
|
||||||
|
"errors": { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] Translate all UI strings
|
||||||
|
- Go through all components and pages
|
||||||
|
- Replace hardcoded German strings with `$t('key')` or `t('key')`
|
||||||
|
- Add translations to both de-DE.json and en-US.json
|
||||||
|
- Components to translate:
|
||||||
|
- Header, Footer
|
||||||
|
- Auth forms (Login, Register)
|
||||||
|
- Product list, Product detail
|
||||||
|
- Cart, CartItem, CartSummary
|
||||||
|
- Checkout form
|
||||||
|
- PayPal button texts
|
||||||
|
- Error messages
|
||||||
|
- Success messages
|
||||||
|
- Validation messages (VeeValidate)
|
||||||
|
|
||||||
|
### UI Components
|
||||||
|
|
||||||
|
- [ ] Create language switcher component
|
||||||
|
- File: `components/Common/LanguageSwitcher.vue`
|
||||||
|
- Dropdown or toggle: 🇩🇪 Deutsch | 🇬🇧 English
|
||||||
|
- Uses: `$i18n.locale` and `$switchLocalePath()`
|
||||||
|
- Place in header
|
||||||
|
- Styling: shadcn-nuxt DropdownMenu or simple buttons
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- [ ] Test route localization
|
||||||
|
- Visit /produkte (German)
|
||||||
|
- Switch to English → should redirect to /en/products
|
||||||
|
- Test all main routes:
|
||||||
|
- / → /en/ (homepage)
|
||||||
|
- /produkte → /en/products
|
||||||
|
- /warenkorb → /en/cart
|
||||||
|
- /kasse → /en/checkout
|
||||||
|
- Verify route parameters preserved (/produkte/[id] → /en/products/[id])
|
||||||
|
|
||||||
|
- [ ] Test currency/date formatting
|
||||||
|
- Use Intl.NumberFormat for currency (EUR)
|
||||||
|
- Use Intl.DateTimeFormat for dates
|
||||||
|
- Example in component:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { locale } = useI18n()
|
||||||
|
const formatPrice = (price: number) => {
|
||||||
|
return new Intl.NumberFormat(locale.value, {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'EUR',
|
||||||
|
}).format(price)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Test: German → "19,99 €", English → "€19.99"
|
||||||
|
|
||||||
|
- [ ] Document i18n structure
|
||||||
|
- Document translation file structure
|
||||||
|
- Document how to add new translations
|
||||||
|
- Document naming conventions for translation keys
|
||||||
|
- Document how language switcher works
|
||||||
|
- Document how to format currency/dates per locale
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] @nuxtjs/i18n is installed and configured
|
||||||
|
- [x] Locale files exist for German and English
|
||||||
|
- [x] All UI strings are translated (no hardcoded German text)
|
||||||
|
- [x] Language switcher component is functional
|
||||||
|
- [x] Can switch between German and English
|
||||||
|
- [x] Routes change with language (/produkte ↔ /en/products)
|
||||||
|
- [x] Currency formatting respects locale
|
||||||
|
- [x] Date formatting respects locale
|
||||||
|
- [x] i18n structure is documented
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Default Language:** German (de)
|
||||||
|
- **Route Strategy:** prefix_except_default (German has no prefix, English has /en/)
|
||||||
|
- **SEO:** Automatic hreflang tags for SEO
|
||||||
|
- **Lazy Loading:** Translation files loaded on demand
|
||||||
|
- **Browser Detection:** Redirects to browser language on first visit (if supported)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blockers
|
||||||
|
|
||||||
|
- None currently
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [docs/PRD.md: Bilingual Support](../docs/PRD.md#bilingual-support)
|
||||||
|
- [docs/TECH_STACK.md: i18n](../docs/TECH_STACK.md#9-internationalization-i18n)
|
||||||
|
- [CLAUDE.md: Important Constraints](../CLAUDE.md#important-constraints)
|
||||||
|
- [@nuxtjs/i18n Docs](https://i18n.nuxtjs.org/)
|
||||||
256
tasks/11-testing-deployment.md
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
# Phase 11: Testing & Deployment
|
||||||
|
|
||||||
|
**Status:** ⏳ Todo
|
||||||
|
**Progress:** 0/15 tasks (0%)
|
||||||
|
**Started:** -
|
||||||
|
**Completed:** -
|
||||||
|
**Assigned to:** -
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Setup testing frameworks (Vitest, Playwright), write tests, create production Docker setup, configure CI/CD pipeline, and deploy to staging/production.
|
||||||
|
|
||||||
|
**Goal:** Fully tested MVP deployed to production with automated CI/CD.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- ✅ All previous phases should be completed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
### Unit Testing Setup
|
||||||
|
|
||||||
|
- [ ] Setup Vitest for unit tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add -D vitest @vue/test-utils happy-dom
|
||||||
|
```
|
||||||
|
|
||||||
|
- Configure vitest.config.ts
|
||||||
|
- Add test scripts to package.json: `"test": "vitest"`
|
||||||
|
- Create `tests/` folder structure
|
||||||
|
|
||||||
|
- [ ] Write tests for auth utilities
|
||||||
|
- Test: `server/utils/pkce.ts`
|
||||||
|
- Test PKCE generation (verifier, challenge)
|
||||||
|
- Test challenge is base64url encoded SHA-256
|
||||||
|
- Test: `server/utils/jwt.ts`
|
||||||
|
- Test JWT validation (mock JWKS)
|
||||||
|
- Test expired token rejection
|
||||||
|
- Test invalid issuer/audience rejection
|
||||||
|
|
||||||
|
- [ ] Write tests for API endpoints
|
||||||
|
- Test: `/api/products/index.get.ts`
|
||||||
|
- Test returns active products only
|
||||||
|
- Test filtering by category
|
||||||
|
- Test: `/api/cart/items.post.ts`
|
||||||
|
- Test add item to cart
|
||||||
|
- Test validation (invalid product ID)
|
||||||
|
- Test: `/api/orders/index.post.ts`
|
||||||
|
- Test order creation
|
||||||
|
- Test requires authentication
|
||||||
|
- Use: `@nuxt/test-utils` for API testing
|
||||||
|
|
||||||
|
### E2E Testing Setup
|
||||||
|
|
||||||
|
- [ ] Setup Playwright for E2E
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add -D @playwright/test
|
||||||
|
npx playwright install
|
||||||
|
```
|
||||||
|
|
||||||
|
- Configure playwright.config.ts
|
||||||
|
- Add e2e script: `"test:e2e": "playwright test"`
|
||||||
|
- Create `tests/e2e/` folder
|
||||||
|
|
||||||
|
- [ ] Write E2E test: user registration
|
||||||
|
- Navigate to /auth
|
||||||
|
- Click "Register" tab
|
||||||
|
- Fill registration form
|
||||||
|
- Submit form
|
||||||
|
- Verify success message (or redirect to Cidaas)
|
||||||
|
- Note: May need to mock Cidaas for E2E
|
||||||
|
|
||||||
|
- [ ] Write E2E test: complete checkout flow
|
||||||
|
- Login as user (or create test user)
|
||||||
|
- Navigate to /produkte
|
||||||
|
- Click product
|
||||||
|
- Click "In den Warenkorb"
|
||||||
|
- Navigate to /warenkorb
|
||||||
|
- Click "Zur Kasse"
|
||||||
|
- Fill checkout form
|
||||||
|
- Mock PayPal payment (or use sandbox)
|
||||||
|
- Verify order confirmation page
|
||||||
|
|
||||||
|
### Production Docker Setup
|
||||||
|
|
||||||
|
- [ ] Create Dockerfile (production)
|
||||||
|
- File: `Dockerfile`
|
||||||
|
- Multi-stage build (see docs/TECH_STACK.md#dockerfile)
|
||||||
|
- Build stage: Install deps, build Nuxt
|
||||||
|
- Production stage: Copy .output, run server
|
||||||
|
- Optimize for size (alpine, minimal layers)
|
||||||
|
|
||||||
|
- [ ] Create docker-compose.yml (production)
|
||||||
|
- File: `docker-compose.yml`
|
||||||
|
- Services: app, db, redis, worker (BullMQ worker)
|
||||||
|
- Volumes: postgres_data, redis_data
|
||||||
|
- Networks: app-network
|
||||||
|
- Health checks for all services
|
||||||
|
- Secrets for sensitive data
|
||||||
|
- See: docs/TECH_STACK.md#docker-compose
|
||||||
|
|
||||||
|
### CI/CD Pipeline
|
||||||
|
|
||||||
|
- [ ] Configure GitLab CI/CD
|
||||||
|
- File: `.gitlab-ci.yml`
|
||||||
|
- Stages: build, test, deploy-staging, deploy-production
|
||||||
|
- Build stage:
|
||||||
|
- Build Docker image
|
||||||
|
- Push to registry
|
||||||
|
- Test stage:
|
||||||
|
- Run unit tests
|
||||||
|
- Run E2E tests
|
||||||
|
- Check test coverage
|
||||||
|
- Deploy-staging stage:
|
||||||
|
- Deploy to staging automatically on main branch
|
||||||
|
- Deploy-production stage:
|
||||||
|
- Manual trigger required
|
||||||
|
- See: docs/TECH_STACK.md#gitlab-ci
|
||||||
|
|
||||||
|
- [ ] Test production build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build
|
||||||
|
pnpm preview
|
||||||
|
```
|
||||||
|
|
||||||
|
- Verify build completes without errors
|
||||||
|
- Verify production server runs
|
||||||
|
- Test production build locally with Docker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t experimenta-app:latest .
|
||||||
|
docker run -p 3000:3000 experimenta-app:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
- [ ] Setup staging environment
|
||||||
|
- Server: Hetzner VPS or VM (Proxmox)
|
||||||
|
- Domain: staging.my.experimenta.science
|
||||||
|
- SSL: Let's Encrypt (automatic)
|
||||||
|
- Reverse Proxy: Nginx or Traefik
|
||||||
|
- Docker Compose with staging config
|
||||||
|
- Environment: STAGING
|
||||||
|
|
||||||
|
- [ ] Deploy to staging
|
||||||
|
- Use GitLab CI/CD or manual deploy
|
||||||
|
- Verify deployment successful
|
||||||
|
- Run smoke tests on staging
|
||||||
|
- Test full user flow on staging
|
||||||
|
|
||||||
|
- [ ] Final QA on staging
|
||||||
|
- Test all features:
|
||||||
|
- User registration & login
|
||||||
|
- Product browsing
|
||||||
|
- Add to cart
|
||||||
|
- Checkout
|
||||||
|
- PayPal payment (sandbox)
|
||||||
|
- Order confirmation
|
||||||
|
- Order history
|
||||||
|
- Test on multiple devices/browsers
|
||||||
|
- Test language switching (DE/EN)
|
||||||
|
- Test error scenarios
|
||||||
|
|
||||||
|
- [ ] Document deployment process
|
||||||
|
- Document staging deployment steps
|
||||||
|
- Document production deployment steps
|
||||||
|
- Document rollback procedure
|
||||||
|
- Document database migration process
|
||||||
|
- Document secrets management
|
||||||
|
- Document monitoring and logging
|
||||||
|
|
||||||
|
- [ ] Deploy to production 🚀
|
||||||
|
- Server: Hetzner dedicated/VPS
|
||||||
|
- Domain: my.experimenta.science
|
||||||
|
- SSL: Let's Encrypt
|
||||||
|
- Reverse Proxy: Nginx or Traefik
|
||||||
|
- Docker Compose with production config
|
||||||
|
- Environment: PRODUCTION
|
||||||
|
- PayPal: LIVE mode
|
||||||
|
- X-API: Production endpoint
|
||||||
|
- Cidaas: Production credentials
|
||||||
|
- Database backups enabled
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] Vitest is set up and running
|
||||||
|
- [x] Unit tests cover critical utilities and endpoints
|
||||||
|
- [x] Playwright is set up and running
|
||||||
|
- [x] E2E tests cover registration and checkout flows
|
||||||
|
- [x] Production Dockerfile is optimized and working
|
||||||
|
- [x] docker-compose.yml for production is complete
|
||||||
|
- [x] GitLab CI/CD pipeline is configured
|
||||||
|
- [x] Production build works locally
|
||||||
|
- [x] Staging environment is set up and accessible
|
||||||
|
- [x] Deployed to staging successfully
|
||||||
|
- [x] QA testing on staging passes
|
||||||
|
- [x] Deployment process is documented
|
||||||
|
- [x] Deployed to production successfully 🎉
|
||||||
|
- [x] Production app is accessible and functional
|
||||||
|
- [x] Monitoring and error tracking are active
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Test Coverage Goal:** 70%+ for critical code paths
|
||||||
|
- **E2E Testing:** Focus on happy path for MVP (error scenarios in later phases)
|
||||||
|
- **Docker Production:** Use Docker Secrets for sensitive data (not env vars)
|
||||||
|
- **CI/CD:** Auto-deploy to staging, manual approval for production
|
||||||
|
- **Monitoring:** Setup Sentry or similar for error tracking (optional for MVP)
|
||||||
|
- **Backups:** Daily automated database backups with 7-day retention
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blockers
|
||||||
|
|
||||||
|
- ⚠️ **Production Servers:** Need access to production servers
|
||||||
|
- ⚠️ **Production Credentials:** Need production credentials for Cidaas, PayPal, X-API
|
||||||
|
- ⚠️ **Domain DNS:** Need to point domain to production server
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [docs/TECH_STACK.md: Testing](../docs/TECH_STACK.md#12-testing)
|
||||||
|
- [docs/TECH_STACK.md: Docker](../docs/TECH_STACK.md#11-deployment--infrastructure)
|
||||||
|
- [docs/TECH_STACK.md: CI/CD](../docs/TECH_STACK.md#cicd-mit-gitlab)
|
||||||
|
- [README.md: Development Setup](../README.md#lokale-entwicklung)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Post-Launch
|
||||||
|
|
||||||
|
After successful production launch:
|
||||||
|
|
||||||
|
- [ ] Monitor error rates (Sentry or logs)
|
||||||
|
- [ ] Monitor queue performance (BullBoard)
|
||||||
|
- [ ] Monitor PayPal transaction success rate
|
||||||
|
- [ ] Monitor X-API submission success rate
|
||||||
|
- [ ] Gather user feedback
|
||||||
|
- [ ] Plan Phase 2 features (Educator roles, etc.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**🎉 Congratulations on launching the MVP! 🎉**
|
||||||
243
tasks/README.md
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
# Task Management System
|
||||||
|
|
||||||
|
## my.experimenta.science MVP Implementation
|
||||||
|
|
||||||
|
Dieses Verzeichnis enthält die feingranulare Task-Planung für die Implementierung des MVP.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Ordnerstruktur
|
||||||
|
|
||||||
|
```
|
||||||
|
tasks/
|
||||||
|
├── README.md # Diese Datei - Übersicht & Anleitung
|
||||||
|
├── 00-PROGRESS.md # Zentrale Fortschrittsverfolgung
|
||||||
|
├── 01-foundation.md # Phase 1: Foundation (Nuxt Setup)
|
||||||
|
├── 02-database.md # Phase 2: Database Schema & Migrations
|
||||||
|
├── 03-authentication.md # Phase 3: Cidaas OAuth2 Integration
|
||||||
|
├── 04-products.md # Phase 4: Product Display
|
||||||
|
├── 05-cart.md # Phase 5: Shopping Cart
|
||||||
|
├── 06-checkout.md # Phase 6: Checkout Flow
|
||||||
|
├── 07-payment.md # Phase 7: PayPal Integration
|
||||||
|
├── 08-order-processing.md # Phase 8: Order Processing (BullMQ + X-API)
|
||||||
|
├── 09-erp-integration.md # Phase 9: NAV ERP Product Sync
|
||||||
|
├── 10-i18n.md # Phase 10: Internationalization
|
||||||
|
└── 11-testing-deployment.md # Phase 11: Testing & Deployment
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Zweck
|
||||||
|
|
||||||
|
Dieses System ermöglicht:
|
||||||
|
|
||||||
|
✅ **Strukturierte Entwicklung:** Klare Aufteilung in logische Phasen
|
||||||
|
✅ **Progress Tracking:** Nachvollziehbarer Fortschritt pro Phase
|
||||||
|
✅ **Agent-freundlich:** Claude Code Agents können Tasks autonom abarbeiten
|
||||||
|
✅ **Resume-fähig:** Einfaches Fortsetzen nach Unterbrechung
|
||||||
|
✅ **Transparenz:** Blockers & Decisions werden dokumentiert
|
||||||
|
✅ **Dependencies:** Klare Abhängigkeiten zwischen Phasen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Status-Definitionen
|
||||||
|
|
||||||
|
| Status | Symbol | Bedeutung |
|
||||||
|
| --------------- | ------ | -------------------------------------- |
|
||||||
|
| **Todo** | ⏳ | Noch nicht begonnen |
|
||||||
|
| **In Progress** | 🔄 | Aktuell in Arbeit |
|
||||||
|
| **Done** | ✅ | Abgeschlossen & getestet |
|
||||||
|
| **Blocked** | 🚫 | Blockiert, wartet auf externes Input |
|
||||||
|
| **Skipped** | ⏭️ | Übersprungen (optional/nicht relevant) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Workflow für Agents
|
||||||
|
|
||||||
|
### 1. Start einer Arbeitssession
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
1. Öffne `00-PROGRESS.md`
|
||||||
|
2. Identifiziere nächste Phase mit Status "⏳ Todo" oder "🔄 In Progress"
|
||||||
|
3. Öffne die entsprechende Phase-Datei (z.B. `03-authentication.md`)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Während der Implementierung
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
1. Arbeite Tasks sequenziell ab (von oben nach unten)
|
||||||
|
2. Markiere Tasks als erledigt: `- [ ]` → `- [x]`
|
||||||
|
3. Dokumentiere wichtige Entscheidungen im Notes-Bereich
|
||||||
|
4. Bei Blocker: Status auf 🚫, Grund dokumentieren
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Fortschritt aktualisieren
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
1. Nach jedem abgeschlossenen Task:
|
||||||
|
- Aktualisiere Progress in Phase-Datei: "3/15 tasks (20%)"
|
||||||
|
|
||||||
|
2. Nach Abschluss einer Phase:
|
||||||
|
- Status auf ✅ Done setzen
|
||||||
|
- `00-PROGRESS.md` aktualisieren
|
||||||
|
- Nächste Phase identifizieren
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Bei Unterbrechung
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
1. Aktuellen Task-Status in Phase-Datei speichern
|
||||||
|
2. In `00-PROGRESS.md` unter "Current Work" dokumentieren:
|
||||||
|
- Welche Phase
|
||||||
|
- Welcher Task
|
||||||
|
- Was als nächstes zu tun ist
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Phase-Datei Format
|
||||||
|
|
||||||
|
Jede Phase-Datei folgt diesem Template:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Phase X: [Name]
|
||||||
|
|
||||||
|
**Status:** ⏳ Todo | 🔄 In Progress | ✅ Done | 🚫 Blocked
|
||||||
|
**Progress:** 0/15 tasks (0%)
|
||||||
|
**Started:** -
|
||||||
|
**Completed:** -
|
||||||
|
**Assigned to:** -
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
[Beschreibung was in dieser Phase erreicht werden soll]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- ✅ Phase Y: [Name] must be completed first
|
||||||
|
- ⏳ Phase Z: [Name] (optional, can run parallel)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
- [ ] Task 1
|
||||||
|
- [ ] Task 2
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
|
||||||
|
- [ ] Task 3
|
||||||
|
- [ ] Task 4
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- [ ] Task 5
|
||||||
|
- [ ] Task 6
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] Criterion 1
|
||||||
|
- [ ] Criterion 2
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Important decision: ...
|
||||||
|
- Issue encountered: ...
|
||||||
|
- Resource link: ...
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blockers
|
||||||
|
|
||||||
|
- None currently
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [PRD Section X](../docs/PRD.md#section)
|
||||||
|
- [Architecture Section Y](../docs/ARCHITECTURE.md#section)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎓 Best Practices
|
||||||
|
|
||||||
|
### Für Agents
|
||||||
|
|
||||||
|
1. **Lies zuerst die Phase-Übersicht:** Verstehe das Ziel, bevor du startest
|
||||||
|
2. **Prüfe Dependencies:** Sind alle abhängigen Phasen abgeschlossen?
|
||||||
|
3. **Arbeite sequenziell:** Tasks sind nach Abhängigkeit sortiert
|
||||||
|
4. **Teste nach jedem Task:** Nicht alle Tasks am Ende testen
|
||||||
|
5. **Dokumentiere Blocker:** Wenn stuck, dokumentiere warum
|
||||||
|
6. **Update Progress häufig:** Nach jedem Task, nicht nur am Ende
|
||||||
|
|
||||||
|
### Für Entwickler
|
||||||
|
|
||||||
|
1. **Review 00-PROGRESS.md täglich:** Übersicht behalten
|
||||||
|
2. **Nutze Git Commits pro Task:** Ermöglicht einfaches Rollback
|
||||||
|
3. **Dokumentiere Abweichungen:** Wenn von Plan abgewichen wird
|
||||||
|
4. **Update Acceptance Criteria:** Falls sich Requirements ändern
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Phase-Übersicht
|
||||||
|
|
||||||
|
| # | Phase | Schwerpunkt | Geschätzte Tasks |
|
||||||
|
| --- | -------------------- | ----------------------------------- | ---------------- |
|
||||||
|
| 01 | Foundation | Nuxt 4 Setup, shadcn-nuxt, Tailwind | ~10 |
|
||||||
|
| 02 | Database | Drizzle Schema, Migrations | ~12 |
|
||||||
|
| 03 | Authentication | Cidaas OAuth2/OIDC | ~18 |
|
||||||
|
| 04 | Products | Product Display & List | ~10 |
|
||||||
|
| 05 | Cart | Shopping Cart Logic | ~12 |
|
||||||
|
| 06 | Checkout | Checkout Flow & Forms | ~15 |
|
||||||
|
| 07 | Payment | PayPal Integration | ~12 |
|
||||||
|
| 08 | Order Processing | BullMQ + X-API Submission | ~15 |
|
||||||
|
| 09 | ERP Integration | NAV ERP Product Sync API | ~10 |
|
||||||
|
| 10 | i18n | Internationalization DE/EN | ~8 |
|
||||||
|
| 11 | Testing & Deployment | E2E Tests, Docker Production | ~15 |
|
||||||
|
|
||||||
|
**Total:** ~137 granulare Tasks
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Getting Started
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Lies die zentrale Progress-Datei
|
||||||
|
cat tasks/00-PROGRESS.md
|
||||||
|
|
||||||
|
# 2. Identifiziere nächste Phase
|
||||||
|
# z.B. Phase 1: Foundation
|
||||||
|
|
||||||
|
# 3. Öffne Phase-Datei
|
||||||
|
cat tasks/01-foundation.md
|
||||||
|
|
||||||
|
# 4. Starte Implementierung
|
||||||
|
# Arbeite Tasks von oben nach unten ab
|
||||||
|
|
||||||
|
# 5. Update Progress nach jedem Task
|
||||||
|
# Markiere Task als done, update Progress-Zeile
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Bei Fragen
|
||||||
|
|
||||||
|
- Schaue in die relevante Dokumentation: `docs/PRD.md`, `docs/ARCHITECTURE.md`, `docs/TECH_STACK.md`
|
||||||
|
- Prüfe `CLAUDE.md` für Code-Patterns
|
||||||
|
- Bei Blocker: Dokumentiere in Phase-Datei + `00-PROGRESS.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Happy Coding! 🎉**
|
||||||
18
tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
// https://nuxt.com/docs/guide/concepts/typescript
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.server.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.shared.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.node.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||