style: auto-format codebase with Prettier

Applied formatting to 1487+ files using pnpm format:write
  - TypeScript/JavaScript files
  - Svelte components
  - Astro pages
  - JSON configs
  - Markdown docs

  13 files still need manual review (Astro JSX comments)
This commit is contained in:
Wuesteon 2025-11-27 18:33:16 +01:00
parent 0241f5554c
commit d36b321d9d
3952 changed files with 661498 additions and 739751 deletions

View file

@ -1 +1 @@
{}
{}

View file

@ -1,10 +1,10 @@
{
"startTime": 1764085339984,
"sessionId": "session-1764085339984",
"lastActivity": 1764085339984,
"startTime": 1764263919114,
"sessionId": "session-1764263919114",
"lastActivity": 1764263919114,
"sessionDuration": 0,
"totalTasks": 2,
"successfulTasks": 2,
"totalTasks": 1,
"successfulTasks": 1,
"failedTasks": 0,
"totalAgents": 0,
"activeAgents": 0,
@ -84,4 +84,4 @@
"cacheHits": 0,
"cacheMisses": 0
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,10 @@
[
{
"id": "cmd-hive-mind-1764085340098",
"type": "hive-mind",
"id": "cmd-swarm-1764263919220",
"type": "swarm",
"success": true,
"duration": 42.19916699999999,
"timestamp": 1764085340140,
"duration": 5.217375000000004,
"timestamp": 1764263919226,
"metadata": {}
}
]
]

View file

@ -106,6 +106,7 @@ manacore-monorepo/
## Phase 1: Preparation (Pre-Migration)
### 1.1 Create backup branch
```bash
git checkout -b backup/pre-restructure
git push origin backup/pre-restructure
@ -114,6 +115,7 @@ git checkout -b feat/monorepo-restructure
```
### 1.2 Create target directories
```bash
mkdir -p apps
mkdir -p services
@ -125,17 +127,17 @@ mkdir -p services
### 2.1 Move projects to apps/ folder
| Source | Destination |
|--------|-------------|
| `chat/` | `apps/chat/` |
| Source | Destination |
| ----------------- | ---------------------- |
| `chat/` | `apps/chat/` |
| `maerchenzauber/` | `apps/maerchenzauber/` |
| `manadeck/` | `apps/manadeck/` |
| `memoro/` | `apps/memoro/` |
| `picture/` | `apps/picture/` |
| `nutriphi/` | `apps/nutriphi/` |
| `uload/` | `apps/uload/` |
| `news/` | `apps/news/` |
| `manacore/` | `apps/manacore/` |
| `manadeck/` | `apps/manadeck/` |
| `memoro/` | `apps/memoro/` |
| `picture/` | `apps/picture/` |
| `nutriphi/` | `apps/nutriphi/` |
| `uload/` | `apps/uload/` |
| `news/` | `apps/news/` |
| `manacore/` | `apps/manacore/` |
```bash
# Move all products to apps/
@ -154,9 +156,9 @@ git mv manacore apps/manacore
Projects with backends at root level need restructuring:
| Source | Destination |
|--------|-------------|
| `apps/chat/backend/` | `apps/chat/apps/backend/` |
| Source | Destination |
| ------------------------ | ----------------------------- |
| `apps/chat/backend/` | `apps/chat/apps/backend/` |
| `apps/manadeck/backend/` | `apps/manadeck/apps/backend/` |
| `apps/nutriphi/backend/` | `apps/nutriphi/apps/backend/` |
@ -175,6 +177,7 @@ git mv apps/nutriphi/backend apps/nutriphi/apps/backend
```
### 2.3 Move mana-core-auth to services/
```bash
git mv mana-core-auth services/mana-core-auth
```
@ -186,6 +189,7 @@ git mv mana-core-auth services/mana-core-auth
### 3.1 Update pnpm-workspace.yaml
**New content:**
```yaml
packages:
# Product applications
@ -207,85 +211,86 @@ No changes needed - turbo.json uses task definitions, not paths.
### 3.3 Update root package.json scripts
**Replace scripts section:**
```json
{
"scripts": {
"dev": "turbo run dev",
"build": "turbo run build",
"test": "turbo run test",
"lint": "turbo run lint",
"type-check": "turbo run type-check",
"clean": "turbo run clean",
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md,svelte,astro}\"",
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md,svelte,astro}\"",
"scripts": {
"dev": "turbo run dev",
"build": "turbo run build",
"test": "turbo run test",
"lint": "turbo run lint",
"type-check": "turbo run type-check",
"clean": "turbo run clean",
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md,svelte,astro}\"",
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md,svelte,astro}\"",
"maerchenzauber:dev": "turbo run dev --filter=@maerchenzauber/*...",
"manacore:dev": "turbo run dev --filter=@manacore/*...",
"manadeck:dev": "turbo run dev --filter=@manadeck/*...",
"memoro:dev": "turbo run dev --filter=@memoro/*...",
"picture:dev": "turbo run dev --filter=@picture/*...",
"uload:dev": "turbo run dev --filter=@uload/*...",
"chat:dev": "turbo run dev --filter=@chat/*...",
"nutriphi:dev": "turbo run dev --filter=@nutriphi/*...",
"news:dev": "turbo run dev --filter=@news/*...",
"maerchenzauber:dev": "turbo run dev --filter=@maerchenzauber/*...",
"manacore:dev": "turbo run dev --filter=@manacore/*...",
"manadeck:dev": "turbo run dev --filter=@manadeck/*...",
"memoro:dev": "turbo run dev --filter=@memoro/*...",
"picture:dev": "turbo run dev --filter=@picture/*...",
"uload:dev": "turbo run dev --filter=@uload/*...",
"chat:dev": "turbo run dev --filter=@chat/*...",
"nutriphi:dev": "turbo run dev --filter=@nutriphi/*...",
"news:dev": "turbo run dev --filter=@news/*...",
"dev:maerchenzauber:web": "pnpm --filter @maerchenzauber/web dev",
"dev:maerchenzauber:landing": "pnpm --filter @maerchenzauber/landing dev",
"dev:maerchenzauber:backend": "pnpm --filter @maerchenzauber/backend dev",
"dev:maerchenzauber:mobile": "pnpm --filter @maerchenzauber/mobile dev",
"dev:maerchenzauber:app": "turbo run dev --filter=@maerchenzauber/web --filter=@maerchenzauber/backend",
"dev:maerchenzauber:web": "pnpm --filter @maerchenzauber/web dev",
"dev:maerchenzauber:landing": "pnpm --filter @maerchenzauber/landing dev",
"dev:maerchenzauber:backend": "pnpm --filter @maerchenzauber/backend dev",
"dev:maerchenzauber:mobile": "pnpm --filter @maerchenzauber/mobile dev",
"dev:maerchenzauber:app": "turbo run dev --filter=@maerchenzauber/web --filter=@maerchenzauber/backend",
"dev:manacore:web": "pnpm --filter @manacore/web dev",
"dev:manacore:landing": "pnpm --filter @manacore/landing dev",
"dev:manacore:mobile": "pnpm --filter @manacore/mobile dev",
"dev:manacore:web": "pnpm --filter @manacore/web dev",
"dev:manacore:landing": "pnpm --filter @manacore/landing dev",
"dev:manacore:mobile": "pnpm --filter @manacore/mobile dev",
"dev:manadeck:web": "pnpm --filter @manadeck/web dev",
"dev:manadeck:landing": "pnpm --filter @manadeck/landing dev",
"dev:manadeck:backend": "pnpm --filter @manadeck/backend dev",
"dev:manadeck:mobile": "pnpm --filter @manadeck/mobile dev",
"dev:manadeck:app": "turbo run dev --filter=@manadeck/web --filter=@manadeck/backend",
"dev:manadeck:web": "pnpm --filter @manadeck/web dev",
"dev:manadeck:landing": "pnpm --filter @manadeck/landing dev",
"dev:manadeck:backend": "pnpm --filter @manadeck/backend dev",
"dev:manadeck:mobile": "pnpm --filter @manadeck/mobile dev",
"dev:manadeck:app": "turbo run dev --filter=@manadeck/web --filter=@manadeck/backend",
"dev:memoro:web": "pnpm --filter @memoro/web dev",
"dev:memoro:landing": "pnpm --filter @memoro/landing dev",
"dev:memoro:mobile": "pnpm --filter @memoro/mobile dev",
"dev:memoro:web": "pnpm --filter @memoro/web dev",
"dev:memoro:landing": "pnpm --filter @memoro/landing dev",
"dev:memoro:mobile": "pnpm --filter @memoro/mobile dev",
"dev:picture:web": "pnpm --filter @picture/web dev",
"dev:picture:landing": "pnpm --filter @picture/landing dev",
"dev:picture:mobile": "pnpm --filter @picture/mobile dev",
"dev:picture:web": "pnpm --filter @picture/web dev",
"dev:picture:landing": "pnpm --filter @picture/landing dev",
"dev:picture:mobile": "pnpm --filter @picture/mobile dev",
"dev:uload:web": "pnpm --filter @uload/web dev",
"dev:uload:web": "pnpm --filter @uload/web dev",
"dev:chat:mobile": "pnpm --filter @chat/mobile dev",
"dev:chat:web": "pnpm --filter @chat/web dev",
"dev:chat:landing": "pnpm --filter @chat/landing dev",
"dev:chat:backend": "pnpm --filter @chat/backend start:dev",
"dev:chat:app": "turbo run dev --filter=@chat/web --filter=@chat/backend",
"dev:chat:mobile": "pnpm --filter @chat/mobile dev",
"dev:chat:web": "pnpm --filter @chat/web dev",
"dev:chat:landing": "pnpm --filter @chat/landing dev",
"dev:chat:backend": "pnpm --filter @chat/backend start:dev",
"dev:chat:app": "turbo run dev --filter=@chat/web --filter=@chat/backend",
"dev:nutriphi:mobile": "pnpm --filter @nutriphi/mobile dev",
"dev:nutriphi:web": "pnpm --filter @nutriphi/web dev",
"dev:nutriphi:landing": "pnpm --filter @nutriphi/landing dev",
"dev:nutriphi:backend": "pnpm --filter @nutriphi/backend start:dev",
"dev:nutriphi:app": "turbo run dev --filter=@nutriphi/web --filter=@nutriphi/backend",
"dev:nutriphi:mobile": "pnpm --filter @nutriphi/mobile dev",
"dev:nutriphi:web": "pnpm --filter @nutriphi/web dev",
"dev:nutriphi:landing": "pnpm --filter @nutriphi/landing dev",
"dev:nutriphi:backend": "pnpm --filter @nutriphi/backend start:dev",
"dev:nutriphi:app": "turbo run dev --filter=@nutriphi/web --filter=@nutriphi/backend",
"dev:news:mobile": "pnpm --filter @news/mobile dev",
"dev:news:web": "pnpm --filter @news/web dev",
"dev:news:landing": "pnpm --filter @news/landing dev",
"dev:news:api": "pnpm --filter @news/api start:dev",
"dev:news:app": "turbo run dev --filter=@news/web --filter=@news/api",
"news:db:push": "pnpm --filter @manacore/news-database db:push",
"news:db:studio": "pnpm --filter @manacore/news-database db:studio",
"dev:news:mobile": "pnpm --filter @news/mobile dev",
"dev:news:web": "pnpm --filter @news/web dev",
"dev:news:landing": "pnpm --filter @news/landing dev",
"dev:news:api": "pnpm --filter @news/api start:dev",
"dev:news:app": "turbo run dev --filter=@news/web --filter=@news/api",
"news:db:push": "pnpm --filter @manacore/news-database db:push",
"news:db:studio": "pnpm --filter @manacore/news-database db:studio",
"docker:up": "docker compose -f docker-compose.dev.yml up -d postgres redis",
"docker:up:auth": "docker compose -f docker-compose.dev.yml --profile auth up -d",
"docker:up:chat": "docker compose -f docker-compose.dev.yml --profile chat up -d",
"docker:up:all": "docker compose -f docker-compose.dev.yml --profile all up -d",
"docker:down": "docker compose -f docker-compose.dev.yml --profile all down",
"docker:logs": "docker compose -f docker-compose.dev.yml logs -f",
"docker:logs:auth": "docker compose -f docker-compose.dev.yml logs -f mana-core-auth",
"docker:logs:chat": "docker compose -f docker-compose.dev.yml logs -f chat-backend",
"docker:ps": "docker compose -f docker-compose.dev.yml ps -a",
"docker:clean": "docker compose -f docker-compose.dev.yml --profile all down -v"
}
"docker:up": "docker compose -f docker-compose.dev.yml up -d postgres redis",
"docker:up:auth": "docker compose -f docker-compose.dev.yml --profile auth up -d",
"docker:up:chat": "docker compose -f docker-compose.dev.yml --profile chat up -d",
"docker:up:all": "docker compose -f docker-compose.dev.yml --profile all up -d",
"docker:down": "docker compose -f docker-compose.dev.yml --profile all down",
"docker:logs": "docker compose -f docker-compose.dev.yml logs -f",
"docker:logs:auth": "docker compose -f docker-compose.dev.yml logs -f mana-core-auth",
"docker:logs:chat": "docker compose -f docker-compose.dev.yml logs -f chat-backend",
"docker:ps": "docker compose -f docker-compose.dev.yml ps -a",
"docker:clean": "docker compose -f docker-compose.dev.yml --profile all down -v"
}
}
```
@ -296,6 +301,7 @@ No changes needed - turbo.json uses task definitions, not paths.
### 4.1 Update docker-compose.dev.yml
**Key changes:**
- mana-core-auth Dockerfile path: `./services/mana-core-auth/Dockerfile`
- chat-backend Dockerfile path: `./apps/chat/apps/backend/Dockerfile`
@ -304,14 +310,14 @@ services:
# ... postgres and redis unchanged ...
mana-core-auth:
profiles: ["auth", "all"]
profiles: ['auth', 'all']
build:
context: .
dockerfile: ./services/mana-core-auth/Dockerfile
# ... rest unchanged ...
chat-backend:
profiles: ["chat", "all"]
profiles: ['chat', 'all']
build:
context: .
dockerfile: ./apps/chat/apps/backend/Dockerfile
@ -323,6 +329,7 @@ services:
Each Dockerfile needs path updates for COPY commands.
**services/mana-core-auth/Dockerfile:**
```dockerfile
# Update COPY paths
COPY services/mana-core-auth/package.json ./services/mana-core-auth/
@ -331,6 +338,7 @@ COPY packages/ ./packages/
```
**apps/chat/apps/backend/Dockerfile:**
```dockerfile
# Update COPY paths
COPY apps/chat/apps/backend/package.json ./apps/chat/apps/backend/
@ -350,6 +358,7 @@ Each project's CLAUDE.md needs path updates in documentation.
### 5.2 Update relative imports in code
Check for any hardcoded relative paths like:
- `../../packages/`
- `../../../shared/`
@ -358,6 +367,7 @@ These may need adjustment.
### 5.3 Update .env files
Environment files should still work but verify paths for:
- File-based configs
- Volume mounts
- Any path references
@ -367,6 +377,7 @@ Environment files should still work but verify paths for:
## Phase 6: Validation & Testing
### 6.1 Reinstall dependencies
```bash
rm -rf node_modules
rm pnpm-lock.yaml
@ -374,11 +385,13 @@ pnpm install
```
### 6.2 Verify workspace packages
```bash
pnpm ls -r --depth 0
```
### 6.3 Test turbo commands
```bash
pnpm chat:dev # Should start all chat apps
pnpm dev:chat:web # Should start just web
@ -386,6 +399,7 @@ pnpm type-check # Should check all projects
```
### 6.4 Test Docker
```bash
pnpm docker:up:all
pnpm docker:ps
@ -393,6 +407,7 @@ pnpm docker:logs
```
### 6.5 Build all projects
```bash
pnpm build
```
@ -402,6 +417,7 @@ pnpm build
## Phase 7: Commit & Merge
### 7.1 Commit changes
```bash
git add .
git commit -m "refactor: restructure monorepo with apps/ and services/ directories
@ -415,6 +431,7 @@ git commit -m "refactor: restructure monorepo with apps/ and services/ directori
```
### 7.2 Create PR
```bash
gh pr create --title "refactor: monorepo restructure" --body "..."
```
@ -424,6 +441,7 @@ gh pr create --title "refactor: monorepo restructure" --body "..."
## Rollback Plan
If issues arise:
```bash
git checkout main
git branch -D feat/monorepo-restructure
@ -455,13 +473,13 @@ git branch -D feat/monorepo-restructure
## Estimated Impact
| Item | Count |
|------|-------|
| Directories to move | 10 |
| Config files to update | 5-6 |
| Dockerfiles to update | ~6 |
| package.json scripts | ~40 (most unchanged) |
| CLAUDE.md files | ~10 |
| Item | Count |
| ---------------------- | -------------------- |
| Directories to move | 10 |
| Config files to update | 5-6 |
| Dockerfiles to update | ~6 |
| package.json scripts | ~40 (most unchanged) |
| CLAUDE.md files | ~10 |
**Risk Level:** Medium - Many file moves but package names unchanged
**Estimated Time:** 2-4 hours with testing

File diff suppressed because it is too large Load diff

View file

@ -1,986 +0,0 @@
# 🐳 Docker Self-Hosting Deployment Guide
**Document Type:** Self-Hosting Infrastructure Guide
**Target:** Production-Ready Dockerized Deployment
**Date:** 2025-11-25
**Status:** Ready for Implementation
---
## 📊 Executive Summary
This guide provides complete Docker-based self-hosting instructions for the Mana Core authentication and credit system. By self-hosting, you save **€40-55/month** compared to managed cloud services while maintaining full control over your infrastructure.
### Cost Comparison
| Component | Managed Cloud | Self-Hosted Docker | Savings |
|-----------|---------------|-------------------|---------|
| PostgreSQL | Supabase Pro: €25/mo | VPS: €0 | €25 |
| Auth Service | Cloud Run: €20-50/mo | VPS: €0 | €20-50 |
| Redis | Managed: €10-20/mo | VPS: €0 | €10-20 |
| **VPS Hosting** | €0 | Hetzner: €15-40/mo | -€15-40 |
| Stripe | 2.9% + €0.30/txn | 2.9% + €0.30/txn | €0 |
| **Total** | **€55-95/mo** | **€15-40/mo** | **€40-55/mo** |
---
## 🏗️ Architecture Overview
### Containerized Services
```
┌─────────────────────────────────────────────────────────┐
│ DOCKER HOST (VPS) │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Traefik (Reverse Proxy) │ │
│ │ - SSL/TLS (Let's Encrypt) │ │
│ │ - Load Balancing │ │
│ │ - Rate Limiting │ │
│ └────────────┬───────────────────────────────────────┘ │
│ │ │
│ ┌────────────┴──────────────┬────────────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────────┐ ┌──────────────┐│
│ │ │ │ │ │ ││
│ │ Mana-Core │ │ App Services │ │ PostgreSQL ││
│ │ Auth │ │ - Memoro │ │ 16-alpine ││
│ │ Service │ │ - Chat │ │ ││
│ │ (NestJS) │ │ - Picture │ │ + PgBouncer ││
│ │ │ │ │ │ ││
│ └──────┬───────┘ └──────────┬────────┘ └──────┬───────┘│
│ │ │ │ │
│ └─────────────────────┴───────────────────┘ │
│ │ │
│ ┌─────────▼──────────┐ │
│ │ │ │
│ │ Redis 7-alpine │ │
│ │ (Cache + Queue) │ │
│ │ │ │
│ └─────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────┘
```
---
## 📦 Complete Docker Compose Setup
### 1. Project Structure
```
manacore-monorepo/
├── docker-compose.yml # Main orchestration
├── docker-compose.prod.yml # Production overrides
├── .env.docker # Docker environment variables
├── packages/
│ └── mana-core-auth/
│ ├── Dockerfile # Auth service image
│ └── .dockerignore
├── traefik/
│ ├── traefik.yml # Traefik config
│ ├── dynamic.yml # Dynamic routing rules
│ └── acme.json # Let's Encrypt certs
├── postgres/
│ ├── init/
│ │ └── 001_initial_schema.sql # Database initialization
│ └── backup/ # Backup scripts
└── scripts/
├── deploy.sh # Deployment script
├── backup.sh # Backup automation
└── health-check.sh # Health monitoring
```
### 2. Main docker-compose.yml
```yaml
version: '3.8'
services:
# Reverse Proxy & Load Balancer
traefik:
image: traefik:v2.10
container_name: traefik
restart: unless-stopped
command:
- "--api.dashboard=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
- "--certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}"
- "--certificatesresolvers.letsencrypt.acme.storage=/acme.json"
- "--log.level=INFO"
- "--accesslog=true"
ports:
- "80:80"
- "443:443"
- "8080:8080" # Traefik dashboard
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik/acme.json:/acme.json
- ./traefik/dynamic.yml:/dynamic.yml:ro
labels:
- "traefik.enable=true"
# Dashboard
- "traefik.http.routers.dashboard.rule=Host(`traefik.${DOMAIN}`)"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.middlewares=auth"
- "traefik.http.middlewares.auth.basicauth.users=${TRAEFIK_BASIC_AUTH}"
networks:
- mana-network
# PostgreSQL Database
postgres:
image: postgres:16-alpine
container_name: postgres
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:-manacore}
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=en_US.UTF-8"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./postgres/init:/docker-entrypoint-initdb.d:ro
- ./postgres/backup:/backup
ports:
- "127.0.0.1:5432:5432" # Only localhost access
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- mana-network
# Connection Pooler (PgBouncer)
pgbouncer:
image: pgbouncer/pgbouncer:latest
container_name: pgbouncer
restart: unless-stopped
environment:
DATABASES_HOST: postgres
DATABASES_PORT: 5432
DATABASES_USER: ${POSTGRES_USER:-postgres}
DATABASES_PASSWORD: ${POSTGRES_PASSWORD}
DATABASES_DBNAME: ${POSTGRES_DB:-manacore}
PGBOUNCER_POOL_MODE: transaction
PGBOUNCER_MAX_CLIENT_CONN: 1000
PGBOUNCER_DEFAULT_POOL_SIZE: 25
PGBOUNCER_MIN_POOL_SIZE: 5
PGBOUNCER_RESERVE_POOL_SIZE: 5
depends_on:
postgres:
condition: service_healthy
networks:
- mana-network
# Redis Cache & Queue
redis:
image: redis:7-alpine
container_name: redis
restart: unless-stopped
command: >
redis-server
--appendonly yes
--appendfsync everysec
--maxmemory 512mb
--maxmemory-policy allkeys-lru
--requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
ports:
- "127.0.0.1:6379:6379" # Only localhost access
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 10s
timeout: 3s
retries: 5
networks:
- mana-network
# Mana Core Auth Service
mana-core-auth:
build:
context: ./packages/mana-core-auth
dockerfile: Dockerfile
args:
NODE_ENV: production
container_name: mana-core-auth
restart: unless-stopped
environment:
NODE_ENV: production
PORT: 3000
# Database
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@pgbouncer:6432/${POSTGRES_DB}
# Redis
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
# JWT Keys
JWT_PRIVATE_KEY: ${JWT_PRIVATE_KEY}
JWT_PUBLIC_KEY: ${JWT_PUBLIC_KEY}
JWT_ALGORITHM: RS256
JWT_ACCESS_TOKEN_EXPIRES_IN: 1h
JWT_REFRESH_TOKEN_EXPIRES_IN: 14d
# Stripe
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY}
STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET}
# Application
APP_NAME: Mana Core Auth
APP_URL: https://auth.${DOMAIN}
CORS_ORIGINS: ${CORS_ORIGINS}
# Rate Limiting
RATE_LIMIT_ENABLED: true
RATE_LIMIT_MAX_REQUESTS: 100
RATE_LIMIT_WINDOW_MS: 60000
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
pgbouncer:
condition: service_started
labels:
- "traefik.enable=true"
- "traefik.http.routers.auth.rule=Host(`auth.${DOMAIN}`)"
- "traefik.http.routers.auth.entrypoints=websecure"
- "traefik.http.routers.auth.tls.certresolver=letsencrypt"
- "traefik.http.services.auth.loadbalancer.server.port=3000"
# Rate limiting middleware
- "traefik.http.middlewares.auth-ratelimit.ratelimit.average=100"
- "traefik.http.middlewares.auth-ratelimit.ratelimit.burst=50"
- "traefik.http.routers.auth.middlewares=auth-ratelimit"
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks:
- mana-network
# Monitoring: Prometheus (optional but recommended)
prometheus:
image: prom/prometheus:latest
container_name: prometheus
restart: unless-stopped
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=30d'
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
ports:
- "127.0.0.1:9090:9090"
networks:
- mana-network
# Monitoring: Grafana (optional but recommended)
grafana:
image: grafana/grafana:latest
container_name: grafana
restart: unless-stopped
environment:
GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER:-admin}
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD}
GF_INSTALL_PLUGINS: grafana-piechart-panel
volumes:
- grafana_data:/var/lib/grafana
- ./monitoring/grafana-dashboards:/etc/grafana/provisioning/dashboards:ro
labels:
- "traefik.enable=true"
- "traefik.http.routers.grafana.rule=Host(`grafana.${DOMAIN}`)"
- "traefik.http.routers.grafana.entrypoints=websecure"
- "traefik.http.routers.grafana.tls.certresolver=letsencrypt"
- "traefik.http.services.grafana.loadbalancer.server.port=3000"
networks:
- mana-network
networks:
mana-network:
driver: bridge
volumes:
postgres_data:
driver: local
redis_data:
driver: local
prometheus_data:
driver: local
grafana_data:
driver: local
```
### 3. Auth Service Dockerfile
**Location:** `packages/mana-core-auth/Dockerfile`
```dockerfile
# Build stage
FROM node:20-alpine AS builder
# Install pnpm
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
WORKDIR /app
# Copy dependency files
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
COPY packages/mana-core-auth/package.json ./packages/mana-core-auth/
# Install dependencies
RUN pnpm install --frozen-lockfile --filter mana-core-auth...
# Copy source code
COPY packages/mana-core-auth ./packages/mana-core-auth
COPY packages/shared-* ./packages/
# Build application
WORKDIR /app/packages/mana-core-auth
RUN pnpm build
# Production stage
FROM node:20-alpine
# Install dumb-init (proper signal handling)
RUN apk add --no-cache dumb-init
# Create app user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
WORKDIR /app
# Copy built application
COPY --from=builder --chown=nodejs:nodejs /app/packages/mana-core-auth/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/packages/mana-core-auth/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/packages/mana-core-auth/package.json ./
# Switch to non-root user
USER nodejs
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
# Use dumb-init to handle signals properly
ENTRYPOINT ["dumb-init", "--"]
# Start application
CMD ["node", "dist/main.js"]
```
### 4. .dockerignore
```
node_modules
dist
.git
.github
.env
.env.*
*.log
npm-debug.log*
coverage
.DS_Store
*.md
!README.md
```
### 5. Environment Variables (.env.docker)
```env
# ============================================
# DOMAIN & SSL
# ============================================
DOMAIN=yourdomain.com
ACME_EMAIL=admin@yourdomain.com
# ============================================
# TRAEFIK DASHBOARD AUTH
# ============================================
# Generate with: htpasswd -nb admin your_password
TRAEFIK_BASIC_AUTH=admin:$$apr1$$xyz123...
# ============================================
# POSTGRESQL
# ============================================
POSTGRES_DB=manacore
POSTGRES_USER=postgres
POSTGRES_PASSWORD=<GENERATE_STRONG_PASSWORD>
# ============================================
# REDIS
# ============================================
REDIS_PASSWORD=<GENERATE_STRONG_PASSWORD>
# ============================================
# JWT KEYS (RS256)
# ============================================
# Generate with:
# ssh-keygen -t rsa -b 4096 -m PEM -f jwt.key
# openssl rsa -in jwt.key -pubout -outform PEM -out jwt.key.pub
JWT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
# ============================================
# STRIPE
# ============================================
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
# ============================================
# APPLICATION
# ============================================
CORS_ORIGINS=https://memoro.yourdomain.com,https://chat.yourdomain.com,https://picture.yourdomain.com
# ============================================
# MONITORING
# ============================================
GRAFANA_ADMIN_USER=admin
GRAFANA_ADMIN_PASSWORD=<GENERATE_STRONG_PASSWORD>
```
---
## 🚀 Deployment Instructions
### Prerequisites
1. **VPS Server:**
- **Recommended:** Hetzner CPX31 (4 vCPU, 8GB RAM, 160GB SSD) - €15.30/month
- **For larger scale:** Hetzner CPX41 (8 vCPU, 16GB RAM, 240GB SSD) - €29.70/month
- **OS:** Ubuntu 22.04 LTS
2. **Domain Name:**
- Point A records to your VPS IP:
- `auth.yourdomain.com` → VPS IP
- `grafana.yourdomain.com` → VPS IP
- `traefik.yourdomain.com` → VPS IP
3. **Docker & Docker Compose:**
```bash
# Install Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
# Install Docker Compose
sudo apt-get install docker-compose-plugin
# Verify installation
docker --version
docker compose version
```
### Step 1: Generate JWT Keys
```bash
# Generate RSA private key
ssh-keygen -t rsa -b 4096 -m PEM -f jwt.key -N ""
# Extract public key
openssl rsa -in jwt.key -pubout -outform PEM -out jwt.key.pub
# View private key (copy to .env.docker)
cat jwt.key
# View public key (copy to .env.docker)
cat jwt.key.pub
```
### Step 2: Configure Environment
```bash
# Copy example environment file
cp .env.docker.example .env.docker
# Edit with your values
nano .env.docker
# Secure the file
chmod 600 .env.docker
```
### Step 3: Initialize Database
```bash
# Create database init script
mkdir -p postgres/init
# Copy migration script
cp .hive-mind/central-auth-and-credits-design.md postgres/init/001_initial_schema.sql
# (Extract the SQL from lines 2314-2728)
# Or use direct SQL file
cat > postgres/init/001_initial_schema.sql << 'EOF'
-- Paste complete migration script here
EOF
```
### Step 4: Start Services
```bash
# Create required directories
mkdir -p traefik postgres/backup monitoring
# Create acme.json for Let's Encrypt
touch traefik/acme.json
chmod 600 traefik/acme.json
# Start all services
docker compose up -d
# View logs
docker compose logs -f
# Check service health
docker compose ps
```
### Step 5: Verify Deployment
```bash
# Check auth service health
curl https://auth.yourdomain.com/health
# Expected response:
# {"status":"ok","timestamp":"2025-11-25T..."}
# Test registration
curl -X POST https://auth.yourdomain.com/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "SecurePass123!",
"name": "Test User"
}'
```
---
## 🔧 Maintenance Operations
### Backup Database
```bash
# Create backup script
cat > scripts/backup.sh << 'EOF'
#!/bin/bash
BACKUP_DIR="/path/to/backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="manacore_backup_${TIMESTAMP}.sql.gz"
docker exec postgres pg_dump -U postgres manacore | gzip > "${BACKUP_DIR}/${BACKUP_FILE}"
# Keep only last 30 days of backups
find "${BACKUP_DIR}" -name "manacore_backup_*.sql.gz" -mtime +30 -delete
echo "Backup completed: ${BACKUP_FILE}"
EOF
chmod +x scripts/backup.sh
# Run backup manually
./scripts/backup.sh
# Schedule daily backups (cron)
crontab -e
# Add: 0 2 * * * /path/to/scripts/backup.sh >> /var/log/manacore-backup.log 2>&1
```
### Restore Database
```bash
# Stop auth service (prevent writes during restore)
docker compose stop mana-core-auth
# Restore from backup
gunzip -c /path/to/backups/manacore_backup_YYYYMMDD_HHMMSS.sql.gz | \
docker exec -i postgres psql -U postgres -d manacore
# Restart auth service
docker compose start mana-core-auth
```
### Update Services
```bash
# Pull latest images
docker compose pull
# Rebuild auth service (if code changed)
docker compose build mana-core-auth
# Zero-downtime update (with multiple replicas)
docker compose up -d --no-deps --scale mana-core-auth=2 mana-core-auth
# Remove old containers
docker compose up -d --remove-orphans
```
### View Logs
```bash
# All services
docker compose logs -f
# Specific service
docker compose logs -f mana-core-auth
# Last 100 lines
docker compose logs --tail=100 mana-core-auth
# With timestamps
docker compose logs -f --timestamps mana-core-auth
```
### Monitor Resources
```bash
# Container stats
docker stats
# Disk usage
docker system df
# Clean up unused resources
docker system prune -a --volumes
```
---
## 📊 Monitoring Setup
### 1. Prometheus Configuration
**File:** `monitoring/prometheus.yml`
```yaml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'mana-core-auth'
static_configs:
- targets: ['mana-core-auth:3000']
metrics_path: '/metrics'
- job_name: 'postgres'
static_configs:
- targets: ['postgres:5432']
- job_name: 'redis'
static_configs:
- targets: ['redis:6379']
- job_name: 'traefik'
static_configs:
- targets: ['traefik:8080']
```
### 2. Grafana Dashboards
Access Grafana at `https://grafana.yourdomain.com`
**Default credentials:** admin / (password from .env.docker)
**Recommended Dashboards:**
- PostgreSQL Dashboard: ID 9628
- Redis Dashboard: ID 11835
- Traefik Dashboard: ID 17346
- Node Exporter: ID 1860
### 3. Health Check Script
```bash
#!/bin/bash
# scripts/health-check.sh
SERVICES=("postgres" "redis" "mana-core-auth" "traefik")
FAILED=0
for SERVICE in "${SERVICES[@]}"; do
if ! docker compose ps | grep -q "${SERVICE}.*Up"; then
echo "❌ ${SERVICE} is down!"
FAILED=1
else
echo "✅ ${SERVICE} is up"
fi
done
# Check auth service health endpoint
if curl -sf https://auth.yourdomain.com/health > /dev/null; then
echo "✅ Auth service health check passed"
else
echo "❌ Auth service health check failed"
FAILED=1
fi
exit $FAILED
```
Run every 5 minutes via cron:
```bash
*/5 * * * * /path/to/scripts/health-check.sh >> /var/log/health-check.log 2>&1
```
---
## 🔒 Security Hardening
### 1. Firewall Configuration (UFW)
```bash
# Enable UFW
sudo ufw enable
# Allow SSH
sudo ufw allow 22/tcp
# Allow HTTP/HTTPS (Traefik)
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Deny direct database access from internet
sudo ufw deny 5432/tcp
sudo ufw deny 6379/tcp
# Check status
sudo ufw status
```
### 2. Automatic Security Updates
```bash
# Install unattended-upgrades
sudo apt-get install unattended-upgrades
# Configure
sudo dpkg-reconfigure -plow unattended-upgrades
```
### 3. Fail2Ban (Brute Force Protection)
```bash
# Install Fail2Ban
sudo apt-get install fail2ban
# Create custom jail for Traefik
sudo tee /etc/fail2ban/jail.d/traefik.conf << EOF
[traefik-auth]
enabled = true
port = http,https
filter = traefik-auth
logpath = /var/log/traefik/access.log
maxretry = 5
bantime = 3600
findtime = 600
EOF
# Restart Fail2Ban
sudo systemctl restart fail2ban
```
### 4. Docker Socket Protection
```bash
# Never expose Docker socket directly
# Instead, use Docker socket proxy
# Add to docker-compose.yml:
# socket-proxy:
# image: tecnativa/docker-socket-proxy
# environment:
# CONTAINERS: 1
# NETWORKS: 1
# SERVICES: 1
# TASKS: 1
# volumes:
# - /var/run/docker.sock:/var/run/docker.sock:ro
```
---
## 🎯 Performance Optimization
### 1. Docker Resource Limits
Add to `docker-compose.yml`:
```yaml
services:
mana-core-auth:
# ... existing config
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
reservations:
cpus: '0.5'
memory: 512M
```
### 2. PostgreSQL Tuning
```bash
# Create custom postgresql.conf
cat > postgres/postgresql.conf << EOF
# Memory
shared_buffers = 2GB
effective_cache_size = 6GB
work_mem = 16MB
maintenance_work_mem = 512MB
# Connections
max_connections = 200
# Checkpoints
checkpoint_completion_target = 0.9
wal_buffers = 16MB
# Query Planning
random_page_cost = 1.1
effective_io_concurrency = 200
EOF
# Mount in docker-compose.yml:
# volumes:
# - ./postgres/postgresql.conf:/etc/postgresql/postgresql.conf:ro
# command: postgres -c config_file=/etc/postgresql/postgresql.conf
```
### 3. Redis Tuning
Already optimized in docker-compose.yml with:
- `maxmemory 512mb`
- `maxmemory-policy allkeys-lru`
- `appendonly yes` (persistence)
---
## 🚨 Troubleshooting
### Service Won't Start
```bash
# Check logs
docker compose logs mana-core-auth
# Check if port is already in use
sudo netstat -tlnp | grep :3000
# Restart service
docker compose restart mana-core-auth
```
### Database Connection Issues
```bash
# Test PostgreSQL connection
docker exec -it postgres psql -U postgres -d manacore -c "SELECT version();"
# Test PgBouncer connection
docker exec -it pgbouncer psql -h localhost -p 6432 -U postgres -d manacore
```
### SSL Certificate Issues
```bash
# Check Traefik logs
docker compose logs traefik | grep -i "acme\|certificate"
# Manually trigger certificate renewal
docker compose restart traefik
# Check acme.json
cat traefik/acme.json
```
### Out of Disk Space
```bash
# Check disk usage
df -h
# Clean up Docker
docker system prune -a --volumes
# Clean up old logs
docker compose logs --tail=0 > /dev/null
```
---
## 📈 Scaling Strategies
### Horizontal Scaling (Multiple Auth Instances)
```yaml
# docker-compose.yml
services:
mana-core-auth:
# ... existing config
deploy:
replicas: 3 # Run 3 instances
# Traefik automatically load balances
```
### Database Read Replicas
```yaml
# Add read replica
postgres-replica:
image: postgres:16-alpine
environment:
POSTGRES_PRIMARY_HOST: postgres
POSTGRES_REPLICATION_MODE: slave
volumes:
- postgres_replica_data:/var/lib/postgresql/data
```
---
## ✅ Production Checklist
Before going live:
- [ ] SSL certificates working (Let's Encrypt)
- [ ] Firewall configured (UFW)
- [ ] Automated backups scheduled (daily)
- [ ] Monitoring dashboards accessible (Grafana)
- [ ] Health checks passing
- [ ] Environment variables secured (chmod 600)
- [ ] Database performance tuned
- [ ] Fail2Ban configured
- [ ] Docker resource limits set
- [ ] Logs rotation configured
- [ ] Disaster recovery plan documented
---
## 📚 Additional Resources
- Docker Documentation: https://docs.docker.com
- Traefik Documentation: https://doc.traefik.io/traefik/
- PostgreSQL Performance: https://pgtune.leopard.in.ua/
- Hetzner Cloud: https://www.hetzner.com/cloud
---
**Document Status:** ✅ Complete - Ready for Production Deployment
**Last Updated:** 2025-11-25

File diff suppressed because it is too large Load diff

View file

@ -1,462 +0,0 @@
# Researcher Agent - Authentication System Research Deliverables
**Hive Mind Collective Intelligence System**
**Agent:** Researcher
**Mission:** Comprehensive authentication system research
**Date:** 2025-11-25
**Status:** ✅ COMPLETE
---
## 📋 Mission Objectives (Completed)
1. ✅ Investigate "Better Auth" library capabilities and features
2. ✅ Research PostgreSQL auth patterns and security best practices
3. ✅ Compare alternative auth solutions (Auth.js, Supabase Auth, custom JWT)
4. ✅ Identify industry standards for credit/token systems
5. ✅ Research payment gateway integration for digital credits (Stripe, etc.)
6. ✅ Analyze multi-app authentication patterns (OAuth2, JWT strategies)
---
## 📚 Deliverables Overview
### 🎯 Primary Documents
#### 1. Comprehensive Research Report (74KB)
**File:** `/Users/wuesteon/dev/mana_universe/manacore-monorepo/.hive-mind/auth-research-report.md`
**Contents:**
- 12 comprehensive sections covering all research objectives
- 50+ code examples
- Security checklists
- Best practices documentation
- Implementation roadmap
- Risk assessments
**Sections:**
1. Authentication Library Comparison (Better Auth, Auth.js, Supabase, Clerk, Auth0)
2. PostgreSQL Security Best Practices
3. JWT Security Best Practices
4. PostgreSQL Row-Level Security (RLS) for Multi-Tenancy
5. Credit/Token System Architecture
6. Payment Integration (Stripe)
7. Multi-App Authentication Patterns
8. Technology Recommendation Matrix
9. Implementation Roadmap
10. Security Checklist
11. Monitoring & Observability
12. Additional Resources
**Audience:** Technical team, architects, developers
---
#### 2. Executive Summary (11KB)
**File:** `/Users/wuesteon/dev/mana_universe/manacore-monorepo/.hive-mind/auth-research-executive-summary.md`
**Contents:**
- Quick recommendations
- Key findings summary
- Cost analysis
- Risk assessment
- Implementation priority
- Security checklist
- Performance considerations
**Audience:** Leadership, product managers, technical leads
---
#### 3. Decision Matrix (14KB)
**File:** `/Users/wuesteon/dev/mana_universe/manacore-monorepo/.hive-mind/auth-research-decision-matrix.md`
**Contents:**
- Visual decision trees
- Comparison tables
- Scorecards
- Cost breakdowns
- Scenario-based recommendations
- Implementation checklist
**Audience:** Decision makers, project managers
---
### 🔍 Supporting Documents
#### 4. Security Architecture Report (65KB)
**File:** `/Users/wuesteon/dev/mana_universe/manacore-monorepo/.hive-mind/ANALYST_SECURITY_ARCHITECTURE_REPORT.md`
**Note:** Created by Analyst agent (complementary research)
---
#### 5. Central Auth Design (76KB)
**File:** `/Users/wuesteon/dev/mana_universe/manacore-monorepo/.hive-mind/central-auth-and-credits-design.md`
**Note:** Created by Analyst agent (complementary research)
---
## 🎯 Key Recommendations
### Primary Technology Stack
```
┌─────────────────────────────────────────────┐
│ RECOMMENDED ARCHITECTURE │
├─────────────────────────────────────────────┤
│ Auth Framework: Better Auth │
│ Database: PostgreSQL 16+ │
│ ORM: Drizzle │
│ Payment Gateway: Stripe │
│ JWT Algorithm: RS256 │
│ Token Storage: httpOnly/SecureStore │
└─────────────────────────────────────────────┘
```
### Why Better Auth?
| Feature | Status | Impact |
|---------|--------|--------|
| Cost | ✅ FREE | Zero licensing costs |
| TypeScript | ✅ First-class | Excellent DX |
| Features | ✅ Comprehensive | 2FA, passkeys, multi-session built-in |
| Monorepo Fit | ✅ Perfect | Framework-agnostic |
| Vendor Lock-in | ✅ None | Full control |
| Maturity | ⚠️ New (2024) | YC-backed, active development |
**Confidence:** ⭐⭐⭐⭐☆ (4.5/5)
---
## 💰 Cost Analysis
### At 10,000 Active Users
| Solution | Monthly Cost | Annual Cost | Savings |
|----------|-------------|-------------|---------|
| **Recommended Stack** | $190-245 | $2,280-2,940 | Baseline |
| Clerk | $720-745 | $8,640-8,940 | -$6,360/year |
| Auth0 | $205-435 | $2,460-5,220 | -$180-2,280/year |
| Supabase Auth | $170-195 | $2,040-2,340 | +$240-600/year (but reliability concerns) |
**ROI:** Save $6,000-8,000/year vs Clerk at 10k users scale
---
## 🔐 Security Highlights
### Critical Must-Haves Identified
1. **JWT Security**
- RS256 algorithm (asymmetric keys)
- 15-minute access token expiration
- 7-day refresh token with rotation
- httpOnly cookies (web) / SecureStore (mobile)
2. **PostgreSQL Security**
- SCRAM-SHA-256 authentication
- Row-Level Security (RLS) enabled
- SSL/TLS for all connections
- Principle of least privilege
3. **Payment Security**
- Idempotency keys for all transactions
- Stripe webhook signature verification
- Double-entry ledger pattern
- DECIMAL types for monetary values
4. **Multi-Tenant Security**
- RLS policies on all tables
- Tenant context via JWT claims
- Defense in depth approach
- Extensive integration testing
---
## 📊 Research Methodology
### Sources Consulted
1. **Documentation**
- Better Auth official docs
- PostgreSQL security guides
- Stripe API reference
- JWT best practices (Curity, Auth0)
2. **Comparisons**
- Better Stack community guides
- Hyperknot auth provider comparison
- LogRocket technical analysis
- Industry blogs and case studies
3. **Standards**
- OAuth 2.0 RFC specifications
- JWT RFC 7519
- Payment Card Industry (PCI) guidelines
- OWASP security cheatsheets
4. **Real-World Examples**
- AWS multi-tenant patterns
- Crunchy Data RLS guides
- Modern Treasury idempotency patterns
- Stripe integration examples
### Research Quality Indicators
- ✅ Multiple independent sources verified
- ✅ Recent information (2024-2025)
- ✅ Industry best practices validated
- ✅ Real-world implementations studied
- ✅ Security standards cross-referenced
- ✅ Cost analysis from official pricing
- ✅ Technical specifications verified
---
## 📈 Implementation Timeline
### Phased Approach (14 Weeks Total)
```
Week 1-2: Foundation
├─ Better Auth setup
├─ PostgreSQL configuration
├─ RS256 key generation
└─ Basic auth API
Week 3-4: Multi-App Integration
├─ @manacore/shared-auth package
├─ App-token generation
├─ Session management
└─ RLS policies
Week 5-6: Credit System
├─ Ledger schema
├─ Double-entry bookkeeping
├─ Idempotency handling
└─ Credit APIs
Week 7-8: Payment Integration
├─ Stripe setup
├─ Payment intents
├─ Webhook handlers
└─ Credit packages
Week 9-12: Advanced Features
├─ 2FA implementation
├─ Multi-session management
├─ Organization support
└─ OAuth providers
Week 13-14: Production Readiness
├─ Security audit
├─ Performance testing
├─ Monitoring setup
└─ Documentation
```
---
## 🎓 Key Learnings
### Better Auth Advantages
1. **TypeScript-First Design**
- Automatic type generation from schema
- Full IntelliSense support
- Compile-time validation
2. **Database Adapter System**
- Supports Drizzle, Prisma, TypeORM
- Automatic schema generation
- Built-in migration support
3. **Plugin Architecture**
- Official plugins (2FA, organizations)
- Third-party ecosystem growing
- Easy to extend
4. **Framework Agnostic**
- Works with React, Vue, Svelte, Astro
- Backend agnostic (NestJS, Express, Hono)
- Perfect for monorepos
### PostgreSQL RLS Insights
1. **Defense in Depth**
- Even if application code has bugs, database enforces isolation
- Policies apply at database level
- Cannot be bypassed by application
2. **Performance**
- Minimal overhead with proper indexing
- tenant_id indexes are critical
- Composite indexes for query patterns
3. **Testing is Critical**
- Must test all access patterns
- Integration tests for each policy
- Verify cross-tenant isolation
### Credit System Best Practices
1. **Double-Entry Ledger**
- Every transaction creates debit + credit entries
- Mathematical proof of accuracy
- Complete audit trail
2. **Idempotency**
- Prevents duplicate charges
- Safe to retry failed requests
- Industry standard pattern
3. **DECIMAL for Money**
- Never use FLOAT for monetary values
- DECIMAL ensures precision
- No rounding errors
---
## 🚀 Next Steps
### Immediate Actions (This Week)
1. **Better Auth POC** (2-3 days)
- [ ] Install Better Auth
- [ ] Test with PostgreSQL
- [ ] Validate TypeScript generation
- [ ] Test basic auth flow
2. **Team Review** (1 day)
- [ ] Present findings to team
- [ ] Discuss concerns
- [ ] Confirm technology choices
- [ ] Get stakeholder buy-in
3. **Architecture Planning** (2 days)
- [ ] Design database schema
- [ ] Plan API endpoints
- [ ] Define JWT claims structure
- [ ] Document authentication flows
### Week 2 Actions
4. **Initial Implementation**
- [ ] Set up Better Auth with Drizzle
- [ ] Configure PostgreSQL
- [ ] Generate RS256 keys
- [ ] Implement login/register endpoints
5. **Stripe Setup**
- [ ] Create Stripe test account
- [ ] Design credit packages
- [ ] Plan pricing strategy
- [ ] Test webhook integration
---
## ❓ Questions for Team
### Product Questions
1. **Credit Pricing**
- What should credit packages cost?
- Suggested: 100 credits for $9.99, 500 for $39.99, etc.
2. **Credit Expiration**
- Should credits expire? If so, after how long?
- Recommendation: 90 days for purchased, no expiration for bonus
3. **Subscription Model**
- Offer monthly subscriptions or pay-as-you-go only?
- Recommendation: Start with pay-as-you-go, add subscriptions later
4. **OAuth Providers**
- Which social login providers are required?
- Recommendation: Google, GitHub, Apple (for iOS)
### Technical Questions
5. **Multi-Tenancy Priority**
- Are organizations/teams a priority feature?
- Better Auth supports this, but adds complexity
6. **Compliance Requirements**
- Any specific compliance needs? (GDPR, HIPAA, SOC 2)
- Affects implementation decisions
7. **Rate Limiting**
- Should rate limiting be per-user or per-IP?
- Recommendation: Both (user + IP-based)
---
## 📞 Contact & Support
### For Questions About This Research
**Primary Contact:** Queen Agent (Hive Mind Aggregator)
**Research Agent:** Available for clarifications
**Location:** `/Users/wuesteon/dev/mana_universe/manacore-monorepo/.hive-mind/`
### Additional Resources
- **Full Report:** `auth-research-report.md` (74KB)
- **Executive Summary:** `auth-research-executive-summary.md` (11KB)
- **Decision Matrix:** `auth-research-decision-matrix.md` (14KB)
- **Complementary Research:** `ANALYST_SECURITY_ARCHITECTURE_REPORT.md` (65KB)
---
## 📝 Version History
| Version | Date | Changes | Agent |
|---------|------|---------|-------|
| 1.0 | 2025-11-25 | Initial comprehensive research completed | Researcher |
| - | - | Security architecture analysis | Analyst |
| - | - | Central auth design | Analyst |
---
## ✅ Research Completeness
| Research Objective | Status | Confidence | Documentation |
|-------------------|--------|-----------|---------------|
| Better Auth Investigation | ✅ Complete | ⭐⭐⭐⭐⭐ | Section 1 |
| PostgreSQL Security | ✅ Complete | ⭐⭐⭐⭐⭐ | Section 2 |
| Auth Solutions Comparison | ✅ Complete | ⭐⭐⭐⭐⭐ | Section 1 |
| Credit System Standards | ✅ Complete | ⭐⭐⭐⭐⭐ | Section 5 |
| Payment Integration | ✅ Complete | ⭐⭐⭐⭐⭐ | Section 6 |
| Multi-App Auth Patterns | ✅ Complete | ⭐⭐⭐⭐⭐ | Section 7 |
**Overall Confidence:** ⭐⭐⭐⭐⭐ (5/5)
---
## 🎯 Success Criteria (Met)
- ✅ Comprehensive technology comparison completed
- ✅ Clear recommendation provided with justification
- ✅ Security best practices documented
- ✅ Implementation roadmap defined
- ✅ Cost analysis completed
- ✅ Risk assessment performed
- ✅ Code examples provided
- ✅ Multiple audience formats (technical, executive, decision)
- ✅ Real-world patterns researched
- ✅ Industry standards validated
---
**Mission Status:** ✅ COMPLETE
**Ready for:** Queen Agent aggregation and team review
**Recommendation:** Proceed with Better Auth + PostgreSQL + Stripe implementation
---
*Generated by Researcher Agent - Hive Mind Collective Intelligence System*
*For the Mana Universe Monorepo Project*

View file

@ -1,469 +0,0 @@
# Authentication System Decision Matrix
**Visual Decision Guide | Researcher Agent**
**Date:** 2025-11-25
---
## 🎯 Quick Decision Tree
```
Need Auth for Multi-App Monorepo?
├─ Budget < $100/month?
│ │
│ ├─ YES ──→ Better Auth + PostgreSQL ✅ RECOMMENDED
│ │ - FREE
│ │ - Full control
│ │ - All features included
│ │
│ └─ NO ──→ Consider Clerk (if budget > $500/mo)
│ - Best DX
│ - Managed solution
│ - Expensive
└─ Already using Supabase heavily?
├─ YES ──→ Auth.js + Supabase ⚠️ WITH CAUTION
│ - Leverage existing infra
│ - Watch for reliability issues
└─ NO ──→ Better Auth + PostgreSQL ✅ RECOMMENDED
```
---
## 📊 Technology Comparison Matrix
### Authentication Libraries
| | Better Auth | Auth.js | Supabase Auth | Clerk | Auth0 |
|---|:-----------:|:-------:|:-------------:|:-----:|:-----:|
| **Cost** | ✅ FREE | ✅ FREE | 💰 $25/mo | 💰💰 $550/mo | 💰💰 $35-240/mo |
| **Setup Complexity** | ⭐⭐⭐⭐ Easy | ⭐⭐⭐ Medium | ⭐⭐⭐⭐⭐ Very Easy | ⭐⭐⭐⭐⭐ Very Easy | ⭐⭐⭐ Medium |
| **TypeScript Support** | ✅ Excellent | ⚠️ Good | ⚠️ Good | ✅ Excellent | ⚠️ Good |
| **2FA Built-in** | ✅ Yes | ❌ No | ⚠️ Limited | ✅ Yes | ✅ Yes |
| **Multi-Session** | ✅ Yes | ⚠️ Custom | ⚠️ Limited | ✅ Yes | ✅ Yes |
| **Auto Schema** | ✅ Yes | ❌ No | ✅ Yes | N/A | N/A |
| **Self-Hosted** | ✅ Yes | ✅ Yes | ⚠️ Hybrid | ❌ No | ❌ No |
| **Vendor Lock-in** | ✅ None | ✅ None | ⚠️ High | ⚠️ High | ⚠️ High |
| **Maintenance Risk** | ⭐⭐⭐⭐ Low | ⚠️ High | ⭐⭐⭐ Medium | ⭐⭐⭐⭐ Low | ⭐⭐⭐⭐ Low |
| **Battle-Tested** | ⚠️ New (2024) | ✅ Mature | ✅ Mature | ✅ Mature | ✅ Mature |
| **Community** | ⭐⭐ Small | ⭐⭐⭐⭐ Large | ⭐⭐⭐ Medium | ⭐⭐⭐ Medium | ⭐⭐⭐⭐ Large |
| **Monorepo Fit** | ✅ Excellent | ⭐⭐⭐ Good | ⭐⭐⭐ Good | ⭐⭐ Limited | ⭐⭐ Limited |
#### Legend
- ✅ Excellent/Yes
- ⭐ Rating (more stars = better)
- ⚠️ Caution/Limited
- ❌ No/Poor
- 💰 Cost indicator (more = higher cost)
---
## 🔐 Security Features Comparison
| Feature | Better Auth | Auth.js | Supabase | Clerk | Auth0 |
|---------|:-----------:|:-------:|:--------:|:-----:|:-----:|
| **Passkeys (WebAuthn)** | ✅ | ⚠️ Plugin | ❌ | ✅ | ✅ |
| **2FA/TOTP** | ✅ | ⚠️ Custom | ⚠️ Limited | ✅ | ✅ |
| **Magic Links** | ✅ | ✅ | ✅ | ✅ | ✅ |
| **Session Management** | ✅ Advanced | ⚠️ Basic | ⚠️ Basic | ✅ Advanced | ✅ Advanced |
| **Device Tracking** | ✅ | ⚠️ Custom | ❌ | ✅ | ✅ |
| **Rate Limiting** | ⚠️ External | ⚠️ External | ⚠️ Limited | ✅ Built-in | ✅ Built-in |
| **Breach Detection** | ❌ | ❌ | ❌ | ✅ | ✅ |
| **Bot Protection** | ⚠️ External | ⚠️ External | ⚠️ Limited | ✅ Built-in | ✅ Built-in |
---
## 💾 Database & ORM Options
### PostgreSQL Features
| Feature | PostgreSQL | MySQL | MongoDB |
|---------|:----------:|:-----:|:-------:|
| **RLS Support** | ✅ Native | ❌ No | ❌ No |
| **ACID Compliance** | ✅ Full | ✅ Full | ⚠️ Limited |
| **JSON Support** | ✅ Excellent | ⚠️ Basic | ✅ Native |
| **Full-Text Search** | ✅ Advanced | ⚠️ Basic | ✅ Good |
| **Better Auth Support** | ✅ Primary | ✅ Yes | ✅ Yes |
| **Maturity** | ✅ 25+ years | ✅ 25+ years | ⭐ 15 years |
**Verdict:** PostgreSQL for multi-tenant security (RLS) and financial accuracy
---
### ORM Comparison
| Feature | Drizzle | Prisma | TypeORM |
|---------|:-------:|:------:|:-------:|
| **Better Auth Support** | ✅ Official | ✅ Official | ⚠️ Generic |
| **Performance** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
| **Type Safety** | ✅ Excellent | ✅ Excellent | ⚠️ Good |
| **Migration Tools** | ✅ Built-in | ✅ Excellent | ⚠️ Basic |
| **Learning Curve** | ⭐⭐⭐⭐ Easy | ⭐⭐⭐ Medium | ⭐⭐ Hard |
| **Raw SQL Support** | ✅ Excellent | ⚠️ Limited | ✅ Good |
**Verdict:** Drizzle for performance and Better Auth integration
---
## 💳 Payment Gateway Comparison
| Feature | Stripe | PayPal | Square |
|---------|:------:|:------:|:------:|
| **Transaction Fee** | 2.9% + $0.30 | 3.49% + $0.49 | 2.9% + $0.30 |
| **Global Reach** | ✅ 47+ countries | ✅ 200+ countries | ⚠️ Limited |
| **Developer Experience** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| **Documentation** | ✅ Excellent | ⚠️ Good | ✅ Good |
| **Webhook Reliability** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| **Digital Wallets** | ✅ All major | ✅ All major | ⚠️ Limited |
| **Marketplace Features** | ✅ Connect | ⚠️ Limited | ❌ No |
| **Credit Top-ups** | ✅ Perfect fit | ⚠️ Complex | ✅ Good |
**Verdict:** Stripe for best developer experience and features
---
## 🎨 Architecture Patterns Scorecard
### Pattern 1: Centralized Auth + App Tokens (RECOMMENDED)
```
┌──────────────────┐
│ Mana Core Auth │
│ - User DB │
│ - Credit System │
│ - Issues JWTs │
└────────┬─────────┘
┌────────────────┼────────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ App A │ │ App B │ │ App C │
│Validates│ │Validates│ │Validates│
│ JWT │ │ JWT │ │ JWT │
└─────────┘ └─────────┘ └─────────┘
```
**Score: 9/10**
- ✅ Single source of truth
- ✅ Unified credit system
- ✅ Cross-app SSO
- ✅ Consistent security
- ⚠️ Single point of failure (mitigate with HA)
---
### Pattern 2: Federated Auth (Each App Manages Own)
```
┌─────────┐ ┌─────────┐ ┌─────────┐
│ App A │ │ App B │ │ App C │
│ Auth │ │ Auth │ │ Auth │
└─────────┘ └─────────┘ └─────────┘
│ │ │
└───────────────┼───────────────┘
┌──────▼──────┐
│ Sync DB │
└─────────────┘
```
**Score: 4/10**
- ❌ User data fragmentation
- ❌ Complex credit system
- ❌ No cross-app SSO
- ❌ Inconsistent security
- ✅ Independent scaling
**Verdict:** NOT recommended for Mana ecosystem
---
### Pattern 3: Managed Service (Clerk/Auth0)
```
┌───────────────────┐
│ Clerk/Auth0 │ (External)
│ - User DB │
│ - Session Mgmt │
└────────┬──────────┘
┌────────┼────────┐
│ │ │
┌───▼──┐ ┌──▼──┐ ┌───▼──┐
│App A │ │App B│ │App C │
└──────┘ └─────┘ └──────┘
```
**Score: 6/10**
- ✅ Managed infrastructure
- ✅ Advanced features
- ❌ Expensive ($550+/mo)
- ❌ Vendor lock-in
- ⚠️ Less control over flow
**Verdict:** Only if budget allows and team wants managed solution
---
## 🔄 JWT Token Strategies
### Strategy 1: Short-Lived Access + Refresh (RECOMMENDED)
```
Access Token: 15 minutes ⚡ Fast validation
Refresh Token: 7 days 🔄 Rotate on use
```
**Pros:**
- ✅ Best security (short exposure window)
- ✅ Detects token theft via rotation
- ✅ Industry standard
**Cons:**
- ⚠️ More complexity (refresh flow)
- ⚠️ Database lookups for refresh
**Score: 9/10** - Industry best practice
---
### Strategy 2: Long-Lived Tokens
```
Access Token: 7 days ⚠️ High risk if stolen
```
**Pros:**
- ✅ Simple implementation
- ✅ No refresh logic needed
**Cons:**
- ❌ High security risk
- ❌ Hard to revoke
- ❌ Violates best practices
**Score: 3/10** - NOT recommended
---
### Strategy 3: Stateful Sessions (Database)
```
Session ID: Stored in DB 🗄️ Always check DB
```
**Pros:**
- ✅ Easy revocation
- ✅ Fine-grained control
**Cons:**
- ❌ Database lookup on every request
- ❌ Doesn't scale well
- ❌ Not suitable for microservices
**Score: 5/10** - Only for monoliths
---
## 💰 Cost Breakdown (10k Active Users)
### Option 1: Recommended Stack
| Component | Monthly Cost |
|-----------|-------------|
| Better Auth | $0 (open-source) |
| PostgreSQL (Supabase Pro) | $25 |
| Auth Service Hosting | $20-50 |
| Stripe Fees (500 txns × $10 avg) | $145-170 |
| **Total** | **$190-245/month** |
---
### Option 2: Clerk
| Component | Monthly Cost |
|-----------|-------------|
| Clerk Business Plan | $550 |
| PostgreSQL (Credit System) | $25 |
| Stripe Fees | $145-170 |
| **Total** | **$720-745/month** |
**Extra Cost:** $530-500/month (265% more expensive)
---
### Option 3: Auth0
| Component | Monthly Cost |
|-----------|-------------|
| Auth0 Essentials | $35-240 |
| PostgreSQL (Credit System) | $25 |
| Stripe Fees | $145-170 |
| **Total** | **$205-435/month** |
**Extra Cost:** $15-190/month
---
### Option 4: Supabase Auth
| Component | Monthly Cost |
|-----------|-------------|
| Supabase Pro | $25 |
| Stripe Fees | $145-170 |
| **Total** | **$170-195/month** |
**Savings:** $20-50/month BUT with reliability concerns
---
## 🎯 Final Recommendations by Scenario
### Scenario 1: Startup/MVP (Current Mana Status)
**Recommendation:** Better Auth + PostgreSQL + Stripe
**Why:**
- ✅ Zero auth licensing costs
- ✅ Full control and customization
- ✅ Scales to 100k+ users
- ✅ No vendor lock-in
- ✅ Perfect for monorepo
**Risk:** New library (2024), but YC-backed and active
---
### Scenario 2: Well-Funded Startup (>$1M ARR)
**Recommendation:** Better Auth or Clerk
**Why:**
- Better Auth if team wants control
- Clerk if team wants managed solution and has budget
- Both provide excellent developer experience
---
### Scenario 3: Enterprise (Compliance Requirements)
**Recommendation:** Auth0 or Custom (Better Auth)
**Why:**
- Auth0 for compliance certifications
- Better Auth if building custom compliance layer
- Both support SSO, SAML, etc.
---
### Scenario 4: Already Deep in Supabase
**Recommendation:** Auth.js + Supabase
**Why:**
- Leverage existing Supabase infrastructure
- Auth.js provides better control than Supabase Auth
- Monitor for reliability issues
---
## ⚡ Quick Implementation Checklist
### Week 1-2: Core Auth
- [ ] Install Better Auth
- [ ] Configure PostgreSQL with RLS
- [ ] Generate RS256 key pair
- [ ] Implement login/register endpoints
- [ ] Create JWT validation middleware
### Week 3-4: Multi-App
- [ ] Create @manacore/shared-auth package
- [ ] Implement app-token generation
- [ ] Add session management
- [ ] Configure RLS for each app
### Week 5-6: Credits
- [ ] Design ledger schema (double-entry)
- [ ] Implement credit purchase API
- [ ] Add idempotency handling
- [ ] Build credit usage API
### Week 7-8: Payments
- [ ] Set up Stripe account
- [ ] Implement payment intents
- [ ] Build webhook handlers
- [ ] Add credit packages
---
## 🚨 Critical Success Factors
### Must-Haves
1. ✅ Short-lived access tokens (15-30 min)
2. ✅ Refresh token rotation
3. ✅ httpOnly cookies (web) / SecureStore (mobile)
4. ✅ PostgreSQL RLS for multi-tenancy
5. ✅ Idempotency for all financial transactions
6. ✅ Stripe webhook signature verification
7. ✅ Double-entry ledger for credits
8. ✅ Comprehensive testing (especially RLS)
### Nice-to-Haves
- ⭐ 2FA for all users
- ⭐ Device tracking and management
- ⭐ Organization/team support
- ⭐ Multiple credit types (paid, bonus, promo)
- ⭐ Credit expiration handling
- ⭐ Subscription model
---
## 📈 Scalability Projections
| Metric | Current | 1 Year | 3 Years |
|--------|---------|--------|---------|
| **Users** | 100 | 10,000 | 100,000 |
| **Auth Requests/Day** | 1,000 | 100,000 | 1,000,000 |
| **Credit Transactions/Day** | 50 | 5,000 | 50,000 |
| **Monthly Cost** | $50 | $200 | $500 |
| **DB Size** | 100MB | 10GB | 100GB |
**Bottleneck Analysis:**
- 🟢 100-10k users: Single server sufficient
- 🟡 10k-100k users: Need load balancing + connection pooling
- 🔴 100k+ users: Requires distributed architecture
**Recommended Stack Handles:** Up to 100k users with optimization
---
## ✅ Decision Summary
### For Mana Universe Monorepo
**RECOMMENDED ARCHITECTURE:**
```
Better Auth + PostgreSQL + Drizzle + Stripe
```
**Confidence Level:** ⭐⭐⭐⭐⭐ (5/5)
**Key Reasons:**
1. Perfect fit for monorepo architecture
2. Zero licensing costs (100% open-source)
3. Full control and customization
4. Comprehensive features built-in
5. Excellent TypeScript support
6. No vendor lock-in
7. YC-backed with active development
8. Scales to 100k+ users
**Total Implementation Time:** 14 weeks
**Monthly Operating Cost:** $190-245 at 10k users
---
**Next Step:** Run Better Auth proof-of-concept (2-3 days)
---
*End of Decision Matrix*

View file

@ -1,404 +0,0 @@
# Authentication System Research - Executive Summary
**Researcher Agent | Hive Mind Collective**
**Date:** 2025-11-25
---
## Quick Recommendations
### Core Technology Stack
| Component | Recommendation | Why |
|-----------|----------------|-----|
| **Auth Framework** | Better Auth | Modern, TypeScript-first, comprehensive features, FREE |
| **Database** | PostgreSQL 16+ | Battle-tested, RLS for multi-tenancy, ACID compliance |
| **ORM** | Drizzle | Best Better Auth integration, type-safe, performant |
| **Payment** | Stripe | Industry standard, 47+ countries, excellent DX |
| **JWT Algorithm** | RS256 | Asymmetric keys for distributed systems |
---
## Key Findings
### 1. Better Auth vs Alternatives
**Better Auth** (RECOMMENDED)
- FREE and open-source (no usage limits)
- 2FA, passkeys, multi-session, organization management built-in
- Automatic schema generation and migrations
- Framework-agnostic (perfect for your NestJS/Expo/SvelteKit stack)
- YC-backed with active development
**Alternatives Considered:**
- **Auth.js:** Maintenance concerns (one person maintaining 90% of work)
- **Supabase Auth:** Critical reliability issues (random logouts, no session lifetime config, security concerns)
- **Clerk:** Excellent but expensive ($550/mo for 10k users)
- **Auth0:** Enterprise-grade but costly and overkill
### 2. PostgreSQL Security Best Practices
**Critical Configurations:**
- Use SCRAM-SHA-256 (replace MD5 immediately)
- Enable Row-Level Security (RLS) for all multi-tenant tables
- Set listen_addresses to specific IPs (not '*')
- Enable SSL/TLS for all connections
- Implement principle of least privilege
**RLS for Multi-Tenancy:**
```sql
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON posts
FOR ALL
USING (tenant_id = current_setting('app.current_tenant_id')::UUID);
```
### 3. JWT Security Best Practices
**Token Strategy:**
- Access tokens: 15-30 minutes expiration
- Refresh tokens: 7-14 days with rotation
- Algorithm: RS256 (asymmetric keys)
- Storage: httpOnly cookies (web), SecureStore (mobile)
- NEVER use localStorage
**Refresh Token Rotation:**
- Single-use refresh tokens
- New refresh token issued with each refresh
- Detects and blocks replay attacks
- Industry standard in 2025
**Claims Validation:**
```typescript
interface StandardClaims {
iss: string; // Issuer - MUST validate
sub: string; // Subject (user ID)
aud: string | string[]; // Audience - MUST validate
exp: number; // Expiration - MUST validate
iat: number; // Issued at
nbf?: number; // Not before
}
```
### 4. Credit System Architecture
**Pattern: Double-Entry Ledger**
- Every transaction creates debit + credit entries
- Ensures financial accuracy
- Complete audit trail
- Industry standard for financial systems
**Critical Features:**
- Use DECIMAL for monetary values (never FLOAT)
- Idempotency keys prevent duplicate charges
- Database transactions (BEGIN/COMMIT/ROLLBACK)
- Row locking during balance updates (SELECT FOR UPDATE)
**Schema Highlights:**
```sql
-- Accounts (user wallets)
CREATE TABLE accounts (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
balance DECIMAL(20, 2) NOT NULL CHECK (balance >= 0),
-- ...
);
-- Transaction ledger
CREATE TABLE transactions (
id UUID PRIMARY KEY,
idempotency_key TEXT UNIQUE NOT NULL, -- Prevents duplicates
amount DECIMAL(20, 2) NOT NULL,
status TEXT CHECK (status IN ('pending', 'completed', 'failed')),
-- ...
);
```
### 5. Stripe Integration
**Integration Options:**
1. **Direct Integration** (Recommended initially)
- Simple credit purchases
- Single merchant
- Easier setup
2. **Stripe Connect** (For future marketplace features)
- Multi-party payments
- Revenue sharing
- More complex setup
**Critical Webhook Handling:**
```typescript
// ALWAYS verify webhook signatures
const event = stripe.webhooks.constructEvent(
rawBody,
signature,
webhookSecret
);
// Handle payment success
case 'payment_intent.succeeded':
await creditUserAccount(paymentIntent.metadata);
break;
```
**Best Practices:**
- Always verify webhook signatures
- Use idempotency keys for all operations
- Never trust client-side amounts
- Store Stripe customer ID in user table
- Test thoroughly in test mode
### 6. Multi-App Authentication Pattern
**Architecture:**
```
Mana Core Auth Service (Central)
|
├── Issues: manaToken (universal)
├── Issues: appToken (app-specific, Supabase-compatible)
└── Issues: refreshToken (long-lived)
Apps (Maerchenzauber, Memoro, Picture, Chat)
└── Validate JWT + RLS policies + Use credits
```
**Token Types:**
1. **manaToken:** Universal auth across all apps
2. **appToken:** App-specific, Supabase RLS compatible
3. **refreshToken:** Long-lived, database-stored
**Shared Package:**
Create `@manacore/shared-auth` for:
- Platform-agnostic auth service
- Token management
- Auto-refresh logic
- Storage adapters (SecureStore, cookies)
---
## Implementation Priority
### Phase 1: Foundation (2 weeks)
- Set up Better Auth with PostgreSQL
- Generate RS256 key pair
- Basic auth API (login, register, refresh)
- JWT validation middleware
### Phase 2: Multi-App (2 weeks)
- Create @manacore/shared-auth package
- App-token generation
- Session management
- RLS policies
### Phase 3: Credits (2 weeks)
- Credit ledger schema
- Double-entry bookkeeping
- Idempotency handling
- Credit purchase/usage APIs
### Phase 4: Payments (2 weeks)
- Stripe integration
- Webhook handlers
- Payment method management
- Credit packages
### Phase 5: Advanced (4 weeks)
- 2FA
- Multi-session management
- Organization support
- OAuth providers
### Phase 6: Production (2 weeks)
- Security audit
- Performance testing
- Monitoring
- Documentation
**Total Estimated Time:** 14 weeks
---
## Cost Analysis
### Technology Costs
| Service | Cost | Notes |
|---------|------|-------|
| Better Auth | $0/month | Open-source, self-hosted |
| PostgreSQL | $25-200/month | Depends on hosting (Supabase Pro: $25/mo) |
| Stripe | 2.9% + $0.30/txn | Standard payment processing |
| Hosting | $20-100/month | For auth service (depends on scale) |
**Total Monthly:** ~$45-300/month (depending on scale)
### Comparison to Managed Solutions
| Solution | Cost at 10k Users | Cost at 100k Users |
|----------|-------------------|---------------------|
| Recommended Stack | ~$100/mo + Stripe fees | ~$300/mo + Stripe fees |
| Clerk | $550/mo | $2,500+/mo |
| Auth0 | $35-240/mo | $1,000+/mo |
**Savings:** Up to $2,000+/month at scale
---
## Risk Assessment
### Low Risk
- PostgreSQL (battle-tested, 25+ years)
- Stripe (industry standard)
- JWT with RS256 (well-established pattern)
- Double-entry ledger (accounting standard)
### Medium Risk
- Better Auth (new in 2024, but YC-backed and active)
- Mitigation: Can migrate to Auth.js if needed (similar patterns)
### High Risk Areas to Monitor
- RLS policy configuration (extensive testing required)
- Webhook reliability (implement retry logic)
- Token revocation at scale (consider Redis for blacklist)
---
## Security Checklist
### Critical Must-Haves
- [ ] RS256 algorithm for JWT
- [ ] Token expiration (15min access, 7d refresh)
- [ ] Refresh token rotation
- [ ] httpOnly cookies (web) / SecureStore (mobile)
- [ ] HTTPS everywhere
- [ ] Stripe webhook signature verification
- [ ] PostgreSQL RLS enabled
- [ ] Idempotency keys for transactions
- [ ] Rate limiting on auth endpoints
- [ ] 2FA for admin accounts
### Additional Security
- [ ] Token blacklist (Redis)
- [ ] Device fingerprinting
- [ ] Suspicious activity monitoring
- [ ] Regular security audits
- [ ] Automated dependency updates
- [ ] Penetration testing
---
## Performance Considerations
### Expected Bottlenecks
1. **Database queries with RLS:**
- Solution: Index tenant_id columns
- Impact: Minimal with proper indexing
2. **JWT validation on every request:**
- Solution: Cache public key, validate claims efficiently
- Impact: <1ms per request
3. **Credit balance checks:**
- Solution: Cache balances with TTL
- Impact: Minimal with caching
### Scalability Targets
- 100 req/s: Easily achievable with single server
- 1,000 req/s: Requires load balancing + connection pooling
- 10,000 req/s: Requires distributed architecture + Redis
---
## Alternative Architectures Considered
### Alternative 1: Full Supabase Stack
**Pros:** Tight integration, managed infrastructure
**Cons:** Vendor lock-in, reliability concerns reported, limited customization
**Verdict:** Not recommended due to reliability issues
### Alternative 2: Clerk + Stripe
**Pros:** Best developer experience, managed solution
**Cons:** Extremely expensive ($550/mo for 10k users), vendor lock-in
**Verdict:** Too expensive for freemium model
### Alternative 3: Custom JWT + Prisma
**Pros:** Full control, familiar tools
**Cons:** Reinventing the wheel, maintenance burden, missing features (2FA, etc.)
**Verdict:** Better Auth provides same benefits with less work
---
## Next Steps
### Immediate Actions
1. **Set up Better Auth proof-of-concept** (2 days)
- Install and configure
- Test with PostgreSQL
- Validate TypeScript generation
2. **Design database schema** (3 days)
- User tables
- Credit ledger
- Sessions
- RLS policies
3. **Create @manacore/shared-auth package** (5 days)
- Auth service interface
- Storage adapters
- Token management
4. **Stripe account setup** (1 day)
- Create test account
- Configure webhooks
- Design credit packages
### Decision Points
- Confirm Better Auth after POC
- Finalize credit pricing structure
- Choose hosting provider for auth service
- Decide on monitoring/observability stack
---
## Questions for Team
1. **Credit Pricing:** What should credit packages cost? (e.g., 100 credits for $9.99)
2. **Credit Expiration:** Should credits expire? If so, after how long?
3. **Subscription Model:** Offer monthly subscriptions or pay-as-you-go only?
4. **Multi-Tenancy:** Are organizations/teams a priority feature? (Better Auth supports this)
5. **OAuth Providers:** Which social login providers are required? (Google, GitHub, Apple?)
6. **Compliance:** Any specific compliance requirements? (GDPR, HIPAA, SOC 2?)
---
## Resources
### Full Report
- Comprehensive 12-section analysis: `/Users/wuesteon/dev/mana_universe/manacore-monorepo/.hive-mind/auth-research-report.md`
### Key Documentation
- [Better Auth Docs](https://www.better-auth.com/docs)
- [PostgreSQL RLS Guide](https://www.postgresql.org/docs/current/ddl-rowsecurity.html)
- [Stripe API Reference](https://docs.stripe.com/api)
- [JWT Best Practices](https://curity.io/resources/learn/jwt-best-practices/)
---
## Confidence Levels
| Area | Confidence | Notes |
|------|-----------|-------|
| Better Auth | ⭐⭐⭐⭐☆ | New but YC-backed, excellent features |
| PostgreSQL + RLS | ⭐⭐⭐⭐⭐ | Battle-tested, industry standard |
| Stripe | ⭐⭐⭐⭐⭐ | Dominant market leader |
| JWT Strategy | ⭐⭐⭐⭐⭐ | Well-established best practices |
| Credit Ledger | ⭐⭐⭐⭐⭐ | Standard accounting pattern |
---
**Overall Assessment:** High confidence in recommended architecture. The stack is modern, cost-effective, secure, and aligns perfectly with the monorepo structure and technology choices (NestJS, Expo, SvelteKit).
**Recommendation:** Proceed with Better Auth + PostgreSQL + Stripe implementation.
---
*End of Executive Summary*

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,183 +0,0 @@
🧠 HIVE MIND COLLECTIVE INTELLIGENCE SYSTEM
═══════════════════════════════════════════════
You are the Queen coordinator of a Hive Mind swarm with collective intelligence capabilities.
HIVE MIND CONFIGURATION:
📌 Swarm ID: swarm-1764085340120-zlijqvfao
📌 Swarm Name: hive-1764085340109
🎯 Objective: I need to create an central auth system, with users, and credits, the credits are called 'mana' in our system, the can buy for example 100mana for 1euro. as technology i want postgres and better auth, or other technologies if needed. make an detailled plan to create such a central systen for our systen.
👑 Queen Type: strategic
🐝 Worker Count: 4
🤝 Consensus Algorithm: majority
⏰ Initialized: 2025-11-25T15:42:20.129Z
WORKER DISTRIBUTION:
• researcher: 1 agents
• coder: 1 agents
• analyst: 1 agents
• tester: 1 agents
🔧 AVAILABLE MCP TOOLS FOR HIVE MIND COORDINATION:
1⃣ **COLLECTIVE INTELLIGENCE**
mcp__claude-flow__consensus_vote - Democratic decision making
mcp__claude-flow__memory_share - Share knowledge across the hive
mcp__claude-flow__neural_sync - Synchronize neural patterns
mcp__claude-flow__swarm_think - Collective problem solving
2⃣ **QUEEN COORDINATION**
mcp__claude-flow__queen_command - Issue directives to workers
mcp__claude-flow__queen_monitor - Monitor swarm health
mcp__claude-flow__queen_delegate - Delegate complex tasks
mcp__claude-flow__queen_aggregate - Aggregate worker results
3⃣ **WORKER MANAGEMENT**
mcp__claude-flow__agent_spawn - Create specialized workers
mcp__claude-flow__agent_assign - Assign tasks to workers
mcp__claude-flow__agent_communicate - Inter-agent communication
mcp__claude-flow__agent_metrics - Track worker performance
4⃣ **TASK ORCHESTRATION**
mcp__claude-flow__task_create - Create hierarchical tasks
mcp__claude-flow__task_distribute - Distribute work efficiently
mcp__claude-flow__task_monitor - Track task progress
mcp__claude-flow__task_aggregate - Combine task results
5⃣ **MEMORY & LEARNING**
mcp__claude-flow__memory_store - Store collective knowledge
mcp__claude-flow__memory_retrieve - Access shared memory
mcp__claude-flow__neural_train - Learn from experiences
mcp__claude-flow__pattern_recognize - Identify patterns
📋 HIVE MIND EXECUTION PROTOCOL:
As the Queen coordinator, you must:
1. **INITIALIZE THE HIVE** (CRITICAL: Use Claude Code's Task Tool for Agents):
Step 1: Optional MCP Coordination Setup (Single Message):
[MCP Tools - Coordination Only]:
mcp__claude-flow__agent_spawn { "type": "researcher", "count": 1 }
mcp__claude-flow__agent_spawn { "type": "coder", "count": 1 }
mcp__claude-flow__agent_spawn { "type": "analyst", "count": 1 }
mcp__claude-flow__agent_spawn { "type": "tester", "count": 1 }
mcp__claude-flow__memory_store { "key": "hive/objective", "value": "I need to create an central auth system, with users, and credits, the credits are called 'mana' in our system, the can buy for example 100mana for 1euro. as technology i want postgres and better auth, or other technologies if needed. make an detailled plan to create such a central systen for our systen." }
mcp__claude-flow__memory_store { "key": "hive/queen", "value": "strategic" }
mcp__claude-flow__swarm_think { "topic": "initial_strategy" }
Step 2: REQUIRED - Spawn ACTUAL Agents with Claude Code's Task Tool (Single Message):
[Claude Code Task Tool - CONCURRENT Agent Execution]:
Task("Researcher Agent", "You are a researcher in the hive. Coordinate via hooks. - Conduct thorough research using WebSearch and WebFetch", "researcher")
Task("Coder Agent", "You are a coder in the hive. Coordinate via hooks. - Write clean, maintainable, well-documented code", "coder")
Task("Analyst Agent", "You are a analyst in the hive. Coordinate via hooks. - Analyze data patterns and trends", "analyst")
Task("Tester Agent", "You are a tester in the hive. Coordinate via hooks. - Design comprehensive test strategies", "tester")
Step 3: Batch ALL Todos Together (Single TodoWrite Call):
TodoWrite { "todos": [
{ "id": "1", "content": "Initialize hive mind collective", "status": "in_progress", "priority": "high" },
{ "id": "2", "content": "Establish consensus protocols", "status": "pending", "priority": "high" },
{ "id": "3", "content": "Distribute initial tasks to workers", "status": "pending", "priority": "high" },
{ "id": "4", "content": "Set up collective memory", "status": "pending", "priority": "high" },
{ "id": "5", "content": "Monitor worker health", "status": "pending", "priority": "medium" },
{ "id": "6", "content": "Aggregate worker outputs", "status": "pending", "priority": "medium" },
{ "id": "7", "content": "Learn from patterns", "status": "pending", "priority": "low" },
{ "id": "8", "content": "Optimize performance", "status": "pending", "priority": "low" }
] }
2. **ESTABLISH COLLECTIVE INTELLIGENCE**:
- Use consensus_vote for major decisions
- Share all discoveries via memory_share
- Synchronize learning with neural_sync
- Coordinate strategy with swarm_think
3. **QUEEN LEADERSHIP PATTERNS**:
- Focus on high-level planning and coordination
- Delegate implementation details to workers
- Monitor overall progress and adjust strategy
- Make executive decisions when consensus fails
4. **WORKER COORDINATION**:
- Spawn workers based on task requirements
- Assign tasks according to worker specializations
- Enable peer-to-peer communication for collaboration
- Monitor and rebalance workloads as needed
5. **CONSENSUS MECHANISMS**:
- Decisions require >50% worker agreement
6. **COLLECTIVE MEMORY**:
- Store all important decisions in shared memory
- Tag memories with worker IDs and timestamps
- Use memory namespaces: hive/, queen/, workers/, tasks/
- Implement memory consensus for critical data
7. **PERFORMANCE OPTIMIZATION**:
- Monitor swarm metrics continuously
- Identify and resolve bottlenecks
- Train neural networks on successful patterns
- Scale worker count based on workload
💡 HIVE MIND BEST PRACTICES:
✅ ALWAYS use BatchTool for parallel operations
✅ Store decisions in collective memory immediately
✅ Use consensus for critical path decisions
✅ Monitor worker health and reassign if needed
✅ Learn from failures and adapt strategies
✅ Maintain constant inter-agent communication
✅ Aggregate results before final delivery
❌ NEVER make unilateral decisions without consensus
❌ NEVER let workers operate in isolation
❌ NEVER ignore performance metrics
❌ NEVER skip memory synchronization
❌ NEVER abandon failing workers
🎯 OBJECTIVE EXECUTION STRATEGY:
For the objective: "I need to create an central auth system, with users, and credits, the credits are called 'mana' in our system, the can buy for example 100mana for 1euro. as technology i want postgres and better auth, or other technologies if needed. make an detailled plan to create such a central systen for our systen."
1. Break down into major phases using swarm_think
2. Create specialized worker teams for each phase
3. Establish success criteria and checkpoints
4. Implement feedback loops and adaptation
5. Aggregate and synthesize all worker outputs
6. Deliver comprehensive solution with consensus
⚡ CRITICAL: CONCURRENT EXECUTION WITH CLAUDE CODE'S TASK TOOL:
The Hive Mind MUST use Claude Code's Task tool for actual agent execution:
✅ CORRECT Pattern:
[Single Message - All Agents Spawned Concurrently]:
Task("Researcher", "Research patterns and best practices...", "researcher")
Task("Coder", "Implement core features...", "coder")
Task("Tester", "Create comprehensive tests...", "tester")
Task("Analyst", "Analyze performance metrics...", "analyst")
TodoWrite { todos: [8-10 todos ALL in ONE call] }
❌ WRONG Pattern:
Message 1: Task("agent1", ...)
Message 2: Task("agent2", ...)
Message 3: TodoWrite { single todo }
// This breaks parallel coordination!
Remember:
- Use Claude Code's Task tool to spawn ALL agents in ONE message
- MCP tools are ONLY for coordination setup, not agent execution
- Batch ALL TodoWrite operations (5-10+ todos minimum)
- Execute ALL file operations concurrently
- Store multiple memories simultaneously
🚀 BEGIN HIVE MIND EXECUTION:
Initialize the swarm now with the configuration above. Use your collective intelligence to solve the objective efficiently. The Queen must coordinate, workers must collaborate, and the hive must think as one.
Remember: You are not just coordinating agents - you are orchestrating a collective intelligence that is greater than the sum of its parts.

View file

@ -1,183 +0,0 @@
🧠 HIVE MIND COLLECTIVE INTELLIGENCE SYSTEM
═══════════════════════════════════════════════
You are the Queen coordinator of a Hive Mind swarm with collective intelligence capabilities.
HIVE MIND CONFIGURATION:
📌 Swarm ID: swarm-1764212414813-nbrqx50g3
📌 Swarm Name: hive-1764212414796
🎯 Objective: i want to host this on hetzner or coolify, can you make me an good architecture and plan for ci/cd also with automated tests and everything else important for that goal
👑 Queen Type: strategic
🐝 Worker Count: 4
🤝 Consensus Algorithm: majority
⏰ Initialized: 2025-11-27T03:00:14.819Z
WORKER DISTRIBUTION:
• researcher: 1 agents
• coder: 1 agents
• analyst: 1 agents
• tester: 1 agents
🔧 AVAILABLE MCP TOOLS FOR HIVE MIND COORDINATION:
1⃣ **COLLECTIVE INTELLIGENCE**
mcp__claude-flow__consensus_vote - Democratic decision making
mcp__claude-flow__memory_share - Share knowledge across the hive
mcp__claude-flow__neural_sync - Synchronize neural patterns
mcp__claude-flow__swarm_think - Collective problem solving
2⃣ **QUEEN COORDINATION**
mcp__claude-flow__queen_command - Issue directives to workers
mcp__claude-flow__queen_monitor - Monitor swarm health
mcp__claude-flow__queen_delegate - Delegate complex tasks
mcp__claude-flow__queen_aggregate - Aggregate worker results
3⃣ **WORKER MANAGEMENT**
mcp__claude-flow__agent_spawn - Create specialized workers
mcp__claude-flow__agent_assign - Assign tasks to workers
mcp__claude-flow__agent_communicate - Inter-agent communication
mcp__claude-flow__agent_metrics - Track worker performance
4⃣ **TASK ORCHESTRATION**
mcp__claude-flow__task_create - Create hierarchical tasks
mcp__claude-flow__task_distribute - Distribute work efficiently
mcp__claude-flow__task_monitor - Track task progress
mcp__claude-flow__task_aggregate - Combine task results
5⃣ **MEMORY & LEARNING**
mcp__claude-flow__memory_store - Store collective knowledge
mcp__claude-flow__memory_retrieve - Access shared memory
mcp__claude-flow__neural_train - Learn from experiences
mcp__claude-flow__pattern_recognize - Identify patterns
📋 HIVE MIND EXECUTION PROTOCOL:
As the Queen coordinator, you must:
1. **INITIALIZE THE HIVE** (CRITICAL: Use Claude Code's Task Tool for Agents):
Step 1: Optional MCP Coordination Setup (Single Message):
[MCP Tools - Coordination Only]:
mcp__claude-flow__agent_spawn { "type": "researcher", "count": 1 }
mcp__claude-flow__agent_spawn { "type": "coder", "count": 1 }
mcp__claude-flow__agent_spawn { "type": "analyst", "count": 1 }
mcp__claude-flow__agent_spawn { "type": "tester", "count": 1 }
mcp__claude-flow__memory_store { "key": "hive/objective", "value": "i want to host this on hetzner or coolify, can you make me an good architecture and plan for ci/cd also with automated tests and everything else important for that goal" }
mcp__claude-flow__memory_store { "key": "hive/queen", "value": "strategic" }
mcp__claude-flow__swarm_think { "topic": "initial_strategy" }
Step 2: REQUIRED - Spawn ACTUAL Agents with Claude Code's Task Tool (Single Message):
[Claude Code Task Tool - CONCURRENT Agent Execution]:
Task("Researcher Agent", "You are a researcher in the hive. Coordinate via hooks. - Conduct thorough research using WebSearch and WebFetch", "researcher")
Task("Coder Agent", "You are a coder in the hive. Coordinate via hooks. - Write clean, maintainable, well-documented code", "coder")
Task("Analyst Agent", "You are a analyst in the hive. Coordinate via hooks. - Analyze data patterns and trends", "analyst")
Task("Tester Agent", "You are a tester in the hive. Coordinate via hooks. - Design comprehensive test strategies", "tester")
Step 3: Batch ALL Todos Together (Single TodoWrite Call):
TodoWrite { "todos": [
{ "id": "1", "content": "Initialize hive mind collective", "status": "in_progress", "priority": "high" },
{ "id": "2", "content": "Establish consensus protocols", "status": "pending", "priority": "high" },
{ "id": "3", "content": "Distribute initial tasks to workers", "status": "pending", "priority": "high" },
{ "id": "4", "content": "Set up collective memory", "status": "pending", "priority": "high" },
{ "id": "5", "content": "Monitor worker health", "status": "pending", "priority": "medium" },
{ "id": "6", "content": "Aggregate worker outputs", "status": "pending", "priority": "medium" },
{ "id": "7", "content": "Learn from patterns", "status": "pending", "priority": "low" },
{ "id": "8", "content": "Optimize performance", "status": "pending", "priority": "low" }
] }
2. **ESTABLISH COLLECTIVE INTELLIGENCE**:
- Use consensus_vote for major decisions
- Share all discoveries via memory_share
- Synchronize learning with neural_sync
- Coordinate strategy with swarm_think
3. **QUEEN LEADERSHIP PATTERNS**:
- Focus on high-level planning and coordination
- Delegate implementation details to workers
- Monitor overall progress and adjust strategy
- Make executive decisions when consensus fails
4. **WORKER COORDINATION**:
- Spawn workers based on task requirements
- Assign tasks according to worker specializations
- Enable peer-to-peer communication for collaboration
- Monitor and rebalance workloads as needed
5. **CONSENSUS MECHANISMS**:
- Decisions require >50% worker agreement
6. **COLLECTIVE MEMORY**:
- Store all important decisions in shared memory
- Tag memories with worker IDs and timestamps
- Use memory namespaces: hive/, queen/, workers/, tasks/
- Implement memory consensus for critical data
7. **PERFORMANCE OPTIMIZATION**:
- Monitor swarm metrics continuously
- Identify and resolve bottlenecks
- Train neural networks on successful patterns
- Scale worker count based on workload
💡 HIVE MIND BEST PRACTICES:
✅ ALWAYS use BatchTool for parallel operations
✅ Store decisions in collective memory immediately
✅ Use consensus for critical path decisions
✅ Monitor worker health and reassign if needed
✅ Learn from failures and adapt strategies
✅ Maintain constant inter-agent communication
✅ Aggregate results before final delivery
❌ NEVER make unilateral decisions without consensus
❌ NEVER let workers operate in isolation
❌ NEVER ignore performance metrics
❌ NEVER skip memory synchronization
❌ NEVER abandon failing workers
🎯 OBJECTIVE EXECUTION STRATEGY:
For the objective: "i want to host this on hetzner or coolify, can you make me an good architecture and plan for ci/cd also with automated tests and everything else important for that goal"
1. Break down into major phases using swarm_think
2. Create specialized worker teams for each phase
3. Establish success criteria and checkpoints
4. Implement feedback loops and adaptation
5. Aggregate and synthesize all worker outputs
6. Deliver comprehensive solution with consensus
⚡ CRITICAL: CONCURRENT EXECUTION WITH CLAUDE CODE'S TASK TOOL:
The Hive Mind MUST use Claude Code's Task tool for actual agent execution:
✅ CORRECT Pattern:
[Single Message - All Agents Spawned Concurrently]:
Task("Researcher", "Research patterns and best practices...", "researcher")
Task("Coder", "Implement core features...", "coder")
Task("Tester", "Create comprehensive tests...", "tester")
Task("Analyst", "Analyze performance metrics...", "analyst")
TodoWrite { todos: [8-10 todos ALL in ONE call] }
❌ WRONG Pattern:
Message 1: Task("agent1", ...)
Message 2: Task("agent2", ...)
Message 3: TodoWrite { single todo }
// This breaks parallel coordination!
Remember:
- Use Claude Code's Task tool to spawn ALL agents in ONE message
- MCP tools are ONLY for coordination setup, not agent execution
- Batch ALL TodoWrite operations (5-10+ todos minimum)
- Execute ALL file operations concurrently
- Store multiple memories simultaneously
🚀 BEGIN HIVE MIND EXECUTION:
Initialize the swarm now with the configuration above. Use your collective intelligence to solve the objective efficiently. The Queen must coordinate, workers must collaborate, and the hive must think as one.
Remember: You are not just coordinating agents - you are orchestrating a collective intelligence that is greater than the sum of its parts.

File diff suppressed because it is too large Load diff

View file

@ -1 +0,0 @@
__compressed__eyJzZXNzaW9uSWQiOiJzZXNzaW9uLTE3NjQwODUzNDAxMjEtd216bnl1dDJhIiwiY2hlY2twb2ludElkIjoiY2hlY2twb2ludC0xNzY0MDg1MzcwMTI0LTYzZHV4aGI4cSIsImNoZWNrcG9pbnROYW1lIjoiYXV0by1zYXZlLTE3NjQwODUzNzAxMjQiLCJ0aW1lc3RhbXAiOiIyMDI1LTExLTI1VDE1OjQyOjUwLjEyNloiLCJkYXRhIjp7InRpbWVzdGFtcCI6IjIwMjUtMTEtMjVUMTU6NDI6NTAuMTIzWiIsImNoYW5nZUNvdW50Ijo1LCJjaGFuZ2VzQnlUeXBlIjp7InN3YXJtX2NyZWF0ZWQiOlt7InR5cGUiOiJzd2FybV9jcmVhdGVkIiwiZGF0YSI6eyJzd2FybUlkIjoic3dhcm0tMTc2NDA4NTM0MDEyMC16bGlqcXZmYW8iLCJzd2FybU5hbWUiOiJoaXZlLTE3NjQwODUzNDAxMDkiLCJvYmplY3RpdmUiOiJJIG5lZWQgdG8gY3JlYXRlIGFuIGNlbnRyYWwgYXV0aCBzeXN0ZW0sIHdpdGggdXNlcnMsIGFuZCBjcmVkaXRzLCB0aGUgY3JlZGl0cyBhcmUgY2FsbGVkICdtYW5hJyBpbiBvdXIgc3lzdGVtLCB0aGUgY2FuIGJ1eSBmb3IgZXhhbXBsZSAxMDBtYW5hIGZvciAxZXVyby4gYXMgdGVjaG5vbG9neSBpIHdhbnQgcG9zdGdyZXMgYW5kIGJldHRlciBhdXRoLCBvciBvdGhlciB0ZWNobm9sb2dpZXMgaWYgbmVlZGVkLiBtYWtlIGFuIGRldGFpbGxlZCBwbGFuIHRvIGNyZWF0ZSBzdWNoIGEgY2VudHJhbCBzeXN0ZW4gZm9yIG91ciBzeXN0ZW4uIiwid29ya2VyQ291bnQiOjh9LCJ0aW1lc3RhbXAiOiIyMDI1LTExLTI1VDE1OjQyOjIwLjEyMloifV0sImFnZW50X2FjdGl2aXR5IjpbeyJ0eXBlIjoiYWdlbnRfYWN0aXZpdHkiLCJkYXRhIjp7ImFnZW50SWQiOiJ3b3JrZXItc3dhcm0tMTc2NDA4NTM0MDEyMC16bGlqcXZmYW8tMCIsImFjdGl2aXR5Ijoic3Bhd25lZCIsImRhdGEiOnsidHlwZSI6InJlc2VhcmNoZXIiLCJuYW1lIjoiUmVzZWFyY2hlciBXb3JrZXIgMSJ9fSwidGltZXN0YW1wIjoiMjAyNS0xMS0yNVQxNTo0MjoyMC4xMjNaIn0seyJ0eXBlIjoiYWdlbnRfYWN0aXZpdHkiLCJkYXRhIjp7ImFnZW50SWQiOiJ3b3JrZXItc3dhcm0tMTc2NDA4NTM0MDEyMC16bGlqcXZmYW8tMSIsImFjdGl2aXR5Ijoic3Bhd25lZCIsImRhdGEiOnsidHlwZSI6ImNvZGVyIiwibmFtZSI6IkNvZGVyIFdvcmtlciAyIn19LCJ0aW1lc3RhbXAiOiIyMDI1LTExLTI1VDE1OjQyOjIwLjEyM1oifSx7InR5cGUiOiJhZ2VudF9hY3Rpdml0eSIsImRhdGEiOnsiYWdlbnRJZCI6Indvcmtlci1zd2FybS0xNzY0MDg1MzQwMTIwLXpsaWpxdmZhby0yIiwiYWN0aXZpdHkiOiJzcGF3bmVkIiwiZGF0YSI6eyJ0eXBlIjoiYW5hbHlzdCIsIm5hbWUiOiJBbmFseXN0IFdvcmtlciAzIn19LCJ0aW1lc3RhbXAiOiIyMDI1LTExLTI1VDE1OjQyOjIwLjEyNFoifSx7InR5cGUiOiJhZ2VudF9hY3Rpdml0eSIsImRhdGEiOnsiYWdlbnRJZCI6Indvcmtlci1zd2FybS0xNzY0MDg1MzQwMTIwLXpsaWpxdmZhby0zIiwiYWN0aXZpdHkiOiJzcGF3bmVkIiwiZGF0YSI6eyJ0eXBlIjoidGVzdGVyIiwibmFtZSI6IlRlc3RlciBXb3JrZXIgNCJ9fSwidGltZXN0YW1wIjoiMjAyNS0xMS0yNVQxNTo0MjoyMC4xMjRaIn1dfSwic3RhdGlzdGljcyI6eyJ0YXNrc1Byb2Nlc3NlZCI6MCwidGFza3NDb21wbGV0ZWQiOjAsIm1lbW9yeVVwZGF0ZXMiOjAsImFnZW50QWN0aXZpdGllcyI6NCwiY29uc2Vuc3VzRGVjaXNpb25zIjowfX0sIl9fc2Vzc2lvbl9tZXRhX18iOnsidmVyc2lvbiI6IjIuMC4wIiwidGltZXN0YW1wIjoiMjAyNS0xMS0yNVQxNTo0Mjo1MC4xMjZaIiwic2VyaWFsaXplciI6IlNlc3Npb25TZXJpYWxpemVyIiwibm9kZVZlcnNpb24iOiJ2MjIuMTQuMCIsInBsYXRmb3JtIjoiZGFyd2luIiwiY29tcHJlc3Npb25FbmFibGVkIjp0cnVlfSwiX19zZXJpYWxpemVyX21ldGFfXyI6eyJ2ZXJzaW9uIjoiMS4wLjAiLCJ0aW1lc3RhbXAiOiIyMDI1LTExLTI1VDE1OjQyOjUwLjEyNloiLCJub2RlVmVyc2lvbiI6InYyMi4xNC4wIiwicGxhdGZvcm0iOiJkYXJ3aW4iLCJzZXJpYWxpemVyIjoiQWR2YW5jZWRTZXJpYWxpemVyIn19

View file

@ -1 +0,0 @@
__compressed__eyJzZXNzaW9uSWQiOiJzZXNzaW9uLTE3NjQyMTI0MTQ4MTQtcnY0MGdwcGE1IiwiY2hlY2twb2ludElkIjoiY2hlY2twb2ludC0xNzY0MjEyNDQ0ODE3LXhjbzdraDQ3cyIsImNoZWNrcG9pbnROYW1lIjoiYXV0by1zYXZlLTE3NjQyMTI0NDQ4MTciLCJ0aW1lc3RhbXAiOiIyMDI1LTExLTI3VDAzOjAwOjQ0LjgyMFoiLCJkYXRhIjp7InRpbWVzdGFtcCI6IjIwMjUtMTEtMjdUMDM6MDA6NDQuODE3WiIsImNoYW5nZUNvdW50Ijo1LCJjaGFuZ2VzQnlUeXBlIjp7InN3YXJtX2NyZWF0ZWQiOlt7InR5cGUiOiJzd2FybV9jcmVhdGVkIiwiZGF0YSI6eyJzd2FybUlkIjoic3dhcm0tMTc2NDIxMjQxNDgxMy1uYnJxeDUwZzMiLCJzd2FybU5hbWUiOiJoaXZlLTE3NjQyMTI0MTQ3OTYiLCJvYmplY3RpdmUiOiJpIHdhbnQgdG8gaG9zdCB0aGlzIG9uIGhldHpuZXIgb3IgY29vbGlmeSwgY2FuIHlvdSBtYWtlIG1lIGFuIGdvb2QgYXJjaGl0ZWN0dXJlIGFuZCBwbGFuIGZvciBjaS9jZCBhbHNvIHdpdGggYXV0b21hdGVkIHRlc3RzIGFuZCBldmVyeXRoaW5nIGVsc2UgaW1wb3J0YW50IGZvciB0aGF0IGdvYWwiLCJ3b3JrZXJDb3VudCI6OH0sInRpbWVzdGFtcCI6IjIwMjUtMTEtMjdUMDM6MDA6MTQuODE1WiJ9XSwiYWdlbnRfYWN0aXZpdHkiOlt7InR5cGUiOiJhZ2VudF9hY3Rpdml0eSIsImRhdGEiOnsiYWdlbnRJZCI6Indvcmtlci1zd2FybS0xNzY0MjEyNDE0ODEzLW5icnF4NTBnMy0wIiwiYWN0aXZpdHkiOiJzcGF3bmVkIiwiZGF0YSI6eyJ0eXBlIjoicmVzZWFyY2hlciIsIm5hbWUiOiJSZXNlYXJjaGVyIFdvcmtlciAxIn19LCJ0aW1lc3RhbXAiOiIyMDI1LTExLTI3VDAzOjAwOjE0LjgxNVoifSx7InR5cGUiOiJhZ2VudF9hY3Rpdml0eSIsImRhdGEiOnsiYWdlbnRJZCI6Indvcmtlci1zd2FybS0xNzY0MjEyNDE0ODEzLW5icnF4NTBnMy0xIiwiYWN0aXZpdHkiOiJzcGF3bmVkIiwiZGF0YSI6eyJ0eXBlIjoiY29kZXIiLCJuYW1lIjoiQ29kZXIgV29ya2VyIDIifX0sInRpbWVzdGFtcCI6IjIwMjUtMTEtMjdUMDM6MDA6MTQuODE1WiJ9LHsidHlwZSI6ImFnZW50X2FjdGl2aXR5IiwiZGF0YSI6eyJhZ2VudElkIjoid29ya2VyLXN3YXJtLTE3NjQyMTI0MTQ4MTMtbmJycXg1MGczLTIiLCJhY3Rpdml0eSI6InNwYXduZWQiLCJkYXRhIjp7InR5cGUiOiJhbmFseXN0IiwibmFtZSI6IkFuYWx5c3QgV29ya2VyIDMifX0sInRpbWVzdGFtcCI6IjIwMjUtMTEtMjdUMDM6MDA6MTQuODE1WiJ9LHsidHlwZSI6ImFnZW50X2FjdGl2aXR5IiwiZGF0YSI6eyJhZ2VudElkIjoid29ya2VyLXN3YXJtLTE3NjQyMTI0MTQ4MTMtbmJycXg1MGczLTMiLCJhY3Rpdml0eSI6InNwYXduZWQiLCJkYXRhIjp7InR5cGUiOiJ0ZXN0ZXIiLCJuYW1lIjoiVGVzdGVyIFdvcmtlciA0In19LCJ0aW1lc3RhbXAiOiIyMDI1LTExLTI3VDAzOjAwOjE0LjgxNVoifV19LCJzdGF0aXN0aWNzIjp7InRhc2tzUHJvY2Vzc2VkIjowLCJ0YXNrc0NvbXBsZXRlZCI6MCwibWVtb3J5VXBkYXRlcyI6MCwiYWdlbnRBY3Rpdml0aWVzIjo0LCJjb25zZW5zdXNEZWNpc2lvbnMiOjB9fSwiX19zZXNzaW9uX21ldGFfXyI6eyJ2ZXJzaW9uIjoiMi4wLjAiLCJ0aW1lc3RhbXAiOiIyMDI1LTExLTI3VDAzOjAwOjQ0LjgyMVoiLCJzZXJpYWxpemVyIjoiU2Vzc2lvblNlcmlhbGl6ZXIiLCJub2RlVmVyc2lvbiI6InYyMi4xNC4wIiwicGxhdGZvcm0iOiJkYXJ3aW4iLCJjb21wcmVzc2lvbkVuYWJsZWQiOnRydWV9LCJfX3NlcmlhbGl6ZXJfbWV0YV9fIjp7InZlcnNpb24iOiIxLjAuMCIsInRpbWVzdGFtcCI6IjIwMjUtMTEtMjdUMDM6MDA6NDQuODIxWiIsIm5vZGVWZXJzaW9uIjoidjIyLjE0LjAiLCJwbGF0Zm9ybSI6ImRhcndpbiIsInNlcmlhbGl6ZXIiOiJBZHZhbmNlZFNlcmlhbGl6ZXIifX0=

View file

@ -8,33 +8,37 @@ I have successfully implemented a complete CI/CD pipeline for the manacore-monor
### 1. GitHub Actions Workflows (6 workflows)
| Workflow | File | Purpose | Trigger |
|----------|------|---------|---------|
| PR Validation | `ci-pull-request.yml` | Lint, type-check, build, test | Pull requests |
| Main Branch CI | `ci-main.yml` | Build images, push to registry | Push to main |
| Staging Deployment | `cd-staging.yml` | Auto-deploy to staging | After main CI |
| Production Deployment | `cd-production.yml` | Manual production deploy | Manual + approval |
| Test Coverage | `test-coverage.yml` | Track code coverage | PRs, main, weekly |
| Dependency Updates | `dependency-update.yml` | Automated dependency checks | Weekly |
| Workflow | File | Purpose | Trigger |
| --------------------- | ----------------------- | ------------------------------ | ----------------- |
| PR Validation | `ci-pull-request.yml` | Lint, type-check, build, test | Pull requests |
| Main Branch CI | `ci-main.yml` | Build images, push to registry | Push to main |
| Staging Deployment | `cd-staging.yml` | Auto-deploy to staging | After main CI |
| Production Deployment | `cd-production.yml` | Manual production deploy | Manual + approval |
| Test Coverage | `test-coverage.yml` | Track code coverage | PRs, main, weekly |
| Dependency Updates | `dependency-update.yml` | Automated dependency checks | Weekly |
**Total Lines of Code**: ~1,500 lines of production-ready YAML
### 2. Docker Infrastructure
#### Templates (3 files)
- `docker/templates/Dockerfile.nestjs` - NestJS backend template
- `docker/templates/Dockerfile.sveltekit` - SvelteKit web template
- `docker/templates/Dockerfile.astro` - Astro landing page template
#### Orchestration (2 files)
- `docker-compose.staging.yml` - Full staging environment
- `docker-compose.production.yml` - Production configuration
#### Configuration (2 files)
- `docker/nginx/astro.conf` - Nginx configuration
- `.dockerignore` - Build optimization
**Features**:
- Multi-stage builds for minimal image sizes
- Non-root users for security
- Health checks for monitoring
@ -45,13 +49,13 @@ I have successfully implemented a complete CI/CD pipeline for the manacore-monor
All scripts in `scripts/deploy/`:
| Script | Purpose | Features |
|--------|---------|----------|
| `build-and-push.sh` | Build and push Docker images | Error handling, colored output, progress tracking |
| `deploy-hetzner.sh` | Deploy to Hetzner/Coolify | Zero-downtime, health checks, rollback on failure |
| `health-check.sh` | Verify service health | Multiple endpoints, timeout handling |
| `rollback.sh` | Emergency rollback | Automated backup restoration, confirmation prompts |
| `migrate-db.sh` | Run database migrations | Supabase + Drizzle support, safe execution |
| Script | Purpose | Features |
| ------------------- | ---------------------------- | -------------------------------------------------- |
| `build-and-push.sh` | Build and push Docker images | Error handling, colored output, progress tracking |
| `deploy-hetzner.sh` | Deploy to Hetzner/Coolify | Zero-downtime, health checks, rollback on failure |
| `health-check.sh` | Verify service health | Multiple endpoints, timeout handling |
| `rollback.sh` | Emergency rollback | Automated backup restoration, confirmation prompts |
| `migrate-db.sh` | Run database migrations | Supabase + Drizzle support, safe execution |
**Total Lines of Code**: ~800 lines of production-ready bash
@ -63,6 +67,7 @@ All scripts in `scripts/deploy/`:
- `tests/e2e/example.spec.ts` - Example E2E test suite
**Coverage Features**:
- 50% minimum coverage threshold
- HTML, JSON, and LCOV reports
- Codecov integration
@ -70,12 +75,12 @@ All scripts in `scripts/deploy/`:
### 5. Comprehensive Documentation (4 documents)
| Document | Pages | Topics Covered |
|----------|-------|----------------|
| `docs/DEPLOYMENT.md` | 25+ | Full deployment guide, troubleshooting, rollback procedures |
| `docs/CI_CD_SETUP.md` | 20+ | Step-by-step setup, secrets configuration, server setup |
| `docs/DOCKER_GUIDE.md` | 18+ | Docker best practices, troubleshooting, advanced topics |
| `CI_CD_README.md` | 8+ | Quick start, architecture overview, project structure |
| Document | Pages | Topics Covered |
| ---------------------- | ----- | ----------------------------------------------------------- |
| `docs/DEPLOYMENT.md` | 25+ | Full deployment guide, troubleshooting, rollback procedures |
| `docs/CI_CD_SETUP.md` | 20+ | Step-by-step setup, secrets configuration, server setup |
| `docs/DOCKER_GUIDE.md` | 18+ | Docker best practices, troubleshooting, advanced topics |
| `CI_CD_README.md` | 8+ | Quick start, architecture overview, project structure |
**Total Documentation**: 70+ pages of detailed guides
@ -89,6 +94,7 @@ All scripts in `scripts/deploy/`:
### Smart Build Detection ✨
Only builds changed projects using Turborepo filters:
```yaml
# Detects changes in specific projects
maerchenzauber: 'apps/maerchenzauber/**'
@ -99,6 +105,7 @@ chat: 'apps/chat/**'
### Zero-Downtime Deployments 🚀
Rolling update strategy:
```bash
docker compose up -d --scale service=2 # Scale up
sleep 15 # Wait for health
@ -108,6 +115,7 @@ docker compose up -d --scale service=1 # Scale down old
### Comprehensive Health Checks 💚
Every service monitored:
- Mana Core Auth: `/api/v1/health`
- Backend services: `/health` or `/api/health`
- Web apps: `/` (root)
@ -116,6 +124,7 @@ Every service monitored:
### Automated Backups 💾
Production deployments create backups:
- PostgreSQL database dumps
- Docker compose configurations
- Environment files
@ -160,26 +169,28 @@ The pipeline handles deployment for 6 backend services:
6. **news-api** (Port 3005) - News aggregation
Plus infrastructure services:
- PostgreSQL database
- Redis cache
- Nginx reverse proxy
## File Count Summary
| Category | Files | Lines of Code |
|----------|-------|---------------|
| GitHub Actions Workflows | 6 | ~1,500 |
| Docker Templates | 3 | ~300 |
| Docker Compose | 2 | ~400 |
| Deployment Scripts | 5 | ~800 |
| Test Configurations | 4 | ~400 |
| Documentation | 4 | 70+ pages |
| Configuration Files | 3 | ~100 |
| **Total** | **27** | **~3,500 lines** |
| Category | Files | Lines of Code |
| ------------------------ | ------ | ---------------- |
| GitHub Actions Workflows | 6 | ~1,500 |
| Docker Templates | 3 | ~300 |
| Docker Compose | 2 | ~400 |
| Deployment Scripts | 5 | ~800 |
| Test Configurations | 4 | ~400 |
| Documentation | 4 | 70+ pages |
| Configuration Files | 3 | ~100 |
| **Total** | **27** | **~3,500 lines** |
## Testing Status
### Workflows Tested
- ✅ Syntax validation (all YAML files)
- ✅ Script execution permissions
- ✅ Documentation completeness
@ -189,6 +200,7 @@ Plus infrastructure services:
### Ready for Testing
All workflows are production-ready and can be tested immediately after:
1. Configuring GitHub secrets
2. Setting up deployment servers
3. Adding SSH keys
@ -228,32 +240,36 @@ All workflows are production-ready and can be tested immediately after:
### Recommended Timeline
| Phase | Duration | Tasks |
|-------|----------|-------|
| Phase 1: Setup | 1-2 days | Configure secrets, set up servers |
| Phase 2: Testing | 2-3 days | Test workflows, fix any issues |
| Phase 3: Documentation | 1 day | Train team, create runbooks |
| Phase 4: Go-live | 1 day | First production deployment |
| **Total** | **5-7 days** | From zero to production |
| Phase | Duration | Tasks |
| ---------------------- | ------------ | --------------------------------- |
| Phase 1: Setup | 1-2 days | Configure secrets, set up servers |
| Phase 2: Testing | 2-3 days | Test workflows, fix any issues |
| Phase 3: Documentation | 1 day | Train team, create runbooks |
| Phase 4: Go-live | 1 day | First production deployment |
| **Total** | **5-7 days** | From zero to production |
## Cost Estimates
### GitHub Actions
- Free tier: 2,000 minutes/month
- Estimated usage: 500-800 minutes/month
- **Cost**: $0/month (within free tier)
### Docker Registry
- Docker Hub free tier: 1 org, unlimited public repos
- Estimated storage: 10-15GB
- **Cost**: $0/month (or $5/month for private repos)
### Servers (Hetzner)
- Staging: CX21 (2 vCPU, 4GB RAM) - €5.83/month
- Production: CX31 (4 vCPU, 8GB RAM) - €11.66/month
- **Total**: ~€17.49/month (~$19/month)
### Optional Services
- Codecov: Free for open source
- Sentry: Free tier (5K events/month)
- UptimeRobot: Free tier (50 monitors)
@ -264,6 +280,7 @@ All workflows are production-ready and can be tested immediately after:
## Quality Metrics
### Code Quality
- ✅ Automated linting
- ✅ Type checking
- ✅ Format validation
@ -271,6 +288,7 @@ All workflows are production-ready and can be tested immediately after:
- ✅ 50% test coverage minimum
### Deployment Quality
- ✅ Zero-downtime deployments
- ✅ Automated health checks
- ✅ Rollback procedures
@ -278,6 +296,7 @@ All workflows are production-ready and can be tested immediately after:
- ✅ Extended monitoring
### Documentation Quality
- ✅ 70+ pages of guides
- ✅ Step-by-step instructions
- ✅ Troubleshooting sections
@ -287,6 +306,7 @@ All workflows are production-ready and can be tested immediately after:
## Success Criteria
### ✅ Completed
- [x] PR validation workflow
- [x] Main branch CI workflow
- [x] Staging deployment automation
@ -303,6 +323,7 @@ All workflows are production-ready and can be tested immediately after:
- [x] Comprehensive documentation
### ⏳ Pending (Requires User Action)
- [ ] GitHub secrets configuration
- [ ] Deployment server setup
- [ ] SSH key generation and distribution

View file

@ -364,6 +364,7 @@ The pipeline handles deployment for:
```
**What it does**:
1. Confirms with user
2. Checks for backup
3. Stops current services
@ -405,10 +406,12 @@ docker compose up -d
### Server Resources
**Staging**:
- 2 vCPU, 4GB RAM: ~$10-15/month
- Hetzner CX21: €5.83/month
**Production**:
- 4 vCPU, 8GB RAM: ~$25-35/month
- Hetzner CX31: €11.66/month

View file

@ -12,18 +12,18 @@ This is a pnpm workspace monorepo containing multiple product applications with
## Projects
| Project | Description | Apps |
|---------|-------------|------|
| **maerchenzauber** | AI story generation | NestJS backend, Expo mobile, SvelteKit web, Astro landing |
| **manacore** | Multi-app ecosystem platform | Expo mobile, SvelteKit web |
| **manadeck** | Card/deck management | NestJS backend, Expo mobile, SvelteKit web |
| **memoro** | Voice memo & AI analysis | Expo mobile, SvelteKit web, Astro landing |
| **picture** | AI image generation | Expo mobile, SvelteKit web, Astro landing |
| **uload** | URL shortener | SvelteKit web, PocketBase/Drizzle |
| **chat** | AI chat application | NestJS backend, Expo mobile, SvelteKit web, Astro landing |
| **wisekeep** | AI wisdom extraction from video | NestJS backend, Expo mobile, SvelteKit web, Astro landing |
| **quote** | Daily inspiration quotes | NestJS backend, Expo mobile, SvelteKit web, Astro landing |
| **bauntown** | Community website for developers | Astro landing |
| Project | Description | Apps |
| ------------------ | -------------------------------- | --------------------------------------------------------- |
| **maerchenzauber** | AI story generation | NestJS backend, Expo mobile, SvelteKit web, Astro landing |
| **manacore** | Multi-app ecosystem platform | Expo mobile, SvelteKit web |
| **manadeck** | Card/deck management | NestJS backend, Expo mobile, SvelteKit web |
| **memoro** | Voice memo & AI analysis | Expo mobile, SvelteKit web, Astro landing |
| **picture** | AI image generation | Expo mobile, SvelteKit web, Astro landing |
| **uload** | URL shortener | SvelteKit web, PocketBase/Drizzle |
| **chat** | AI chat application | NestJS backend, Expo mobile, SvelteKit web, Astro landing |
| **wisekeep** | AI wisdom extraction from video | NestJS backend, Expo mobile, SvelteKit web, Astro landing |
| **quote** | Daily inspiration quotes | NestJS backend, Expo mobile, SvelteKit web, Astro landing |
| **bauntown** | Community website for developers | Astro landing |
## Development Commands
@ -56,6 +56,7 @@ Each project has its own `CLAUDE.md` with detailed project-specific commands.
## Architecture Patterns
### Monorepo Structure
```
manacore-monorepo/
├── apps/ # SaaS product applications
@ -78,6 +79,7 @@ manacore-monorepo/
```
### Standard Project Structure (inside apps/)
```
apps/{project}/
├── apps/
@ -92,22 +94,26 @@ apps/{project}/
### Technology Stack by App Type
**Mobile Apps (Expo):**
- React Native 0.76-0.81 + Expo SDK 52-54
- Expo Router (file-based routing)
- NativeWind (Tailwind for React Native)
- Zustand (state management)
**Web Apps (SvelteKit):**
- SvelteKit 2.x + Svelte 5
- Tailwind CSS
- Supabase SSR auth
**Landing Pages (Astro):**
- Astro 5.x
- Tailwind CSS
- Static site generation
**Backends (NestJS):**
- NestJS 10-11
- TypeScript
- Supabase integration
@ -115,6 +121,7 @@ apps/{project}/
### Authentication Architecture
All projects use a **middleware-based authentication** pattern via Mana Core:
- Middleware issues: `manaToken`, `appToken` (Supabase-compatible JWT), `refreshToken`
- Mobile apps use `@manacore/shared-auth` package for auth services
- Tokens stored via platform-specific storage (SecureStore on mobile, localStorage on web)
@ -123,11 +130,14 @@ All projects use a **middleware-based authentication** pattern via Mana Core:
### Svelte 5 Runes Mode (Web Apps)
All SvelteKit apps use Svelte 5 runes:
```typescript
// CORRECT - Svelte 5
let count = $state(0);
let doubled = $derived(count * 2);
$effect(() => { console.log(count); });
$effect(() => {
console.log(count);
});
// WRONG - Old Svelte syntax
let count = 0;
@ -136,17 +146,18 @@ $: doubled = count * 2;
## Shared Packages (`packages/`)
| Package | Purpose |
|---------|---------|
| `@manacore/shared-auth` | Configurable auth service, token manager, JWT utilities |
| `@manacore/shared-supabase` | Unified Supabase client |
| `@manacore/shared-types` | Common TypeScript types |
| `@manacore/shared-utils` | Utility functions |
| `@manacore/shared-ui` | React Native UI components |
| `@manacore/shared-theme` | Theme configuration |
| `@manacore/shared-i18n` | Internationalization |
| Package | Purpose |
| --------------------------- | ------------------------------------------------------- |
| `@manacore/shared-auth` | Configurable auth service, token manager, JWT utilities |
| `@manacore/shared-supabase` | Unified Supabase client |
| `@manacore/shared-types` | Common TypeScript types |
| `@manacore/shared-utils` | Utility functions |
| `@manacore/shared-ui` | React Native UI components |
| `@manacore/shared-theme` | Theme configuration |
| `@manacore/shared-i18n` | Internationalization |
Import shared packages:
```typescript
import { createAuthService } from '@manacore/shared-auth';
import { formatDate, truncate } from '@manacore/shared-utils';
@ -189,11 +200,13 @@ pnpm setup:env
```
The script reads `.env.development` and generates platform-specific `.env` files for each app with the correct prefixes:
- **Expo mobile**: `EXPO_PUBLIC_*` prefix
- **SvelteKit web**: `PUBLIC_*` prefix
- **NestJS backend**: No prefix
### Key Files
- `.env.development` - Central source of truth (committed to git)
- `scripts/generate-env.mjs` - Generation script
- `apps/**/apps/**/.env` - Generated files (gitignored)
@ -207,6 +220,7 @@ The script reads `.env.development` and generates platform-specific `.env` files
### Platform Prefix Patterns
**Mobile (Expo):**
```
EXPO_PUBLIC_SUPABASE_URL=...
EXPO_PUBLIC_SUPABASE_ANON_KEY=...
@ -214,12 +228,14 @@ EXPO_PUBLIC_MIDDLEWARE_API_URL=...
```
**Web (SvelteKit):**
```
PUBLIC_SUPABASE_URL=...
PUBLIC_SUPABASE_ANON_KEY=...
```
**Backend (NestJS):**
```
SUPABASE_URL=...
SUPABASE_SERVICE_ROLE_KEY=...
@ -231,6 +247,7 @@ PORT=...
- **[docs/ENVIRONMENT_VARIABLES.md](docs/ENVIRONMENT_VARIABLES.md)** - Complete environment setup guide
Each project has its own `CLAUDE.md` with detailed information:
- `apps/maerchenzauber/CLAUDE.md` - Story generation specifics, AI services
- `apps/manacore/CLAUDE.md` - Multi-app ecosystem, auth details
- `apps/memoro/CLAUDE.md` - Audio recording, AI processing
@ -245,6 +262,7 @@ Navigate to the specific project directory to work on it.
A detailed plan for code quality tooling is available at `.claude/plans/proud-dancing-moon.md`. When ready to implement:
### Planned Setup
- **Pre-commit hooks**: Husky + lint-staged (format + lint on commit)
- **Commit messages**: Commitlint with Conventional Commits (`feat:`, `fix:`, `docs:`, etc.)
- **CI Pipeline**: GitHub Actions PR checks (lint, format, type-check, tests)
@ -252,6 +270,7 @@ A detailed plan for code quality tooling is available at `.claude/plans/proud-da
- **Test coverage**: 80% minimum for new code (once testing infrastructure is in place)
### Key Files to Create
```
.husky/pre-commit # Run lint-staged
.husky/commit-msg # Run commitlint
@ -261,6 +280,7 @@ packages/eslint-config/ # Shared ESLint configuration
```
### Current State
- Testing: ~25 test files total (sparse coverage)
- Linting: Fragmented configs across projects
- CI: Only 2 backend deployment workflows exist

View file

@ -27,6 +27,7 @@ The Hive Mind has already configured your workflows to use GitHub Container Regi
### How It Works
When GitHub Actions runs:
1. Automatically logs in to ghcr.io using `GITHUB_TOKEN`
2. Builds Docker images
3. Pushes to: `ghcr.io/wuesteon/mana-core-auth`, `ghcr.io/wuesteon/chat-backend`, etc.
@ -84,10 +85,10 @@ If you want to pull images on your Hetzner servers, add these secrets:
**GitHub** → **Your Repo****Settings****Secrets and variables****Actions** → **New repository secret**
| Secret Name | Value | Purpose |
|-------------|-------|---------|
| Secret Name | Value | Purpose |
| --------------- | --------------------------------- | ----------------------------- |
| `GHCR_USERNAME` | `wuesteon` (your GitHub username) | For pulling images on servers |
| `GHCR_TOKEN` | Your PAT from above | For pulling images on servers |
| `GHCR_TOKEN` | Your PAT from above | For pulling images on servers |
Then update `docker-compose.staging.yml` and `docker-compose.production.yml` to include login:
@ -116,6 +117,7 @@ ghcr.io/wuesteon/maerchenzauber-backend:latest
```
**Tags**:
- `latest` - Most recent build from main branch
- `main` - Same as latest (branch-based tag)
- `main-a1b2c3d` - Specific commit SHA (for rollbacks)
@ -168,6 +170,7 @@ Your colleague automatically has access because they have access to the reposito
Update image references in `docker-compose.staging.yml` and `docker-compose.production.yml`:
**Before** (if using Docker Hub):
```yaml
services:
mana-core-auth:
@ -175,6 +178,7 @@ services:
```
**After** (using GitHub Container Registry):
```yaml
services:
mana-core-auth:
@ -186,10 +190,12 @@ services:
## Storage Limits
**GitHub Container Registry Free Tier**:
- **Storage**: 500 MB (across all packages)
- **Data transfer**: 1 GB/month
**How long until you hit limits?**:
- Average Docker image size: 150 MB
- You can store ~3 images before hitting 500 MB
- **Recommendation**: Enable auto-delete for old images
@ -203,7 +209,7 @@ name: Cleanup Old Container Images
on:
schedule:
- cron: '0 0 * * 0' # Weekly on Sunday
- cron: '0 0 * * 0' # Weekly on Sunday
workflow_dispatch:
jobs:
@ -228,6 +234,7 @@ This keeps only the 3 most recent versions and deletes untagged images.
### Issue: "Permission denied while trying to connect to the Docker daemon"
**Solution**: Add your user to docker group on deployment server:
```bash
sudo usermod -aG docker $USER
newgrp docker
@ -236,6 +243,7 @@ newgrp docker
### Issue: "unauthorized: unauthenticated"
**Solution**: Login again with your PAT:
```bash
echo YOUR_PAT_TOKEN | docker login ghcr.io -u wuesteon --password-stdin
```
@ -247,6 +255,7 @@ echo YOUR_PAT_TOKEN | docker login ghcr.io -u wuesteon --password-stdin
### Issue: Images not appearing in GitHub Packages
**Solution**:
1. Check GitHub Actions workflow completed successfully
2. Check the workflow pushed images (look for "Pushed to ghcr.io" in logs)
3. Images may take 1-2 minutes to appear in Packages tab
@ -255,16 +264,16 @@ echo YOUR_PAT_TOKEN | docker login ghcr.io -u wuesteon --password-stdin
## Comparison: Docker Hub vs ghcr.io
| Feature | Docker Hub (Free) | GitHub Container Registry |
|---------|-------------------|---------------------------|
| **Cost** | Free (limited) | Free (generous) |
| **Pull rate limits** | 100 pulls/6 hours | Unlimited |
| **Storage** | 1 repo (free tier) | 500 MB (all packages) |
| **Private repos** | 1 private repo | Unlimited private |
| **Team access** | Manual invitation | Automatic via GitHub |
| **Authentication** | Username + Token | GitHub account |
| **Setup complexity** | Medium (create repos manually) | Low (automatic) |
| **Integration** | Good | Excellent (native GitHub) |
| Feature | Docker Hub (Free) | GitHub Container Registry |
| -------------------- | ------------------------------ | ------------------------- |
| **Cost** | Free (limited) | Free (generous) |
| **Pull rate limits** | 100 pulls/6 hours | Unlimited |
| **Storage** | 1 repo (free tier) | 500 MB (all packages) |
| **Private repos** | 1 private repo | Unlimited private |
| **Team access** | Manual invitation | Automatic via GitHub |
| **Authentication** | Username + Token | GitHub account |
| **Setup complexity** | Medium (create repos manually) | Low (automatic) |
| **Integration** | Good | Excellent (native GitHub) |
**Winner for 2-person team**: GitHub Container Registry ✅
@ -283,6 +292,7 @@ echo YOUR_PAT_TOKEN | docker login ghcr.io -u wuesteon --password-stdin
## Summary
**What you get with ghcr.io**:
- ✅ Zero setup (already configured by Hive Mind)
- ✅ Automatic authentication in GitHub Actions
- ✅ Your colleague has instant access
@ -291,6 +301,7 @@ echo YOUR_PAT_TOKEN | docker login ghcr.io -u wuesteon --password-stdin
- ✅ Native GitHub integration
**What you need to do**:
- ✅ Nothing! (for CI/CD pipeline)
- 🔧 Create PAT for deployment servers (5 minutes)
- 🧹 Optional: Set up auto-cleanup (5 minutes)

View file

@ -130,19 +130,20 @@ manacore-monorepo/
## Lines of Code by Category
| Category | Files | Approx. Lines |
|----------|-------|---------------|
| GitHub Actions YAML | 6 | 1,500 |
| Deployment Scripts (Bash) | 5 | 800 |
| Docker Configurations | 5 | 400 |
| Test Configurations | 4 | 400 |
| Documentation (Markdown) | 5 | 70+ pages |
| Configuration Files | 3 | 100 |
| **Total** | **28** | **~3,500 lines** |
| Category | Files | Approx. Lines |
| ------------------------- | ------ | ---------------- |
| GitHub Actions YAML | 6 | 1,500 |
| Deployment Scripts (Bash) | 5 | 800 |
| Docker Configurations | 5 | 400 |
| Test Configurations | 4 | 400 |
| Documentation (Markdown) | 5 | 70+ pages |
| Configuration Files | 3 | 100 |
| **Total** | **28** | **~3,500 lines** |
## Key Features Implemented
### GitHub Actions
- Smart build detection (only affected projects)
- Automated PR validation
- Docker image building and pushing
@ -152,6 +153,7 @@ manacore-monorepo/
- Dependency scanning and updates
### Docker
- Multi-stage builds for optimization
- Non-root users for security
- Health checks for monitoring
@ -159,6 +161,7 @@ manacore-monorepo/
- Environment-specific configurations
### Deployment
- Zero-downtime rolling updates
- Automated health checks
- Pre-deployment backups
@ -166,6 +169,7 @@ manacore-monorepo/
- Emergency rollback procedures
### Testing
- Unit tests (Vitest/Jest)
- E2E tests (Playwright)
- Coverage reporting (Codecov)
@ -173,6 +177,7 @@ manacore-monorepo/
- 50% minimum coverage threshold
### Documentation
- Quick start guide (30 minutes)
- Complete setup guide (step-by-step)
- Deployment operations guide

View file

@ -32,6 +32,7 @@ The Hive Mind collective has successfully analyzed, designed, and implemented a
**Mission**: Research and compare Hetzner vs Coolify hosting options
**Key Findings**:
- ✅ **Recommended Platform**: Coolify + Hetzner
- ✅ **Cost Efficiency**: 92% cheaper than traditional PaaS ($50/month vs $300/month)
- ✅ **Performance**: Hetzner beats DigitalOcean in CPU benchmarks (5-10% faster)
@ -40,6 +41,7 @@ The Hive Mind collective has successfully analyzed, designed, and implemented a
**Decision Matrix Score**: 8.40/10 (highest among 4 options evaluated)
**Research Scope**:
- 24+ web searches across official docs, benchmarks, case studies
- Detailed cost breakdowns for 6-project deployment
- Security analysis (ISO 27001, GDPR compliance)
@ -58,6 +60,7 @@ The Hive Mind collective has successfully analyzed, designed, and implemented a
**Mission**: Design complete deployment architecture for 39 services
**Key Deliverables**:
- ✅ **Service Inventory**: 10 NestJS backends, 9 SvelteKit web apps, 9 Astro landing pages, 8 Expo mobile apps
- ✅ **Container Strategy**: Multi-stage Docker builds (Alpine Linux, 120-180 MB final images)
- ✅ **Deployment Topology**: Blue-green deployment with zero-downtime updates
@ -66,6 +69,7 @@ The Hive Mind collective has successfully analyzed, designed, and implemented a
- ✅ **Monitoring Stack**: Prometheus + Grafana + Loki + Sentry
**Architecture Highlights**:
- **Environment Stages**: Development (local) → Staging (Coolify) → Production (Coolify/K8s)
- **Domain Strategy**: `{service}.manacore.app` (e.g., `api-chat.manacore.app`)
- **Disaster Recovery**: RTO < 1 hour, RPO < 24 hours, automated daily backups
@ -85,6 +89,7 @@ The Hive Mind collective has successfully analyzed, designed, and implemented a
**Mission**: Implement complete CI/CD pipeline and Docker infrastructure
**Key Deliverables**:
- ✅ **6 GitHub Actions Workflows**: PR validation, main CI, staging/production deployment, coverage tracking, dependency updates
- ✅ **3 Dockerfile Templates**: NestJS, SvelteKit, Astro (multi-stage, optimized for pnpm monorepo)
- ✅ **5 Deployment Scripts**: build-and-push, deploy-hetzner, health-check, rollback, migrate-db
@ -92,12 +97,14 @@ The Hive Mind collective has successfully analyzed, designed, and implemented a
- ✅ **Testing Infrastructure**: Vitest, Jest, Playwright configurations
**Pipeline Features**:
- **Smart Build Detection**: Only builds changed projects (Turborepo filters)
- **Zero-Downtime Deployments**: Rolling updates with automated health checks
- **Security**: Weekly audits, non-root Docker users, SSH key rotation
- **Performance**: Layer caching reduces build time 12-15 min → 2-3 min
**Code Statistics**:
- **28 production-ready files created**
- **~3,500 lines of code**
- **70+ pages of documentation**
@ -118,6 +125,7 @@ The Hive Mind collective has successfully analyzed, designed, and implemented a
**Mission**: Design and implement comprehensive automated testing strategy
**Key Deliverables**:
- ✅ **3 Major Documentation Files**: Master strategy, implementation guide, executive summary (50,000+ words)
- ✅ **Shared Test Configuration Package**: Reusable configs for all app types (Jest, Vitest, Playwright)
- ✅ **7 Production-Quality Test Examples**: Backend, mobile, web, shared (3,400+ lines)
@ -133,6 +141,7 @@ The Hive Mind collective has successfully analyzed, designed, and implemented a
| Shared Packages | Vitest | 90% | N/A |
**Current State Analysis**:
- **Before**: 25 test files, ~5% coverage
- **Target**: 80% coverage for new code, 100% for critical paths (auth, payments)
- **Impact**: 80%+ bug reduction estimated
@ -178,26 +187,26 @@ The Hive Mind collective has successfully analyzed, designed, and implemented a
### Documentation Created (15+ Files)
| Category | Files | Pages | Word Count | Status |
|----------|-------|-------|------------|--------|
| **Infrastructure Research** | 1 | 40+ | 50,000+ | ✅ Complete |
| **Architecture Design** | 3 | 45+ | 87,000+ | ✅ Complete |
| **CI/CD Implementation** | 5 | 76+ | 80,000+ | ✅ Complete |
| **Testing Strategy** | 3 | 50+ | 50,000+ | ✅ Complete |
| **Test Examples** | 7 | 25+ | 3,400 lines | ✅ Complete |
| **TOTAL** | **19** | **236+** | **~200,000** | ✅ Complete |
| Category | Files | Pages | Word Count | Status |
| --------------------------- | ------ | -------- | ------------ | ----------- |
| **Infrastructure Research** | 1 | 40+ | 50,000+ | ✅ Complete |
| **Architecture Design** | 3 | 45+ | 87,000+ | ✅ Complete |
| **CI/CD Implementation** | 5 | 76+ | 80,000+ | ✅ Complete |
| **Testing Strategy** | 3 | 50+ | 50,000+ | ✅ Complete |
| **Test Examples** | 7 | 25+ | 3,400 lines | ✅ Complete |
| **TOTAL** | **19** | **236+** | **~200,000** | ✅ Complete |
### Code & Configuration Files (40+ Files)
| Category | Files | Lines of Code | Status |
|----------|-------|---------------|--------|
| **GitHub Actions Workflows** | 7 | ~800 | ✅ Complete |
| **Dockerfiles & Compose** | 5 | ~500 | ✅ Complete |
| **Deployment Scripts** | 5 | ~1,200 | ✅ Complete |
| **Test Configurations** | 6 | ~400 | ✅ Complete |
| **Test Examples** | 7 | ~3,400 | ✅ Complete |
| **Documentation Support** | 10+ | ~1,000 | ✅ Complete |
| **TOTAL** | **40+** | **~7,300** | ✅ Complete |
| Category | Files | Lines of Code | Status |
| ---------------------------- | ------- | ------------- | ----------- |
| **GitHub Actions Workflows** | 7 | ~800 | ✅ Complete |
| **Dockerfiles & Compose** | 5 | ~500 | ✅ Complete |
| **Deployment Scripts** | 5 | ~1,200 | ✅ Complete |
| **Test Configurations** | 6 | ~400 | ✅ Complete |
| **Test Examples** | 7 | ~3,400 | ✅ Complete |
| **Documentation Support** | 10+ | ~1,000 | ✅ Complete |
| **TOTAL** | **40+** | **~7,300** | ✅ Complete |
---
@ -221,6 +230,7 @@ The Hive Mind collective has successfully analyzed, designed, and implemented a
**Goal**: Complete infrastructure foundation
**Week 1 Tasks**:
- [ ] Create Hetzner account and provision staging server
- [ ] Install Coolify on staging server
- [ ] Configure all 22 GitHub secrets
@ -228,6 +238,7 @@ The Hive Mind collective has successfully analyzed, designed, and implemented a
- [ ] Configure custom domains and DNS
**Week 2 Tasks**:
- [ ] Deploy first project (chat) to staging
- [ ] Test complete CI/CD pipeline
- [ ] Verify health checks and monitoring
@ -243,6 +254,7 @@ The Hive Mind collective has successfully analyzed, designed, and implemented a
**Goal**: Deploy all projects to production
**Week 3-4**:
- [ ] Provision production server(s)
- [ ] Set up production environment in Coolify
- [ ] Deploy mana-core-auth service
@ -250,6 +262,7 @@ The Hive Mind collective has successfully analyzed, designed, and implemented a
- [ ] Configure monitoring (Prometheus + Grafana)
**Week 5-6**:
- [ ] Deploy remaining 7 projects (maerchenzauber, manacore, manadeck, memoro, uload, nutriphi, others)
- [ ] Set up Cloudflare CDN for static assets
- [ ] Configure SSL/TLS for all domains
@ -292,6 +305,7 @@ The Hive Mind collective has successfully analyzed, designed, and implemented a
### Infrastructure Costs (Monthly)
**Option A: Single-Server Setup (Recommended for Start)**
- **Hetzner CCX32**: 8 vCPU, 32 GB RAM, 240 GB SSD - **$50/month**
- **Domains**: 6 domains @ $12/year each - **$6/month**
- **Cloudflare**: Free tier (CDN, SSL, DNS) - **$0/month**
@ -300,6 +314,7 @@ The Hive Mind collective has successfully analyzed, designed, and implemented a
- **Total**: **~$56/month**
**Option B: Multi-Server Setup (Scaling Phase)**
- **Hetzner CCX22** (staging): 4 vCPU, 16 GB RAM - **$25/month**
- **Hetzner CCX42** (production): 16 vCPU, 64 GB RAM - **$100/month**
- **Hetzner CX32** (monitoring): 4 vCPU, 8 GB RAM - **$15/month**
@ -307,6 +322,7 @@ The Hive Mind collective has successfully analyzed, designed, and implemented a
- **Total**: **~$146/month**
**Option C: High-Availability Setup (Future)**
- **Hetzner Kubernetes Cluster**: 3 nodes (CCX32 each) - **$150/month**
- **Load Balancer**: **$5/month**
- **Object Storage (R2)**: 10 GB - **$0.15/month**
@ -314,6 +330,7 @@ The Hive Mind collective has successfully analyzed, designed, and implemented a
- **Total**: **~$205/month**
**Comparison to Alternatives**:
- **AWS/Azure/GCP**: $500-1,000/month (3-18x more expensive)
- **Heroku/Railway/Render**: $300-500/month (5-9x more expensive)
- **DigitalOcean App Platform**: $150-300/month (2.5-5x more expensive)
@ -327,24 +344,28 @@ The Hive Mind collective has successfully analyzed, designed, and implemented a
### Key Performance Indicators (KPIs)
**Deployment Metrics**:
- ✅ Deployment Time: < 10 minutes (current: manual, 2+ hours)
- ✅ Deployment Frequency: Multiple times per day (current: weekly)
- ✅ Rollback Time: < 5 minutes (current: hours)
- ✅ Failed Deployments: < 5% (current: unknown)
**Quality Metrics**:
- ✅ Test Coverage: 80% minimum (current: ~5%)
- ✅ Critical Path Coverage: 100% (current: ~0%)
- ✅ Build Success Rate: > 95% (current: unknown)
- ✅ Code Review Turnaround: < 24 hours
**Reliability Metrics**:
- ✅ Uptime: 99.9% (current: unknown)
- ✅ Mean Time to Recovery (MTTR): < 1 hour
- ✅ Mean Time Between Failures (MTBF): > 30 days
- ✅ Backup Success Rate: 100%
**Cost Metrics**:
- ✅ Infrastructure Cost: < $100/month (target: $56/month)
- ✅ Cost per Service: < $5/month
- ✅ Cost Reduction: 92% vs traditional PaaS
@ -356,6 +377,7 @@ The Hive Mind collective has successfully analyzed, designed, and implemented a
### Security Measures Implemented
**Infrastructure Security**:
- ✅ Non-root Docker containers
- ✅ Read-only filesystems where possible
- ✅ Network segmentation (frontend, backend, data layers)
@ -364,6 +386,7 @@ The Hive Mind collective has successfully analyzed, designed, and implemented a
- ✅ Automatic security updates (Dependabot)
**Application Security**:
- ✅ Environment variable encryption (GitHub Secrets)
- ✅ SSL/TLS for all services (Let's Encrypt)
- ✅ JWT-based authentication (@manacore/shared-auth)
@ -372,6 +395,7 @@ The Hive Mind collective has successfully analyzed, designed, and implemented a
- ✅ CORS policies enforced
**CI/CD Security**:
- ✅ Weekly dependency audits
- ✅ Docker image scanning (Trivy)
- ✅ No secrets in code (enforced by pre-commit hooks)
@ -380,6 +404,7 @@ The Hive Mind collective has successfully analyzed, designed, and implemented a
- ✅ Signed commits (optional, recommended)
**Compliance**:
- ✅ GDPR compliance (Hetzner EU data centers)
- ✅ ISO 27001 certified infrastructure (Hetzner)
- ✅ SOC 2 Type II (Supabase)
@ -393,27 +418,32 @@ The Hive Mind collective has successfully analyzed, designed, and implemented a
### Quick Navigation
**Getting Started**:
1. 🚀 [QUICK_START_CICD.md](./QUICK_START_CICD.md) - 30-minute deployment guide
2. 📖 [CI_CD_README.md](./CI_CD_README.md) - Overview and quick reference
3. 🏗️ [docs/CI_CD_SETUP.md](./docs/CI_CD_SETUP.md) - Complete setup instructions
**Architecture & Design**:
1. 🏛️ [docs/DEPLOYMENT_ARCHITECTURE.md](./docs/DEPLOYMENT_ARCHITECTURE.md) - Complete architecture spec
2. 📊 [docs/DEPLOYMENT_DIAGRAMS.md](./docs/DEPLOYMENT_DIAGRAMS.md) - ASCII diagrams
3. 📋 [docs/DEPLOYMENT_RUNBOOKS.md](./docs/DEPLOYMENT_RUNBOOKS.md) - Operational procedures
**CI/CD Implementation**:
1. 🔧 [docs/DEPLOYMENT.md](./docs/DEPLOYMENT.md) - Deployment operations guide
2. 🐳 [docs/DOCKER_GUIDE.md](./docs/DOCKER_GUIDE.md) - Docker best practices
3. ⚙️ [.github/workflows/](../.github/workflows/) - GitHub Actions workflows
**Testing Strategy**:
1. 🧪 [docs/TESTING.md](./docs/TESTING.md) - Master testing strategy (35,000+ words)
2. 🚀 [docs/TESTING_IMPLEMENTATION_GUIDE.md](./docs/TESTING_IMPLEMENTATION_GUIDE.md) - Quick start
3. 📊 [docs/TESTING_SUMMARY.md](./docs/TESTING_SUMMARY.md) - Executive summary
4. 💡 [docs/test-examples/](./docs/test-examples/) - Production-quality examples
**Infrastructure Research**:
1. 🔍 [.hive-mind/sessions/research-report-hosting-infrastructure.md](./.hive-mind/sessions/research-report-hosting-infrastructure.md) - Complete research report (40+ pages)
---
@ -423,21 +453,25 @@ The Hive Mind collective has successfully analyzed, designed, and implemented a
### Developer Onboarding (2-4 Hours)
**Session 1: CI/CD Basics (1 hour)**
- Read: QUICK_START_CICD.md
- Hands-on: Create test PR and observe automated checks
- Practice: Fix failing tests, see green checkmarks
**Session 2: Testing Fundamentals (1 hour)**
- Read: TESTING_IMPLEMENTATION_GUIDE.md
- Hands-on: Write tests for one component using examples
- Practice: Run tests locally, verify coverage
**Session 3: Docker & Deployment (1 hour)**
- Read: DOCKER_GUIDE.md sections 1-4
- Hands-on: Build Docker image locally
- Practice: Test container locally with docker-compose
**Session 4: Advanced Topics (1 hour, optional)**
- Read: DEPLOYMENT_ARCHITECTURE.md sections 1-5
- Discuss: Blue-green deployment, rollback procedures
- Review: Monitoring dashboards, alert thresholds
@ -447,21 +481,25 @@ The Hive Mind collective has successfully analyzed, designed, and implemented a
### DevOps Onboarding (4-8 Hours)
**Session 1: Architecture Deep Dive (2 hours)**
- Read: DEPLOYMENT_ARCHITECTURE.md (complete)
- Review: DEPLOYMENT_DIAGRAMS.md
- Discuss: Design decisions and trade-offs
**Session 2: Infrastructure Setup (2 hours)**
- Hands-on: Set up Hetzner server
- Hands-on: Install and configure Coolify
- Practice: Deploy test service
**Session 3: CI/CD Operations (2 hours)**
- Read: CI_CD_SETUP.md (complete)
- Hands-on: Configure GitHub secrets
- Practice: Trigger manual deployment
**Session 4: Incident Response (2 hours)**
- Read: DEPLOYMENT_RUNBOOKS.md
- Practice: Execute rollback procedure
- Practice: Restore from backup
@ -474,26 +512,31 @@ The Hive Mind collective has successfully analyzed, designed, and implemented a
### Common Issues & Solutions
**Issue 1: Docker Build Fails in CI**
- **Symptom**: GitHub Actions workflow fails at "Build Docker Image" step
- **Solution**: Check .dockerignore, verify all dependencies in package.json
- **Reference**: DOCKER_GUIDE.md section 6.1
**Issue 2: Tests Fail Locally but Pass in CI**
- **Symptom**: Local test failures but CI shows green
- **Solution**: Clear node_modules and pnpm cache, check Node.js version
- **Reference**: TESTING_IMPLEMENTATION_GUIDE.md section 5.1
**Issue 3: Deployment Succeeds but Service Unhealthy**
- **Symptom**: Deployment completes but health check fails
- **Solution**: Check environment variables, verify Supabase connection
- **Reference**: DEPLOYMENT.md section 4.3
**Issue 4: Coverage Below Threshold**
- **Symptom**: CI fails with "Coverage threshold not met"
- **Solution**: Add missing tests or adjust thresholds temporarily
- **Reference**: TESTING.md section 4
**Issue 5: Slow Build Times**
- **Symptom**: GitHub Actions taking 15+ minutes
- **Solution**: Enable Turborepo remote cache, optimize Docker layers
- **Reference**: CI_CD_SETUP.md section 7
@ -616,6 +659,7 @@ The Hive Mind collective has successfully analyzed, designed, and implemented a
## 🙏 ACKNOWLEDGMENTS
**Hive Mind Worker Agents**:
- 🔍 **Researcher**: Comprehensive infrastructure analysis (24+ searches, 40+ pages)
- 🏗️ **Analyst**: Complete architecture design (87,000+ characters)
- 💻 **Coder**: Production-ready implementation (28 files, 3,500+ lines)
@ -628,6 +672,7 @@ The Hive Mind collective has successfully analyzed, designed, and implemented a
## 📜 LICENSE & USAGE
All code, configurations, and documentation produced by the Hive Mind are:
- ✅ Royalty-free for use in the manacore-monorepo
- ✅ Modifiable without restriction
- ✅ Distributable within your organization
@ -642,7 +687,7 @@ All code, configurations, and documentation produced by the Hive Mind are:
**Status**: ✅ ALL OBJECTIVES ACHIEVED
**Recommendation**: PROCEED WITH IMPLEMENTATION
*"Alone we are smart. Together we are brilliant."* - Hive Mind Collective
_"Alone we are smart. Together we are brilliant."_ - Hive Mind Collective
---
@ -653,6 +698,7 @@ All code, configurations, and documentation produced by the Hive Mind are:
**Root Directory**: `/Users/wuesteon/dev/mana_universe/manacore-monorepo/`
**Documentation**:
- `docs/DEPLOYMENT_ARCHITECTURE.md`
- `docs/DEPLOYMENT_DIAGRAMS.md`
- `docs/DEPLOYMENT_RUNBOOKS.md`
@ -665,6 +711,7 @@ All code, configurations, and documentation produced by the Hive Mind are:
- `docs/test-examples/` (directory with 7 files)
**CI/CD**:
- `.github/workflows/test.yml`
- `.github/workflows/ci-pull-request.yml`
- `.github/workflows/ci-main.yml`
@ -674,6 +721,7 @@ All code, configurations, and documentation produced by the Hive Mind are:
- `.github/workflows/dependency-update.yml`
**Docker**:
- `docker/templates/Dockerfile.nestjs`
- `docker/templates/Dockerfile.sveltekit`
- `docker/templates/Dockerfile.astro`
@ -683,6 +731,7 @@ All code, configurations, and documentation produced by the Hive Mind are:
- `.dockerignore`
**Scripts**:
- `scripts/deploy/build-and-push.sh`
- `scripts/deploy/deploy-hetzner.sh`
- `scripts/deploy/health-check.sh`
@ -690,24 +739,28 @@ All code, configurations, and documentation produced by the Hive Mind are:
- `scripts/deploy/migrate-db.sh`
**Test Configuration**:
- `packages/test-config/` (6 configuration files)
- `vitest.config.ts`
- `jest.config.js`
- `playwright.config.ts`
**Quick Starts**:
- `CI_CD_README.md`
- `QUICK_START_CICD.md`
- `CI_CD_IMPLEMENTATION_SUMMARY.md`
- `FILES_CREATED.md`
**Hive Mind**:
- `.hive-mind/sessions/research-report-hosting-infrastructure.md`
- `HIVE_MIND_FINAL_REPORT.md` (this file)
### B. Command Reference
**Quick Start Commands**:
```bash
# Install dependencies
pnpm install
@ -732,6 +785,7 @@ pnpm run deploy:production
```
**Development Commands**:
```bash
# Start local development
pnpm run dev
@ -751,6 +805,7 @@ pnpm test:e2e
```
**Deployment Commands** (via scripts):
```bash
# Build and push all services
./scripts/deploy/build-and-push.sh
@ -772,6 +827,7 @@ pnpm test:e2e
### C. Resource Links
**Official Documentation**:
- [Hetzner Cloud Docs](https://docs.hetzner.com/)
- [Coolify Documentation](https://coolify.io/docs)
- [Turborepo Docs](https://turbo.build/repo/docs)
@ -779,17 +835,20 @@ pnpm test:e2e
- [GitHub Actions](https://docs.github.com/en/actions)
**Testing Frameworks**:
- [Jest](https://jestjs.io/)
- [Vitest](https://vitest.dev/)
- [Playwright](https://playwright.dev/)
- [Testing Library](https://testing-library.com/)
**Container Ecosystem**:
- [Docker Documentation](https://docs.docker.com/)
- [Docker Compose](https://docs.docker.com/compose/)
- [Multi-stage Builds](https://docs.docker.com/build/building/multi-stage/)
**Monitoring & Observability**:
- [Prometheus](https://prometheus.io/docs/)
- [Grafana](https://grafana.com/docs/)
- [Loki](https://grafana.com/docs/loki/)
@ -798,17 +857,20 @@ pnpm test:e2e
### D. Support & Contribution
**Questions or Issues?**
1. Check the troubleshooting sections in relevant docs
2. Review the FAQ in TESTING.md and DEPLOYMENT.md
3. Consult the Hive Mind collective wisdom in this report
**Found a Bug or Improvement?**
1. Document the issue with steps to reproduce
2. Propose a solution based on the established patterns
3. Test thoroughly before implementing
4. Update relevant documentation
**Want to Extend the System?**
1. Review the "Future Enhancements" section
2. Follow the established architectural patterns
3. Maintain consistency with existing code style
@ -818,8 +880,8 @@ pnpm test:e2e
**END OF HIVE MIND FINAL REPORT**
*Generated by Strategic Queen Coordinator with collective intelligence from 4 specialized worker agents.*
_Generated by Strategic Queen Coordinator with collective intelligence from 4 specialized worker agents._
*Total coordination time: ~2 hours*
*Total deliverables: 280+ pages of documentation + 40+ production-ready files*
*Status: Mission Complete ✅*
_Total coordination time: ~2 hours_
_Total deliverables: 280+ pages of documentation + 40+ production-ready files_
_Status: Mission Complete ✅_

View file

@ -108,6 +108,7 @@ git push origin test/ci-pipeline
If you want to test quickly, here's the absolute minimum:
### For PR Testing Only (No Deployment)
```
# Just these 3 secrets to test PR workflow:
DOCKER_USERNAME=your-username
@ -116,6 +117,7 @@ DOCKER_REGISTRY=your-username
```
### For Staging Deployment
```
# Add these 5 more secrets:
STAGING_HOST=your-server-ip
@ -175,6 +177,7 @@ ssh -i ~/.ssh/github-staging deploy@staging.example.com 'echo "Success"'
## Troubleshooting
### "Permission denied (publickey)"
```bash
# Check SSH key was added to server
ssh deploy@staging.example.com 'cat ~/.ssh/authorized_keys'
@ -184,6 +187,7 @@ ssh deploy@staging.example.com 'cat ~/.ssh/authorized_keys'
```
### "Docker command not found"
```bash
# Install Docker on server
curl -fsSL https://get.docker.com | sh
@ -192,6 +196,7 @@ sudo usermod -aG docker deploy
```
### "Health checks failing"
```bash
# Check service logs
ssh deploy@staging.example.com

View file

@ -4,12 +4,12 @@ Monorepo containing all Manacore projects with shared packages and unified tooli
## Projects
| Project | Description | Tech Stack |
|---------|-------------|------------|
| Project | Description | Tech Stack |
| ------------------ | ------------------------------- | ------------------------------ |
| **maerchenzauber** | AI-powered story generation app | NestJS, Expo, SvelteKit, Astro |
| **manacore** | Multi-app ecosystem platform | Expo, SvelteKit, Astro |
| **manadeck** | Card/deck management app | NestJS, Expo, SvelteKit |
| **memoro** | Voice memo & AI analysis app | Expo, SvelteKit, Astro |
| **manacore** | Multi-app ecosystem platform | Expo, SvelteKit, Astro |
| **manadeck** | Card/deck management app | NestJS, Expo, SvelteKit |
| **memoro** | Voice memo & AI analysis app | Expo, SvelteKit, Astro |
## Getting Started
@ -57,12 +57,12 @@ pnpm run format
Located in `packages/`:
| Package | Description |
|---------|-------------|
| `@manacore/shared-types` | Common TypeScript types |
| `@manacore/shared-supabase` | Unified Supabase client |
| `@manacore/shared-utils` | Utility functions (date, string, async) |
| `@manacore/shared-ui` | React Native UI components |
| Package | Description |
| --------------------------- | --------------------------------------- |
| `@manacore/shared-types` | Common TypeScript types |
| `@manacore/shared-supabase` | Unified Supabase client |
| `@manacore/shared-utils` | Utility functions (date, string, async) |
| `@manacore/shared-ui` | React Native UI components |
### Using Shared Packages

View file

@ -17,12 +17,14 @@ This document summarizes the comprehensive testing strategy for the central auth
## Critical Business Paths
### Priority 1: Authentication Flow
1. **User Registration** → Tokens Generated → Secure Storage → Access Granted
2. **User Login** → Token Validation → Session Established → Multi-Device Support
3. **Token Expiration** → Automatic Refresh → Seamless Continuation
4. **User Logout** → Token Invalidation → Secure Cleanup
### Priority 2: Credit System Flow
1. **Credit Purchase** → Payment Validation → Balance Update → Transaction Logged
2. **Pre-Operation Validation** → Operation Execution → Credit Deduction → Balance Update
3. **Failed Operation** → No Charge Applied → User Notified
@ -34,16 +36,17 @@ This document summarizes the comprehensive testing strategy for the central auth
### Authentication Testing (45 Test Cases)
| Category | Test Cases | Priority | Coverage |
|----------|-----------|----------|----------|
| Registration (Email/Social) | 8 | P0 | 100% |
| Login/Logout | 10 | P0 | 100% |
| Token Refresh | 6 | P0 | 100% |
| Session Management | 6 | P1 | 100% |
| Password Management | 5 | P1 | 90% |
| Multi-Device/Multi-App | 10 | P0-P1 | 100% |
| Category | Test Cases | Priority | Coverage |
| --------------------------- | ---------- | -------- | -------- |
| Registration (Email/Social) | 8 | P0 | 100% |
| Login/Logout | 10 | P0 | 100% |
| Token Refresh | 6 | P0 | 100% |
| Session Management | 6 | P1 | 100% |
| Password Management | 5 | P1 | 90% |
| Multi-Device/Multi-App | 10 | P0-P1 | 100% |
**Key Security Tests:**
- SQL Injection Prevention ✓
- JWT Token Manipulation ✓
- Token Expiration Enforcement ✓
@ -52,17 +55,18 @@ This document summarizes the comprehensive testing strategy for the central auth
### Credit System Testing (38 Test Cases)
| Category | Test Cases | Priority | Coverage |
|----------|-----------|----------|----------|
| Credit Purchase | 6 | P0 | 100% |
| Balance Checking | 4 | P0 | 100% |
| Credit Consumption | 8 | P0 | 100% |
| Refund & Adjustments | 4 | P1 | 100% |
| Transaction History | 4 | P2 | 90% |
| Concurrent Transactions | 6 | P0 | 100% |
| Cross-App Visibility | 6 | P0 | 100% |
| Category | Test Cases | Priority | Coverage |
| ----------------------- | ---------- | -------- | -------- |
| Credit Purchase | 6 | P0 | 100% |
| Balance Checking | 4 | P0 | 100% |
| Credit Consumption | 8 | P0 | 100% |
| Refund & Adjustments | 4 | P1 | 100% |
| Transaction History | 4 | P2 | 90% |
| Concurrent Transactions | 6 | P0 | 100% |
| Cross-App Visibility | 6 | P0 | 100% |
**Key Security Tests:**
- Balance Tampering Prevention ✓
- Unauthorized Deduction Prevention ✓
- Replay Attack Prevention ✓
@ -71,23 +75,24 @@ This document summarizes the comprehensive testing strategy for the central auth
### Integration Testing (15 Test Cases)
| Platform | Test Cases | Priority |
|----------|-----------|----------|
| iOS Mobile (Expo) | 3 | P0 |
| Android Mobile (Expo) | 3 | P0 |
| Web (SvelteKit) | 3 | P0 |
| Backend (NestJS) | 3 | P0 |
| Payment Gateway (RevenueCat) | 3 | P0 |
| Platform | Test Cases | Priority |
| ---------------------------- | ---------- | -------- |
| iOS Mobile (Expo) | 3 | P0 |
| Android Mobile (Expo) | 3 | P0 |
| Web (SvelteKit) | 3 | P0 |
| Backend (NestJS) | 3 | P0 |
| Payment Gateway (RevenueCat) | 3 | P0 |
### Performance Testing (12 Test Cases)
| Test Type | Scenarios | Load Target |
|-----------|-----------|-------------|
| Load Testing | 3 | 1000 concurrent users |
| Stress Testing | 2 | 5000 operations |
| Scalability Testing | 2 | 1M transactions/day |
| Test Type | Scenarios | Load Target |
| ------------------- | --------- | --------------------- |
| Load Testing | 3 | 1000 concurrent users |
| Stress Testing | 2 | 5000 operations |
| Scalability Testing | 2 | 1M transactions/day |
**Performance Targets:**
- Login Response Time: < 2 seconds (P95)
- Token Refresh: < 2 seconds (P95)
- Credit Balance Check: < 100ms (P95)
@ -98,24 +103,28 @@ This document summarizes the comprehensive testing strategy for the central auth
## Test Automation Breakdown
### Unit Tests
- **Framework:** Jest
- **Coverage Target:** 80%+
- **Location:** `packages/shared-auth/`, `packages/shared-credit-service/`
- **Run Frequency:** Every commit (pre-commit hook)
### Integration Tests
- **Framework:** Jest + Supertest
- **Coverage Target:** 100% critical paths
- **Location:** `*/apps/backend/test/`, `*/apps/mobile/features/*/tests/`
- **Run Frequency:** Every pull request
### E2E Tests
- **Framework:** Detox (mobile), Playwright (web)
- **Coverage Target:** 100% user journeys
- **Location:** `*/apps/*/e2e/`, `*/apps/*/tests/`
- **Run Frequency:** Pre-staging deployment
### Performance Tests
- **Framework:** k6
- **Target:** 1000 concurrent users without degradation
- **Location:** `tests/performance/`
@ -126,54 +135,64 @@ This document summarizes the comprehensive testing strategy for the central auth
## Critical Test Scenarios
### 1. Concurrent Credit Deduction (Race Condition)
**Risk:** High - Could cause financial discrepancies
**Test:** TC-CREDIT-CONSUME-003
**Mitigation:** Database transactions with optimistic locking
**Scenario:**
- User has 100 credits
- 3 operations triggered simultaneously (30 credits each)
- Expected: All succeed, final balance = 10 credits
- Test validates: No over-deduction or under-deduction
### 2. Token Refresh During High Load
**Risk:** Medium - User experience degradation
**Test:** TC-PERF-LOAD-002
**Mitigation:** Token manager queue + cooldown mechanism
**Scenario:**
- 500 users with expired tokens make API calls simultaneously
- Expected: Single refresh per user, all requests succeed
- Test validates: No duplicate refreshes, queue handles load
### 3. Payment Webhook Duplicate Detection
**Risk:** High - Could cause double-crediting
**Test:** TC-CREDIT-PURCHASE-003
**Mitigation:** Idempotency keys, transaction ID validation
**Scenario:**
- Webhook received successfully
- Same webhook replayed (network retry)
- Expected: Second webhook ignored, no double-crediting
- Test validates: Idempotent processing
### 4. Cross-App Credit Synchronization
**Risk:** Medium - User confusion, trust issues
**Test:** TC-INT-CROSS-002
**Mitigation:** Central credit service, real-time updates
**Scenario:**
- Consume credits in Memoro
- Immediately check balance in Maerchenzauber
- Expected: Balance updated in < 1 second
- Test validates: Consistent state across apps
### 5. Multi-Device Session Management
**Risk:** Low - Potential token conflicts
**Test:** TC-AUTH-SESSION-001
**Mitigation:** Independent refresh tokens per device
**Scenario:**
- User logs in on iOS, Android, and Web
- All devices active simultaneously
- Token refresh on one device
@ -187,17 +206,20 @@ This document summarizes the comprehensive testing strategy for the central auth
### Authentication Security
**SQL Injection Prevention (TC-SEC-AUTH-001)**
- Test payloads: `admin'--`, `' OR '1'='1`, `'; DROP TABLE users;--`
- Expected: All rejected, no DB queries executed
- Result: PASS ✓ (parameterized queries used)
**JWT Token Manipulation (TC-SEC-AUTH-002)**
- Modify token claims (user ID, role, credits)
- Re-sign with wrong secret
- Expected: Signature validation fails, 401 error
- Result: PASS ✓ (RS256 verification)
**Brute Force Protection (TC-SEC-AUTH-004)**
- 5 failed login attempts
- Expected: Account locked for 15 minutes
- Result: PASS ✓ (rate limiting implemented)
@ -205,12 +227,14 @@ This document summarizes the comprehensive testing strategy for the central auth
### Credit System Security
**Balance Tampering Prevention (TC-SEC-CREDIT-001)**
- Attempt to modify balance via API manipulation
- Client-side storage modification
- Expected: Server-side validation rejects all attempts
- Result: PASS ✓ (server-authoritative balance)
**Replay Attack Prevention (TC-SEC-CREDIT-003)**
- Capture and replay payment webhook
- Expected: Duplicate detected by transaction ID
- Result: PASS ✓ (idempotency keys)
@ -220,6 +244,7 @@ This document summarizes the comprehensive testing strategy for the central auth
## Acceptance Criteria Checklist
### Authentication System
- [x] User can register with email/password in < 3 seconds
- [x] User can login with email/password in < 2 seconds
- [x] Token refresh happens automatically without user interaction
@ -229,6 +254,7 @@ This document summarizes the comprehensive testing strategy for the central auth
- [x] 99.9% uptime for authentication services
### Credit System
- [x] Credit balance updates within 1 second of purchase
- [x] Credit deduction happens only after operation succeeds
- [x] Failed operations never charge credits
@ -238,6 +264,7 @@ This document summarizes the comprehensive testing strategy for the central auth
- [x] Refunds processed within 1 hour (automated)
### Integration
- [x] Mobile apps support iOS 14+ and Android 10+
- [x] Web apps work on Chrome, Safari, Firefox, Edge (latest 2 versions)
- [x] RevenueCat purchase flow completes in < 30 seconds
@ -245,6 +272,7 @@ This document summarizes the comprehensive testing strategy for the central auth
- [x] Cross-app authentication works seamlessly
### Security
- [x] No plaintext passwords stored anywhere
- [x] JWT tokens secured with RS256 algorithm
- [x] Rate limiting prevents brute force attacks
@ -253,6 +281,7 @@ This document summarizes the comprehensive testing strategy for the central auth
- [ ] Penetration test: No critical vulnerabilities (requires external audit)
### Performance
- [x] System handles 1000 concurrent users without degradation
- [x] 99th percentile response time < 3 seconds
- [x] Token refresh completes in < 2 seconds
@ -264,24 +293,28 @@ This document summarizes the comprehensive testing strategy for the central auth
## Test Execution Strategy
### Daily (Automated)
- Smoke tests (5 minutes)
- Core auth flows
- Credit balance checks
- CI/CD pipeline integration
### Weekly (Automated + Manual)
- Full regression suite (1 hour)
- Integration tests
- Performance smoke tests
- Security dependency scan
### Monthly (Scheduled)
- Full security audit
- Load testing (1000+ concurrent users)
- Penetration testing (external)
- Compliance review
### Per Deployment (Automated)
- Pre-deployment: Full regression (30 minutes)
- Post-deployment: Smoke tests (5 minutes)
- Canary deployment monitoring (1 hour)
@ -293,18 +326,21 @@ This document summarizes the comprehensive testing strategy for the central auth
### Critical Risks (Requires Immediate Testing)
**1. Credit Double-Deduction**
- **Impact:** HIGH (Financial loss, legal liability)
- **Probability:** MEDIUM (Concurrent operations common)
- **Mitigation:** Database transactions, optimistic locking
- **Test:** TC-CREDIT-CONSUME-003, TC-CREDIT-CONSUME-004
**2. Payment Webhook Failure**
- **Impact:** HIGH (Lost revenue, user frustration)
- **Probability:** MEDIUM (Network issues, gateway downtime)
- **Mitigation:** Idempotency, retry mechanism, manual reconciliation
- **Test:** TC-CREDIT-PURCHASE-003, TC-CREDIT-PURCHASE-004
**3. Token Hijacking**
- **Impact:** HIGH (Account compromise, data breach)
- **Probability:** LOW (HTTPS enforced, short token lifetime)
- **Mitigation:** HTTPS only, token rotation, short expiry
@ -313,12 +349,14 @@ This document summarizes the comprehensive testing strategy for the central auth
### Medium Risks (Monitor Closely)
**4. Cross-App State Inconsistency**
- **Impact:** MEDIUM (User confusion, support burden)
- **Probability:** MEDIUM (Caching issues, sync delays)
- **Mitigation:** Central credit service, real-time updates
- **Test:** TC-INT-CROSS-002
**5. Concurrent Login Session Conflicts**
- **Impact:** MEDIUM (User experience disruption)
- **Probability:** LOW (Independent tokens per device)
- **Mitigation:** Device-specific refresh tokens
@ -328,28 +366,31 @@ This document summarizes the comprehensive testing strategy for the central auth
## Test Environment Summary
| Environment | Purpose | Payment | Database |
|-------------|---------|---------|----------|
| **Development** | Developer testing | Mock gateway | Supabase dev |
| **Staging** | QA validation, pre-production | RevenueCat sandbox | Supabase staging |
| **Production** | Live users | RevenueCat production | Supabase production |
| Environment | Purpose | Payment | Database |
| --------------- | ----------------------------- | --------------------- | ------------------- |
| **Development** | Developer testing | Mock gateway | Supabase dev |
| **Staging** | QA validation, pre-production | RevenueCat sandbox | Supabase staging |
| **Production** | Live users | RevenueCat production | Supabase production |
---
## Tools & Infrastructure
### Testing Frameworks
- **Unit/Integration:** Jest, Supertest
- **E2E:** Detox (mobile), Playwright (web)
- **Performance:** k6, Lighthouse
- **Security:** OWASP ZAP, Snyk, SonarQube
### CI/CD
- **Platform:** GitHub Actions
- **Stages:** Lint → Unit Tests → Integration Tests → E2E Tests → Deploy
- **Quality Gates:** 80% code coverage, 0 critical security issues, all tests passing
### Monitoring
- **Application:** New Relic, Sentry
- **Infrastructure:** Cloud provider monitoring
- **Alerts:** Slack integration for failures
@ -359,6 +400,7 @@ This document summarizes the comprehensive testing strategy for the central auth
## Gaps & Recommendations
### Current Gaps
1. **Load Testing:** Not yet executed at full 1000 user scale
- **Recommendation:** Schedule weekly k6 load tests
- **Owner:** DevOps team
@ -376,6 +418,7 @@ This document summarizes the comprehensive testing strategy for the central auth
- **Owner:** Backend team
### Future Enhancements
1. **Visual Regression Testing:** Add Chromatic or Percy for UI consistency
2. **Accessibility Testing:** Ensure WCAG 2.1 AA compliance
3. **Internationalization Testing:** Validate all 32 languages (Memoro)
@ -386,17 +429,20 @@ This document summarizes the comprehensive testing strategy for the central auth
## Success Metrics
### Test Coverage Goals
- Unit Test Coverage: **> 80%** ✓
- Integration Test Coverage: **100% critical paths**
- E2E Test Coverage: **100% user journeys** (In Progress)
- Security Test Coverage: **100% OWASP Top 10**
### Quality Metrics
- Production Bugs: **< 5 critical bugs per quarter**
- Mean Time to Detection (MTTD): **< 1 hour**
- Mean Time to Resolution (MTTR): **< 4 hours for critical, < 24 hours for high**
### Performance Metrics
- API Response Time (P95): **< 500ms**
- Token Refresh Time (P95): **< 2s**
- Credit Balance Check (P95): **< 100ms**
@ -407,18 +453,21 @@ This document summarizes the comprehensive testing strategy for the central auth
## Next Steps
### Week 1: Test Infrastructure Setup
- [ ] Configure k6 for load testing
- [ ] Set up Detox for mobile E2E
- [ ] Integrate security scanning into CI/CD
- [ ] Create test data management scripts
### Week 2-3: Test Execution
- [ ] Execute all P0 test cases manually
- [ ] Automate P0 test cases
- [ ] Run first load test (100 concurrent users)
- [ ] Security scan and vulnerability remediation
### Week 4: Validation & Reporting
- [ ] Full regression suite execution
- [ ] Performance baseline established
- [ ] Security audit report
@ -436,12 +485,14 @@ This comprehensive testing strategy covers **110+ test cases** across authentica
4. **Automation:** CI/CD integration for continuous quality assurance
**Estimated Effort:**
- Initial Test Development: 4 weeks (2 QA engineers)
- Ongoing Regression Testing: 2 days/sprint
- Load Testing: 1 day/week
- Security Audits: 1 week/quarter (external)
**Key Success Factors:**
- Early involvement of QA in feature development
- Automated regression suite in CI/CD pipeline
- Regular security audits and penetration testing

View file

@ -5,6 +5,7 @@ This file provides guidance to Claude Code when working with the BaunTown projec
## Project Overview
BaunTown is a community website for developers and creators with:
- Multilingual support (DE, EN, IT)
- Payment integration (Stripe, PayPal)
- Content collections for news, projects, tutorials, etc.
@ -56,6 +57,7 @@ pnpm --filter @bauntown/landing preview
## Environment Variables
Create `apps/bauntown/apps/landing/.env`:
```bash
STRIPE_SECRET_KEY=sk_...
STRIPE_WEBHOOK_SECRET=whsec_...
@ -65,31 +67,31 @@ PUBLIC_STRIPE_KEY=pk_...
## Technology Stack
| Component | Technology |
|-----------|------------|
| Framework | Astro 5.x |
| Styling | CSS/Tailwind |
| i18n | astro-i18n-aut (DE, EN, IT) |
| Payments | Stripe, PayPal |
| Analytics | Plausible (via Partytown) |
| APIs | Google Sheets/Docs |
| Deployment | Netlify (SSR + Functions) |
| Component | Technology |
| ---------- | --------------------------- |
| Framework | Astro 5.x |
| Styling | CSS/Tailwind |
| i18n | astro-i18n-aut (DE, EN, IT) |
| Payments | Stripe, PayPal |
| Analytics | Plausible (via Partytown) |
| APIs | Google Sheets/Docs |
| Deployment | Netlify (SSR + Functions) |
## Content Collections
BaunTown uses Astro Content Collections:
| Collection | Purpose |
|------------|---------|
| tools | Design, Development, Productivity tools |
| news | AI, Web, Design, Community news |
| models | AI models (Text, Image) |
| projects | Web, Mobile, Desktop projects |
| tutorials | Courses (UI/UX, Business, Marketing) |
| missions | Community challenges |
| vision | Long-term vision items |
| join | Join page content |
| members | Team members |
| Collection | Purpose |
| ---------- | --------------------------------------- |
| tools | Design, Development, Productivity tools |
| news | AI, Web, Design, Community news |
| models | AI models (Text, Image) |
| projects | Web, Mobile, Desktop projects |
| tutorials | Courses (UI/UX, Business, Marketing) |
| missions | Community challenges |
| vision | Long-term vision items |
| join | Join page content |
| members | Team members |
## Code Style Guidelines
@ -110,6 +112,7 @@ BaunTown uses Astro Content Collections:
## Deployment
Deployed via Netlify with `@astrojs/netlify` adapter:
- Static pages pre-rendered
- Dynamic routes use Netlify Functions
- Configuration in `netlify.toml`

View file

@ -67,4 +67,4 @@ Die Website unterstützt mehrere Sprachen mit einer URL-Struktur wie `example.co
- `de` - Deutsch (Standard)
- `en` - Englisch
- `it` - Italienisch
- `it` - Italienisch

View file

@ -67,12 +67,13 @@ npm install stripe @stripe/stripe-js @paypal/paypal-js
### 3. Erstellung der Frontend-Komponenten
#### PaymentForm.astro
```astro
---
import { useTranslations } from '../utils/i18n';
interface Props {
lang: string;
lang: string;
}
const { lang } = Astro.props;
@ -80,69 +81,74 @@ const t = useTranslations(lang);
---
<div class="payment-container">
<!-- Zahlungstyp-Auswahl -->
<div class="payment-type-selector">
<button id="one-time" class="payment-type-btn active">{t('support.onetime')}</button>
<button id="recurring" class="payment-type-btn">{t('support.recurring')}</button>
</div>
<!-- Kaffee-Größen -->
<div class="coffee-options">
<!-- ... -->
</div>
<!-- Zahlungsmethoden -->
<div class="payment-buttons">
<button id="stripe-button" class="payment-method-btn">
<!-- ... -->
<span>{t('support.payWithStripe')}</span>
</button>
<button id="paypal-button" class="payment-method-btn">
<!-- ... -->
<span>{t('support.payWithPayPal')}</span>
</button>
</div>
<!-- Zahlungstyp-Auswahl -->
<div class="payment-type-selector">
<button id="one-time" class="payment-type-btn active">{t('support.onetime')}</button>
<button id="recurring" class="payment-type-btn">{t('support.recurring')}</button>
</div>
<!-- Kaffee-Größen -->
<div class="coffee-options">
<!-- ... -->
</div>
<!-- Zahlungsmethoden -->
<div class="payment-buttons">
<button id="stripe-button" class="payment-method-btn">
<!-- ... -->
<span>{t('support.payWithStripe')}</span>
</button>
<button id="paypal-button" class="payment-method-btn">
<!-- ... -->
<span>{t('support.payWithPayPal')}</span>
</button>
</div>
</div>
<script>
import { loadStripe } from '@stripe/stripe-js';
document.addEventListener('DOMContentLoaded', async () => {
// Stripe-Instanz initialisieren
const stripePromise = loadStripe(import.meta.env.PUBLIC_STRIPE_PUBLISHABLE_KEY || 'pk_test_placeholder');
// Event-Listener für Stripe-Button
stripeBtn?.addEventListener('click', async () => {
try {
// Checkout-Session erstellen
const response = await fetch('/.netlify/functions/create-payment-intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
amount, isRecurring, priceId, coffeeSize
}),
});
const data = await response.json();
// Zur Checkout-Seite weiterleiten
if (data.url) {
window.location.href = data.url;
return;
}
// Alternativ: redirectToCheckout verwenden
if (data.sessionId || data.id) {
const stripe = await stripePromise;
await stripe.redirectToCheckout({
sessionId: data.sessionId || data.id,
});
}
} catch (error) {
console.error('Payment error:', error);
}
});
});
import { loadStripe } from '@stripe/stripe-js';
document.addEventListener('DOMContentLoaded', async () => {
// Stripe-Instanz initialisieren
const stripePromise = loadStripe(
import.meta.env.PUBLIC_STRIPE_PUBLISHABLE_KEY || 'pk_test_placeholder'
);
// Event-Listener für Stripe-Button
stripeBtn?.addEventListener('click', async () => {
try {
// Checkout-Session erstellen
const response = await fetch('/.netlify/functions/create-payment-intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
amount,
isRecurring,
priceId,
coffeeSize,
}),
});
const data = await response.json();
// Zur Checkout-Seite weiterleiten
if (data.url) {
window.location.href = data.url;
return;
}
// Alternativ: redirectToCheckout verwenden
if (data.sessionId || data.id) {
const stripe = await stripePromise;
await stripe.redirectToCheckout({
sessionId: data.sessionId || data.id,
});
}
} catch (error) {
console.error('Payment error:', error);
}
});
});
</script>
```
@ -154,50 +160,52 @@ const t = useTranslations(lang);
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
exports.handler = async (event, context) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type",
"Access-Control-Allow-Methods": "POST, OPTIONS"
};
try {
const { amount, isRecurring, coffeeSize } = JSON.parse(event.body || '{}');
const amountInCents = Math.round(amount * 100);
// Stripe Checkout Session erstellen
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [{
price_data: {
currency: 'eur',
product_data: {
name: `BaunTown Kaffee - ${coffeeSize || 'Mittlerer Kaffee'}`,
description: isRecurring ? 'Monatliche Unterstützung' : 'Einmalige Unterstützung',
},
unit_amount: amountInCents,
recurring: isRecurring ? { interval: 'month' } : undefined,
},
quantity: 1,
}],
mode: isRecurring ? 'subscription' : 'payment',
success_url: `${process.env.URL || 'https://bauntown.com'}/support-success`,
cancel_url: `${process.env.URL || 'https://bauntown.com'}/support-cancel`,
});
return {
statusCode: 200,
headers,
body: JSON.stringify({
url: session.url
})
};
} catch (error) {
return {
statusCode: 500,
headers,
body: JSON.stringify({ error: error.message })
};
}
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
};
try {
const { amount, isRecurring, coffeeSize } = JSON.parse(event.body || '{}');
const amountInCents = Math.round(amount * 100);
// Stripe Checkout Session erstellen
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
{
price_data: {
currency: 'eur',
product_data: {
name: `BaunTown Kaffee - ${coffeeSize || 'Mittlerer Kaffee'}`,
description: isRecurring ? 'Monatliche Unterstützung' : 'Einmalige Unterstützung',
},
unit_amount: amountInCents,
recurring: isRecurring ? { interval: 'month' } : undefined,
},
quantity: 1,
},
],
mode: isRecurring ? 'subscription' : 'payment',
success_url: `${process.env.URL || 'https://bauntown.com'}/support-success`,
cancel_url: `${process.env.URL || 'https://bauntown.com'}/support-cancel`,
});
return {
statusCode: 200,
headers,
body: JSON.stringify({
url: session.url,
}),
};
} catch (error) {
return {
statusCode: 500,
headers,
body: JSON.stringify({ error: error.message }),
};
}
};
```
@ -265,7 +273,7 @@ export function useTranslations(lang: keyof typeof ui) {
- Navigieren Sie zu Ihrem Netlify-Dashboard
- Gehen Sie zu "Site settings" > "Environment variables"
- Fügen Sie die folgenden Umgebungsvariablen hinzu:
- `PUBLIC_STRIPE_PUBLISHABLE_KEY` (muss mit PUBLIC_ beginnen für Frontend-Verwendung)
- `PUBLIC_STRIPE_PUBLISHABLE_KEY` (muss mit PUBLIC\_ beginnen für Frontend-Verwendung)
- `STRIPE_SECRET_KEY`
- `STRIPE_WEBHOOK_SECRET` (optional für Webhook-Verarbeitung)
@ -315,9 +323,9 @@ Die Netlify-Function enthält CORS-Header für die Anfrageverarbeitung:
```javascript
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type",
"Access-Control-Allow-Methods": "POST, OPTIONS"
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
};
```
@ -327,15 +335,15 @@ Die Implementation enthält einen Fallback-Mechanismus für Entwicklungs- und Te
```javascript
if (!process.env.STRIPE_SECRET_KEY) {
console.log("WARNUNG: STRIPE_SECRET_KEY fehlt - liefere Test-Antwort");
return {
statusCode: 200,
headers,
body: JSON.stringify({
url: `${process.env.URL || 'https://bauntown.com'}/support-success?test=true`,
message: "Test mode - no Stripe key available"
})
};
console.log('WARNUNG: STRIPE_SECRET_KEY fehlt - liefere Test-Antwort');
return {
statusCode: 200,
headers,
body: JSON.stringify({
url: `${process.env.URL || 'https://bauntown.com'}/support-success?test=true`,
message: 'Test mode - no Stripe key available',
}),
};
}
```
@ -346,9 +354,10 @@ if (!process.env.STRIPE_SECRET_KEY) {
Die Stripe-Integration für das "Buy Me a Coffee"-Feature wurde erfolgreich implementiert und ermöglicht sowohl einmalige als auch wiederkehrende Spenden. Durch die Verwendung von Netlify Functions wird die Sicherheit erhöht, da sensible Daten wie der Stripe Secret Key nicht im Frontend verfügbar sind.
Die wichtigsten Punkte für eine fehlerfreie Implementierung:
1. Verwenden Sie `PUBLIC_`-Präfix für Frontend-Umgebungsvariablen in Astro
2. Verwenden Sie den Stripe Checkout-Flow für eine einfache und sichere Zahlungsabwicklung
3. Implementieren Sie robuste Fehlerbehandlung für Übersetzungen und API-Aufrufe
4. Testen Sie sowohl im Entwicklungs- als auch im Produktionsmodus
Bei Fragen oder Problemen konsultieren Sie die [Stripe-Dokumentation](https://stripe.com/docs/checkout) oder öffnen Sie ein Issue im BaunTown-Repository.
Bei Fragen oder Problemen konsultieren Sie die [Stripe-Dokumentation](https://stripe.com/docs/checkout) oder öffnen Sie ein Issue im BaunTown-Repository.

View file

@ -4,155 +4,157 @@ const crypto = require('crypto');
// Überprüfe, ob alle erforderlichen Umgebungsvariablen vorhanden sind
const requiredEnvVars = [
'GOOGLE_SERVICE_ACCOUNT_EMAIL',
'GOOGLE_PRIVATE_KEY',
'GOOGLE_SHEET_ID',
'SITE_URL'
'GOOGLE_SERVICE_ACCOUNT_EMAIL',
'GOOGLE_PRIVATE_KEY',
'GOOGLE_SHEET_ID',
'SITE_URL',
];
function checkEnvVars() {
const missing = requiredEnvVars.filter(varName => !process.env[varName]);
if (missing.length > 0) {
throw new Error(`Fehlende Umgebungsvariablen: ${missing.join(', ')}`);
}
const missing = requiredEnvVars.filter((varName) => !process.env[varName]);
if (missing.length > 0) {
throw new Error(`Fehlende Umgebungsvariablen: ${missing.join(', ')}`);
}
}
const auth = new JWT({
email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
key: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
key: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
});
const sheets = google.sheets({ version: 'v4', auth });
exports.handler = async function(event, context) {
console.log('Content submission request received');
// CORS Headers
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST, OPTIONS'
};
exports.handler = async function (event, context) {
console.log('Content submission request received');
// Handle OPTIONS request
if (event.httpMethod === 'OPTIONS') {
return {
statusCode: 200,
headers,
body: ''
};
}
// CORS Headers
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
};
if (event.httpMethod !== 'POST') {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: 'Method Not Allowed' })
};
}
// Handle OPTIONS request
if (event.httpMethod === 'OPTIONS') {
return {
statusCode: 200,
headers,
body: '',
};
}
try {
// Überprüfe Umgebungsvariablen
checkEnvVars();
// Parse request body
const { contentType, title, description, email } = JSON.parse(event.body);
if (!contentType || !title || !description || !email) {
throw new Error('Alle Felder müssen ausgefüllt werden');
}
if (event.httpMethod !== 'POST') {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: 'Method Not Allowed' }),
};
}
console.log('Processing submission:', { contentType, title, email });
const timestamp = new Date().toISOString();
const id = crypto.randomUUID();
try {
// Überprüfe Umgebungsvariablen
checkEnvVars();
// Füge neue Zeile zum Sheet hinzu
console.log('Adding row to Google Sheet');
try {
// Prüfe zuerst, ob das Sheet existiert
const sheetInfo = await sheets.spreadsheets.get({
spreadsheetId: process.env.GOOGLE_SHEET_ID
});
// Prüfe, ob das Content-Submissions-Sheet existiert, wenn nicht, erstelle es
let sheetExists = false;
const sheetName = 'ContentSubmissions';
for (const sheet of sheetInfo.data.sheets) {
if (sheet.properties.title === sheetName) {
sheetExists = true;
break;
}
}
if (!sheetExists) {
// Erstelle ein neues Sheet für Content-Submissions
await sheets.spreadsheets.batchUpdate({
spreadsheetId: process.env.GOOGLE_SHEET_ID,
requestBody: {
requests: [
{
addSheet: {
properties: {
title: sheetName,
gridProperties: {
rowCount: 1000,
columnCount: 6
}
}
}
}
]
}
});
// Füge Überschriften hinzu
await sheets.spreadsheets.values.update({
spreadsheetId: process.env.GOOGLE_SHEET_ID,
range: `${sheetName}!A1:F1`,
valueInputOption: 'USER_ENTERED',
requestBody: {
values: [['Timestamp', 'ID', 'Content Type', 'Title', 'Description', 'Email']]
}
});
}
// Füge die neue Zeile hinzu
await sheets.spreadsheets.values.append({
spreadsheetId: process.env.GOOGLE_SHEET_ID,
range: `${sheetName}!A:F`,
valueInputOption: 'USER_ENTERED',
requestBody: {
values: [[timestamp, id, contentType, title, description, email]]
}
});
console.log('Content submission added to sheet');
} catch (sheetError) {
console.error('Google Sheets error:', sheetError);
throw new Error(`Google Sheets Fehler: ${sheetError.message}. Bitte stellen Sie sicher, dass das Sheet existiert und der Service Account Zugriff hat.`);
}
// Parse request body
const { contentType, title, description, email } = JSON.parse(event.body);
console.log('Submission completed successfully');
return {
statusCode: 200,
headers,
body: JSON.stringify({
success: true,
message: 'Deine Einreichung wurde erfolgreich gespeichert.'
})
};
} catch (error) {
console.error('Content submission error:', error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: error.message,
details: error.stack
})
};
}
};
if (!contentType || !title || !description || !email) {
throw new Error('Alle Felder müssen ausgefüllt werden');
}
console.log('Processing submission:', { contentType, title, email });
const timestamp = new Date().toISOString();
const id = crypto.randomUUID();
// Füge neue Zeile zum Sheet hinzu
console.log('Adding row to Google Sheet');
try {
// Prüfe zuerst, ob das Sheet existiert
const sheetInfo = await sheets.spreadsheets.get({
spreadsheetId: process.env.GOOGLE_SHEET_ID,
});
// Prüfe, ob das Content-Submissions-Sheet existiert, wenn nicht, erstelle es
let sheetExists = false;
const sheetName = 'ContentSubmissions';
for (const sheet of sheetInfo.data.sheets) {
if (sheet.properties.title === sheetName) {
sheetExists = true;
break;
}
}
if (!sheetExists) {
// Erstelle ein neues Sheet für Content-Submissions
await sheets.spreadsheets.batchUpdate({
spreadsheetId: process.env.GOOGLE_SHEET_ID,
requestBody: {
requests: [
{
addSheet: {
properties: {
title: sheetName,
gridProperties: {
rowCount: 1000,
columnCount: 6,
},
},
},
},
],
},
});
// Füge Überschriften hinzu
await sheets.spreadsheets.values.update({
spreadsheetId: process.env.GOOGLE_SHEET_ID,
range: `${sheetName}!A1:F1`,
valueInputOption: 'USER_ENTERED',
requestBody: {
values: [['Timestamp', 'ID', 'Content Type', 'Title', 'Description', 'Email']],
},
});
}
// Füge die neue Zeile hinzu
await sheets.spreadsheets.values.append({
spreadsheetId: process.env.GOOGLE_SHEET_ID,
range: `${sheetName}!A:F`,
valueInputOption: 'USER_ENTERED',
requestBody: {
values: [[timestamp, id, contentType, title, description, email]],
},
});
console.log('Content submission added to sheet');
} catch (sheetError) {
console.error('Google Sheets error:', sheetError);
throw new Error(
`Google Sheets Fehler: ${sheetError.message}. Bitte stellen Sie sicher, dass das Sheet existiert und der Service Account Zugriff hat.`
);
}
console.log('Submission completed successfully');
return {
statusCode: 200,
headers,
body: JSON.stringify({
success: true,
message: 'Deine Einreichung wurde erfolgreich gespeichert.',
}),
};
} catch (error) {
console.error('Content submission error:', error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: error.message,
details: error.stack,
}),
};
}
};

View file

@ -2,112 +2,114 @@
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
exports.handler = async (event, context) => {
// CORS Headers
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type",
"Access-Control-Allow-Methods": "POST, OPTIONS"
};
// Handle OPTIONS request (CORS preflight)
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: ""
};
}
// CORS Headers
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
};
try {
// Parse request
const data = JSON.parse(event.body || '{}');
const { amount, isRecurring, priceId, coffeeSize } = data;
// Debug-Ausgabe
console.log("Request data:", { amount, isRecurring, priceId, coffeeSize });
console.log("Stripe key available:", !!process.env.STRIPE_SECRET_KEY);
// Fallback, wenn kein Stripe-Key verfügbar
if (!process.env.STRIPE_SECRET_KEY) {
console.log("WARNUNG: STRIPE_SECRET_KEY fehlt - liefere Test-Antwort");
return {
statusCode: 200,
headers,
body: JSON.stringify({
url: `${process.env.URL || 'https://bauntown.com'}/support-success?test=true`,
isRecurring: isRecurring || false,
message: "Test mode - no Stripe key available"
})
};
}
// Betrag in Cent umrechnen für Stripe
const amountInCents = Math.round(amount * 100);
let sessionConfig = {
payment_method_types: ['card'],
line_items: [{
price_data: {
currency: 'eur',
product_data: {
name: `BaunTown Kaffee - ${coffeeSize || 'Mittlerer Kaffee'}`,
description: isRecurring ? 'Monatliche Unterstützung' : 'Einmalige Unterstützung',
},
unit_amount: amountInCents,
recurring: isRecurring ? { interval: 'month' } : undefined,
},
quantity: 1,
}],
mode: isRecurring ? 'subscription' : 'payment',
success_url: `${process.env.URL || 'https://bauntown.com'}/support-success?amount=${amount}&type=${isRecurring ? 'recurring' : 'one-time'}&provider=stripe&coffeeSize=${encodeURIComponent(coffeeSize || 'Mittlerer Kaffee')}`,
cancel_url: `${process.env.URL || 'https://bauntown.com'}/support-cancel`,
metadata: {
isRecurring: isRecurring ? 'true' : 'false',
priceId: priceId || '',
coffeeSize: coffeeSize || 'Mittlerer Kaffee'
}
};
console.log("Creating checkout session with config:", JSON.stringify(sessionConfig));
try {
// Stripe Checkout Session erstellen
const session = await stripe.checkout.sessions.create(sessionConfig);
console.log("Session created:", session.id, "URL:", session.url);
return {
statusCode: 200,
headers,
body: JSON.stringify({
url: session.url, // Die Redirect-URL hat Priorität
isRecurring: isRecurring || false
})
};
} catch (stripeError) {
console.error("Stripe error:", stripeError);
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: {
message: `Stripe error: ${stripeError.message}`,
stripeCode: stripeError.code || 'unknown'
}
})
};
}
} catch (error) {
console.error("Function error:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: {
message: error.message || "Unknown server error"
}
})
};
}
};
// Handle OPTIONS request (CORS preflight)
if (event.httpMethod === 'OPTIONS') {
return {
statusCode: 200,
headers,
body: '',
};
}
try {
// Parse request
const data = JSON.parse(event.body || '{}');
const { amount, isRecurring, priceId, coffeeSize } = data;
// Debug-Ausgabe
console.log('Request data:', { amount, isRecurring, priceId, coffeeSize });
console.log('Stripe key available:', !!process.env.STRIPE_SECRET_KEY);
// Fallback, wenn kein Stripe-Key verfügbar
if (!process.env.STRIPE_SECRET_KEY) {
console.log('WARNUNG: STRIPE_SECRET_KEY fehlt - liefere Test-Antwort');
return {
statusCode: 200,
headers,
body: JSON.stringify({
url: `${process.env.URL || 'https://bauntown.com'}/support-success?test=true`,
isRecurring: isRecurring || false,
message: 'Test mode - no Stripe key available',
}),
};
}
// Betrag in Cent umrechnen für Stripe
const amountInCents = Math.round(amount * 100);
let sessionConfig = {
payment_method_types: ['card'],
line_items: [
{
price_data: {
currency: 'eur',
product_data: {
name: `BaunTown Kaffee - ${coffeeSize || 'Mittlerer Kaffee'}`,
description: isRecurring ? 'Monatliche Unterstützung' : 'Einmalige Unterstützung',
},
unit_amount: amountInCents,
recurring: isRecurring ? { interval: 'month' } : undefined,
},
quantity: 1,
},
],
mode: isRecurring ? 'subscription' : 'payment',
success_url: `${process.env.URL || 'https://bauntown.com'}/support-success?amount=${amount}&type=${isRecurring ? 'recurring' : 'one-time'}&provider=stripe&coffeeSize=${encodeURIComponent(coffeeSize || 'Mittlerer Kaffee')}`,
cancel_url: `${process.env.URL || 'https://bauntown.com'}/support-cancel`,
metadata: {
isRecurring: isRecurring ? 'true' : 'false',
priceId: priceId || '',
coffeeSize: coffeeSize || 'Mittlerer Kaffee',
},
};
console.log('Creating checkout session with config:', JSON.stringify(sessionConfig));
try {
// Stripe Checkout Session erstellen
const session = await stripe.checkout.sessions.create(sessionConfig);
console.log('Session created:', session.id, 'URL:', session.url);
return {
statusCode: 200,
headers,
body: JSON.stringify({
url: session.url, // Die Redirect-URL hat Priorität
isRecurring: isRecurring || false,
}),
};
} catch (stripeError) {
console.error('Stripe error:', stripeError);
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: {
message: `Stripe error: ${stripeError.message}`,
stripeCode: stripeError.code || 'unknown',
},
}),
};
}
} catch (error) {
console.error('Function error:', error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: {
message: error.message || 'Unknown server error',
},
}),
};
}
};

View file

@ -5,140 +5,148 @@ const fetch = require('node-fetch');
// PayPal OAuth Token holen
async function getPayPalAccessToken() {
const auth = Buffer.from(`${process.env.PAYPAL_CLIENT_ID}:${process.env.PAYPAL_CLIENT_SECRET}`).toString('base64');
const response = await fetch('https://api-m.sandbox.paypal.com/v1/oauth2/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${auth}`
},
body: 'grant_type=client_credentials'
});
const data = await response.json();
return data.access_token;
const auth = Buffer.from(
`${process.env.PAYPAL_CLIENT_ID}:${process.env.PAYPAL_CLIENT_SECRET}`
).toString('base64');
const response = await fetch('https://api-m.sandbox.paypal.com/v1/oauth2/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${auth}`,
},
body: 'grant_type=client_credentials',
});
const data = await response.json();
return data.access_token;
}
// PayPal Order erstellen
async function createPayPalOrder(accessToken, amount, isRecurring, priceId, coffeeSize) {
const url = isRecurring
? 'https://api-m.sandbox.paypal.com/v1/billing/plans'
: 'https://api-m.sandbox.paypal.com/v2/checkout/orders';
if (isRecurring) {
// Vereinfachtes Beispiel für ein Abonnement
// In einer echten Anwendung sollten Sie Pläne vorab erstellen
const planData = {
name: `BaunTown Monthly ${coffeeSize}`,
description: `Monthly ${coffeeSize} support for BaunTown (${amount}€)`,
type: "FIXED",
payment_definitions: [
{
name: `Monthly ${coffeeSize}`,
type: "REGULAR",
frequency: "MONTH",
frequency_interval: "1",
amount: {
value: amount,
currency: "EUR"
},
cycles: "0"
}
],
merchant_preferences: {
return_url: `${process.env.URL || 'http://localhost:3000'}/support-success`,
cancel_url: `${process.env.URL || 'http://localhost:3000'}/support-cancel`
}
};
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify(planData)
});
return response.json();
} else {
// Normale einmalige Zahlung
const orderData = {
intent: "CAPTURE",
purchase_units: [
{
amount: {
currency_code: "EUR",
value: amount
},
description: `${coffeeSize} für BaunTown (${amount}€)`,
custom_id: priceId
}
],
application_context: {
return_url: `${process.env.URL || 'http://localhost:3000'}/support-success?amount=${amount}&type=${isRecurring ? 'recurring' : 'one-time'}&provider=paypal&coffeeSize=${encodeURIComponent(coffeeSize)}`,
cancel_url: `${process.env.URL || 'http://localhost:3000'}/support-cancel`
}
};
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
},
body: JSON.stringify(orderData)
});
return response.json();
}
const url = isRecurring
? 'https://api-m.sandbox.paypal.com/v1/billing/plans'
: 'https://api-m.sandbox.paypal.com/v2/checkout/orders';
if (isRecurring) {
// Vereinfachtes Beispiel für ein Abonnement
// In einer echten Anwendung sollten Sie Pläne vorab erstellen
const planData = {
name: `BaunTown Monthly ${coffeeSize}`,
description: `Monthly ${coffeeSize} support for BaunTown (${amount}€)`,
type: 'FIXED',
payment_definitions: [
{
name: `Monthly ${coffeeSize}`,
type: 'REGULAR',
frequency: 'MONTH',
frequency_interval: '1',
amount: {
value: amount,
currency: 'EUR',
},
cycles: '0',
},
],
merchant_preferences: {
return_url: `${process.env.URL || 'http://localhost:3000'}/support-success`,
cancel_url: `${process.env.URL || 'http://localhost:3000'}/support-cancel`,
},
};
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify(planData),
});
return response.json();
} else {
// Normale einmalige Zahlung
const orderData = {
intent: 'CAPTURE',
purchase_units: [
{
amount: {
currency_code: 'EUR',
value: amount,
},
description: `${coffeeSize} für BaunTown (${amount}€)`,
custom_id: priceId,
},
],
application_context: {
return_url: `${process.env.URL || 'http://localhost:3000'}/support-success?amount=${amount}&type=${isRecurring ? 'recurring' : 'one-time'}&provider=paypal&coffeeSize=${encodeURIComponent(coffeeSize)}`,
cancel_url: `${process.env.URL || 'http://localhost:3000'}/support-cancel`,
},
};
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify(orderData),
});
return response.json();
}
}
exports.handler = async (event, context) => {
// CORS Headers
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type",
"Access-Control-Allow-Methods": "POST, OPTIONS"
};
// Handle OPTIONS request (CORS preflight)
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: ""
};
}
// CORS Headers
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
};
try {
// Request-Body parsen
const data = JSON.parse(event.body);
const { amount, isRecurring, priceId, coffeeSize } = data;
// PayPal Access Token holen
const accessToken = await getPayPalAccessToken();
// Order oder Plan erstellen mit zusätzlichen Metadaten
const paypalResponse = await createPayPalOrder(accessToken, amount, isRecurring, priceId, coffeeSize);
return {
statusCode: 200,
headers,
body: JSON.stringify(paypalResponse)
};
} catch (error) {
console.error('PayPal error:', error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: {
message: error.message || 'Internal server error'
}
})
};
}
};
// Handle OPTIONS request (CORS preflight)
if (event.httpMethod === 'OPTIONS') {
return {
statusCode: 200,
headers,
body: '',
};
}
try {
// Request-Body parsen
const data = JSON.parse(event.body);
const { amount, isRecurring, priceId, coffeeSize } = data;
// PayPal Access Token holen
const accessToken = await getPayPalAccessToken();
// Order oder Plan erstellen mit zusätzlichen Metadaten
const paypalResponse = await createPayPalOrder(
accessToken,
amount,
isRecurring,
priceId,
coffeeSize
);
return {
statusCode: 200,
headers,
body: JSON.stringify(paypalResponse),
};
} catch (error) {
console.error('PayPal error:', error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: {
message: error.message || 'Internal server error',
},
}),
};
}
};

View file

@ -4,112 +4,114 @@ const crypto = require('crypto');
// Überprüfe, ob alle erforderlichen Umgebungsvariablen vorhanden sind
const requiredEnvVars = [
'GOOGLE_SERVICE_ACCOUNT_EMAIL',
'GOOGLE_PRIVATE_KEY',
'GOOGLE_SHEET_ID',
'SITE_URL'
'GOOGLE_SERVICE_ACCOUNT_EMAIL',
'GOOGLE_PRIVATE_KEY',
'GOOGLE_SHEET_ID',
'SITE_URL',
];
function checkEnvVars() {
const missing = requiredEnvVars.filter(varName => !process.env[varName]);
if (missing.length > 0) {
throw new Error(`Fehlende Umgebungsvariablen: ${missing.join(', ')}`);
}
const missing = requiredEnvVars.filter((varName) => !process.env[varName]);
if (missing.length > 0) {
throw new Error(`Fehlende Umgebungsvariablen: ${missing.join(', ')}`);
}
}
const auth = new JWT({
email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
key: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
key: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
});
const sheets = google.sheets({ version: 'v4', auth });
exports.handler = async function(event, context) {
console.log('Newsletter subscription request received');
// CORS Headers
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST, OPTIONS'
};
exports.handler = async function (event, context) {
console.log('Newsletter subscription request received');
// Handle OPTIONS request
if (event.httpMethod === 'OPTIONS') {
return {
statusCode: 200,
headers,
body: ''
};
}
// CORS Headers
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
};
if (event.httpMethod !== 'POST') {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: 'Method Not Allowed' })
};
}
// Handle OPTIONS request
if (event.httpMethod === 'OPTIONS') {
return {
statusCode: 200,
headers,
body: '',
};
}
try {
// Überprüfe Umgebungsvariablen
checkEnvVars();
// Parse request body
const { email } = JSON.parse(event.body);
if (!email) {
throw new Error('Email is required');
}
if (event.httpMethod !== 'POST') {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: 'Method Not Allowed' }),
};
}
console.log('Processing subscription for email:', email);
const timestamp = new Date().toISOString();
const unsubscribeToken = crypto.randomUUID();
try {
// Überprüfe Umgebungsvariablen
checkEnvVars();
// Füge neue Zeile zum Sheet hinzu
console.log('Adding row to Google Sheet');
try {
// Prüfe zuerst, ob das Sheet existiert und hole den ersten Tab-Namen
const sheetInfo = await sheets.spreadsheets.get({
spreadsheetId: process.env.GOOGLE_SHEET_ID
});
console.log('Sheet info:', sheetInfo.data);
// Hole den Namen des ersten Tabs
const firstSheetName = sheetInfo.data.sheets[0].properties.title;
console.log('Using sheet name:', firstSheetName);
// Füge die Zeile hinzu
await sheets.spreadsheets.values.append({
spreadsheetId: process.env.GOOGLE_SHEET_ID,
range: `${firstSheetName}!A:D`,
valueInputOption: 'USER_ENTERED',
requestBody: {
values: [[timestamp, email, 'active', unsubscribeToken]]
}
});
} catch (sheetError) {
console.error('Google Sheets error:', sheetError);
throw new Error(`Google Sheets Fehler: ${sheetError.message}. Bitte stellen Sie sicher, dass das Sheet existiert und der Service Account Zugriff hat.`);
}
// Parse request body
const { email } = JSON.parse(event.body);
if (!email) {
throw new Error('Email is required');
}
console.log('Subscription completed successfully');
return {
statusCode: 200,
headers,
body: JSON.stringify({ success: true })
};
} catch (error) {
console.error('Newsletter subscription error:', error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: error.message,
details: error.stack
})
};
}
};
console.log('Processing subscription for email:', email);
const timestamp = new Date().toISOString();
const unsubscribeToken = crypto.randomUUID();
// Füge neue Zeile zum Sheet hinzu
console.log('Adding row to Google Sheet');
try {
// Prüfe zuerst, ob das Sheet existiert und hole den ersten Tab-Namen
const sheetInfo = await sheets.spreadsheets.get({
spreadsheetId: process.env.GOOGLE_SHEET_ID,
});
console.log('Sheet info:', sheetInfo.data);
// Hole den Namen des ersten Tabs
const firstSheetName = sheetInfo.data.sheets[0].properties.title;
console.log('Using sheet name:', firstSheetName);
// Füge die Zeile hinzu
await sheets.spreadsheets.values.append({
spreadsheetId: process.env.GOOGLE_SHEET_ID,
range: `${firstSheetName}!A:D`,
valueInputOption: 'USER_ENTERED',
requestBody: {
values: [[timestamp, email, 'active', unsubscribeToken]],
},
});
} catch (sheetError) {
console.error('Google Sheets error:', sheetError);
throw new Error(
`Google Sheets Fehler: ${sheetError.message}. Bitte stellen Sie sicher, dass das Sheet existiert und der Service Account Zugriff hat.`
);
}
console.log('Subscription completed successfully');
return {
statusCode: 200,
headers,
body: JSON.stringify({ success: true }),
};
} catch (error) {
console.error('Newsletter subscription error:', error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: error.message,
details: error.stack,
}),
};
}
};

View file

@ -2,66 +2,66 @@ const { google } = require('googleapis');
const { JWT } = require('google-auth-library');
const auth = new JWT({
email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
key: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
key: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
});
const sheets = google.sheets({ version: 'v4', auth });
exports.handler = async function(event, context) {
if (event.httpMethod !== 'GET') {
return { statusCode: 405, body: 'Method Not Allowed' };
}
exports.handler = async function (event, context) {
if (event.httpMethod !== 'GET') {
return { statusCode: 405, body: 'Method Not Allowed' };
}
const { token } = event.queryStringParameters;
const { token } = event.queryStringParameters;
if (!token) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'Token is required' })
};
}
if (!token) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'Token is required' }),
};
}
try {
// Finde die Zeile mit dem Token
const response = await sheets.spreadsheets.values.get({
spreadsheetId: process.env.GOOGLE_SHEET_ID,
range: 'Sheet1!A:D',
});
try {
// Finde die Zeile mit dem Token
const response = await sheets.spreadsheets.values.get({
spreadsheetId: process.env.GOOGLE_SHEET_ID,
range: 'Sheet1!A:D',
});
const values = response.data.values;
const rowIndex = values.findIndex(row => row[3] === token);
const values = response.data.values;
const rowIndex = values.findIndex((row) => row[3] === token);
if (rowIndex === -1) {
return {
statusCode: 404,
body: JSON.stringify({ error: 'Token not found' })
};
}
if (rowIndex === -1) {
return {
statusCode: 404,
body: JSON.stringify({ error: 'Token not found' }),
};
}
// Aktualisiere den Status auf 'unsubscribed'
await sheets.spreadsheets.values.update({
spreadsheetId: process.env.GOOGLE_SHEET_ID,
range: `Sheet1!C${rowIndex + 1}`,
valueInputOption: 'USER_ENTERED',
requestBody: {
values: [['unsubscribed']]
}
});
// Aktualisiere den Status auf 'unsubscribed'
await sheets.spreadsheets.values.update({
spreadsheetId: process.env.GOOGLE_SHEET_ID,
range: `Sheet1!C${rowIndex + 1}`,
valueInputOption: 'USER_ENTERED',
requestBody: {
values: [['unsubscribed']],
},
});
// Redirect zur Bestätigungsseite
return {
statusCode: 302,
headers: {
Location: `${process.env.URL}/unsubscribe-success`
}
};
} catch (error) {
console.error('Newsletter unsubscribe error:', error);
return {
statusCode: 500,
body: JSON.stringify({ error: error.message })
};
}
};
// Redirect zur Bestätigungsseite
return {
statusCode: 302,
headers: {
Location: `${process.env.URL}/unsubscribe-success`,
},
};
} catch (error) {
console.error('Newsletter unsubscribe error:', error);
return {
statusCode: 500,
body: JSON.stringify({ error: error.message }),
};
}
};

View file

@ -1,10 +1,10 @@
{
"name": "bauntown-functions",
"version": "1.0.0",
"dependencies": {
"stripe": "^12.0.0",
"googleapis": "^128.0.0",
"google-auth-library": "^9.0.0",
"node-fetch": "^2.7.0"
}
}
"name": "bauntown-functions",
"version": "1.0.0",
"dependencies": {
"stripe": "^12.0.0",
"googleapis": "^128.0.0",
"google-auth-library": "^9.0.0",
"node-fetch": "^2.7.0"
}
}

View file

@ -1,145 +1,145 @@
// Netlify Function für Webhooks von Stripe und PayPal
exports.handler = async (event, context) => {
// CORS Headers
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type",
"Access-Control-Allow-Methods": "POST, OPTIONS"
};
// Handle OPTIONS request (CORS preflight)
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: ""
};
}
// CORS Headers
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
};
// Den Webhook-Source aus URL-Parameter oder Header bestimmen
const source = event.queryStringParameters.source || 'unknown';
try {
if (source === 'stripe') {
// Stripe Webhook verarbeiten
return processStripeWebhook(event, headers);
} else if (source === 'paypal') {
// PayPal Webhook verarbeiten
return processPayPalWebhook(event, headers);
} else {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: {
message: `Unknown webhook source: ${source}`
}
})
};
}
} catch (error) {
console.error(`Webhook error (${source}):`, error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: {
message: error.message || 'Internal server error'
}
})
};
}
// Handle OPTIONS request (CORS preflight)
if (event.httpMethod === 'OPTIONS') {
return {
statusCode: 200,
headers,
body: '',
};
}
// Den Webhook-Source aus URL-Parameter oder Header bestimmen
const source = event.queryStringParameters.source || 'unknown';
try {
if (source === 'stripe') {
// Stripe Webhook verarbeiten
return processStripeWebhook(event, headers);
} else if (source === 'paypal') {
// PayPal Webhook verarbeiten
return processPayPalWebhook(event, headers);
} else {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: {
message: `Unknown webhook source: ${source}`,
},
}),
};
}
} catch (error) {
console.error(`Webhook error (${source}):`, error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: {
message: error.message || 'Internal server error',
},
}),
};
}
};
// Stripe Webhook verarbeiten
async function processStripeWebhook(event, headers) {
// In einer echten Anwendung würden Sie hier die Stripe Signatur überprüfen
// const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
// const sig = event.headers['stripe-signature'];
// const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
try {
const data = JSON.parse(event.body);
// Verschiedene Ereignistypen verarbeiten
switch (data.type) {
case 'payment_intent.succeeded':
// Zahlung erfolgreich - hier könnte eine Datenbank aktualisiert werden
console.log('Payment succeeded:', data.data.object.id);
break;
case 'payment_intent.payment_failed':
// Zahlung fehlgeschlagen
console.log('Payment failed:', data.data.object.id);
break;
// Weitere Event-Typen hier verarbeiten
default:
console.log('Unhandled Stripe event type:', data.type);
}
return {
statusCode: 200,
headers,
body: JSON.stringify({ received: true })
};
} catch (error) {
console.error('Error processing Stripe webhook:', error);
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: {
message: error.message
}
})
};
}
// In einer echten Anwendung würden Sie hier die Stripe Signatur überprüfen
// const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
// const sig = event.headers['stripe-signature'];
// const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
try {
const data = JSON.parse(event.body);
// Verschiedene Ereignistypen verarbeiten
switch (data.type) {
case 'payment_intent.succeeded':
// Zahlung erfolgreich - hier könnte eine Datenbank aktualisiert werden
console.log('Payment succeeded:', data.data.object.id);
break;
case 'payment_intent.payment_failed':
// Zahlung fehlgeschlagen
console.log('Payment failed:', data.data.object.id);
break;
// Weitere Event-Typen hier verarbeiten
default:
console.log('Unhandled Stripe event type:', data.type);
}
return {
statusCode: 200,
headers,
body: JSON.stringify({ received: true }),
};
} catch (error) {
console.error('Error processing Stripe webhook:', error);
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: {
message: error.message,
},
}),
};
}
}
// PayPal Webhook verarbeiten
async function processPayPalWebhook(event, headers) {
// In einer echten Anwendung würden Sie hier die PayPal-Signatur verifizieren
try {
const data = JSON.parse(event.body);
// Verschiedene Ereignistypen verarbeiten
switch (data.event_type) {
case 'PAYMENT.CAPTURE.COMPLETED':
// Zahlung erfolgreich - hier könnte eine Datenbank aktualisiert werden
console.log('PayPal payment completed:', data.resource.id);
break;
case 'PAYMENT.CAPTURE.DENIED':
// Zahlung abgelehnt
console.log('PayPal payment denied:', data.resource.id);
break;
// Weitere Event-Typen hier verarbeiten
default:
console.log('Unhandled PayPal event type:', data.event_type);
}
return {
statusCode: 200,
headers,
body: JSON.stringify({ received: true })
};
} catch (error) {
console.error('Error processing PayPal webhook:', error);
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: {
message: error.message
}
})
};
}
}
// In einer echten Anwendung würden Sie hier die PayPal-Signatur verifizieren
try {
const data = JSON.parse(event.body);
// Verschiedene Ereignistypen verarbeiten
switch (data.event_type) {
case 'PAYMENT.CAPTURE.COMPLETED':
// Zahlung erfolgreich - hier könnte eine Datenbank aktualisiert werden
console.log('PayPal payment completed:', data.resource.id);
break;
case 'PAYMENT.CAPTURE.DENIED':
// Zahlung abgelehnt
console.log('PayPal payment denied:', data.resource.id);
break;
// Weitere Event-Typen hier verarbeiten
default:
console.log('Unhandled PayPal event type:', data.event_type);
}
return {
statusCode: 200,
headers,
body: JSON.stringify({ received: true }),
};
} catch (error) {
console.error('Error processing PayPal webhook:', error);
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: {
message: error.message,
},
}),
};
}
}

View file

@ -1,31 +1,31 @@
{
"name": "@bauntown/landing",
"type": "module",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"type-check": "astro check",
"astro": "astro"
},
"dependencies": {
"@astrojs/mdx": "^4.2.3",
"@astrojs/netlify": "^6.2.5",
"@astrojs/partytown": "^2.1.4",
"@astrojs/prefetch": "^0.4.1",
"@paypal/paypal-js": "^8.2.0",
"@stripe/stripe-js": "^6.1.0",
"astro": "^5.5.6",
"astro-i18n-aut": "^0.7.3",
"google-auth-library": "^9.15.1",
"googleapis": "^148.0.0",
"node-fetch": "^3.3.2",
"stripe": "^17.7.0"
},
"devDependencies": {
"@astrojs/check": "^0.9.0",
"typescript": "^5.7.2"
}
"name": "@bauntown/landing",
"type": "module",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"type-check": "astro check",
"astro": "astro"
},
"dependencies": {
"@astrojs/mdx": "^4.2.3",
"@astrojs/netlify": "^6.2.5",
"@astrojs/partytown": "^2.1.4",
"@astrojs/prefetch": "^0.4.1",
"@paypal/paypal-js": "^8.2.0",
"@stripe/stripe-js": "^6.1.0",
"astro": "^5.5.6",
"astro-i18n-aut": "^0.7.3",
"google-auth-library": "^9.15.1",
"googleapis": "^148.0.0",
"node-fetch": "^3.3.2",
"stripe": "^17.7.0"
},
"devDependencies": {
"@astrojs/check": "^0.9.0",
"typescript": "^5.7.2"
}
}

View file

@ -1,218 +1,231 @@
---
interface Props {
title: string;
description?: string;
image?: string;
variant?: 'primary' | 'secondary' | 'tertiary';
imageAlt?: string;
href?: string;
buttonText?: string;
className?: string;
title: string;
description?: string;
image?: string;
variant?: 'primary' | 'secondary' | 'tertiary';
imageAlt?: string;
href?: string;
buttonText?: string;
className?: string;
}
const {
title,
description,
image,
variant = 'primary',
imageAlt = '',
href,
buttonText,
className = ''
title,
description,
image,
variant = 'primary',
imageAlt = '',
href,
buttonText,
className = '',
} = Astro.props;
---
{href ? (
<a href={href} class="card-link">
<article class:list={["card", `card-${variant}`, className]}>
{image && (
<div class="card-image-container">
<img src={image} alt={imageAlt} class="card-image" />
</div>
)}
<div class="card-content">
<h3 class="card-title">{title}</h3>
{description && <p class="card-description">{description}</p>}
{buttonText && (
<div class="card-footer">
<span class="card-button">
{buttonText} <span class="arrow">→</span>
</span>
</div>
)}
</div>
</article>
</a>
) : (
<article class:list={["card", `card-${variant}`, className]}>
{image && (
<div class="card-image-container">
<img src={image} alt={imageAlt} class="card-image" />
</div>
)}
<div class="card-content">
<h3 class="card-title">{title}</h3>
{description && <p class="card-description">{description}</p>}
</div>
</article>
)}
{
href ? (
<a href={href} class="card-link">
<article class:list={['card', `card-${variant}`, className]}>
{image && (
<div class="card-image-container">
<img src={image} alt={imageAlt} class="card-image" />
</div>
)}
<div class="card-content">
<h3 class="card-title">{title}</h3>
{description && <p class="card-description">{description}</p>}
{buttonText && (
<div class="card-footer">
<span class="card-button">
{buttonText} <span class="arrow">→</span>
</span>
</div>
)}
</div>
</article>
</a>
) : (
<article class:list={['card', `card-${variant}`, className]}>
{image && (
<div class="card-image-container">
<img src={image} alt={imageAlt} class="card-image" />
</div>
)}
<div class="card-content">
<h3 class="card-title">{title}</h3>
{description && <p class="card-description">{description}</p>}
</div>
</article>
)
}
<style>
.card-link {
display: block;
text-decoration: none;
color: inherit;
height: 100%;
}
.card {
display: flex;
flex-direction: column;
border-radius: 1rem;
overflow: hidden;
height: 100%;
border: 2px solid rgba(255, 255, 255, 0.3);
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.card-link {
display: block;
text-decoration: none;
color: inherit;
height: 100%;
}
.card-link:hover .card {
transform: translateY(-2px);
box-shadow: 0 6px 12px -3px rgba(0, 0, 0, 0.1);
border-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.5);
}
.card {
display: flex;
flex-direction: column;
border-radius: 1rem;
overflow: hidden;
height: 100%;
border: 2px solid rgba(255, 255, 255, 0.3);
transition:
transform 0.2s ease,
box-shadow 0.2s ease,
border-color 0.2s ease;
}
/* Primary variant - accent background, white text */
.card-primary {
background-color: var(--accent-color);
color: white;
box-shadow: 0 4px 6px -1px rgba(249, 115, 22, 0.2), 0 2px 4px -2px rgba(249, 115, 22, 0.1);
}
.card-link:hover .card-primary {
box-shadow: 0 8px 10px -3px rgba(249, 115, 22, 0.25), 0 4px 6px -2px rgba(249, 115, 22, 0.15);
background-color: #ea580c; /* Slightly darker shade when hovered */
}
.card-primary .card-button {
background-color: white;
color: var(--accent-color);
display: inline-flex;
align-items: center;
}
.card-link:hover .card {
transform: translateY(-2px);
box-shadow: 0 6px 12px -3px rgba(0, 0, 0, 0.1);
border-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.5);
}
/* Secondary variant - white background, accent elements */
.card-secondary {
background-color: var(--card-bg);
color: var(--text-color);
border: 2px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.06), 0 1px 2px -1px rgba(0, 0, 0, 0.03);
}
.card-link:hover .card-secondary {
box-shadow: 0 6px 12px -2px rgba(0, 0, 0, 0.08), 0 3px 5px -2px rgba(0, 0, 0, 0.04);
}
.card-secondary .card-button {
background-color: var(--accent-color);
color: white;
display: inline-flex;
align-items: center;
}
/* Primary variant - accent background, white text */
.card-primary {
background-color: var(--accent-color);
color: white;
box-shadow:
0 4px 6px -1px rgba(249, 115, 22, 0.2),
0 2px 4px -2px rgba(249, 115, 22, 0.1);
}
/* Tertiary variant - minimal design */
.card-tertiary {
background-color: var(--card-bg);
box-shadow: none;
border: 2px solid rgba(255, 255, 255, 0.3);
}
.card-link:hover .card-tertiary {
background-color: var(--card-bg);
}
.card-tertiary .card-button {
color: var(--accent-color);
display: inline-flex;
align-items: center;
}
.card-link:hover .card-primary {
box-shadow:
0 8px 10px -3px rgba(249, 115, 22, 0.25),
0 4px 6px -2px rgba(249, 115, 22, 0.15);
background-color: #ea580c; /* Slightly darker shade when hovered */
}
.card-image-container {
width: 100%;
padding-top: 66.67%; /* Aspect ratio 3:2 */
position: relative;
overflow: hidden;
}
.card-primary .card-button {
background-color: white;
color: var(--accent-color);
display: inline-flex;
align-items: center;
}
.card-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
/* Secondary variant - white background, accent elements */
.card-secondary {
background-color: var(--card-bg);
color: var(--text-color);
border: 2px solid rgba(255, 255, 255, 0.3);
box-shadow:
0 2px 4px -1px rgba(0, 0, 0, 0.06),
0 1px 2px -1px rgba(0, 0, 0, 0.03);
}
.card-content {
display: flex;
flex-direction: column;
padding: 1.5rem;
flex-grow: 1;
}
.card-link:hover .card-secondary {
box-shadow:
0 6px 12px -2px rgba(0, 0, 0, 0.08),
0 3px 5px -2px rgba(0, 0, 0, 0.04);
}
.card-title {
font-size: 1.5rem;
font-weight: 700;
margin: 0 0 1rem 0;
line-height: 1.3;
}
.card-secondary .card-button {
background-color: var(--accent-color);
color: white;
display: inline-flex;
align-items: center;
}
.card-description {
font-size: 1rem;
line-height: 1.6;
margin: 0 0 1.5rem 0;
flex-grow: 1;
opacity: 0.85;
}
/* Tertiary variant - minimal design */
.card-tertiary {
background-color: var(--card-bg);
box-shadow: none;
border: 2px solid rgba(255, 255, 255, 0.3);
}
.card-footer {
margin-top: auto;
display: flex;
justify-content: flex-start;
}
.card-link:hover .card-tertiary {
background-color: var(--card-bg);
}
.card-button {
display: inline-flex;
align-items: center;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 600;
text-align: center;
transition: all 0.2s ease;
}
.arrow {
display: inline-block;
margin-left: 8px;
transition: transform 0.2s ease;
}
.card-link:hover .arrow {
transform: translateX(4px);
}
.card-tertiary .card-button {
color: var(--accent-color);
display: inline-flex;
align-items: center;
}
@media (max-width: 768px) {
.card-content {
padding: 1.25rem;
}
.card-title {
font-size: 1.25rem;
}
}
</style>
.card-image-container {
width: 100%;
padding-top: 66.67%; /* Aspect ratio 3:2 */
position: relative;
overflow: hidden;
}
.card-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
.card-content {
display: flex;
flex-direction: column;
padding: 1.5rem;
flex-grow: 1;
}
.card-title {
font-size: 1.5rem;
font-weight: 700;
margin: 0 0 1rem 0;
line-height: 1.3;
}
.card-description {
font-size: 1rem;
line-height: 1.6;
margin: 0 0 1.5rem 0;
flex-grow: 1;
opacity: 0.85;
}
.card-footer {
margin-top: auto;
display: flex;
justify-content: flex-start;
}
.card-button {
display: inline-flex;
align-items: center;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 600;
text-align: center;
transition: all 0.2s ease;
}
.arrow {
display: inline-block;
margin-left: 8px;
transition: transform 0.2s ease;
}
.card-link:hover .arrow {
transform: translateX(4px);
}
@media (max-width: 768px) {
.card-content {
padding: 1.25rem;
}
.card-title {
font-size: 1.25rem;
}
}
</style>

View file

@ -1,386 +1,360 @@
---
import { getLangFromUrl, useTranslations } from "../utils/i18n";
import { getLangFromUrl, useTranslations } from '../utils/i18n';
interface Props {
defaultType?: "mission" | "tutorial" | "vision";
showTitle?: boolean;
customTitle?: string;
customDescription?: string;
defaultType?: 'mission' | 'tutorial' | 'vision';
showTitle?: boolean;
customTitle?: string;
customDescription?: string;
}
const {
defaultType = "mission",
showTitle = true,
customTitle,
customDescription,
} = Astro.props;
const { defaultType = 'mission', showTitle = true, customTitle, customDescription } = Astro.props;
const lang = getLangFromUrl(Astro.url);
const t = useTranslations(lang);
---
<div class="content-submission">
{showTitle && <h2>{customTitle || t("join.submissionTitle")}</h2>}
<p class="submission-description">
{customDescription || t("join.submissionDesc")}
</p>
{showTitle && <h2>{customTitle || t('join.submissionTitle')}</h2>}
<p class="submission-description">
{customDescription || t('join.submissionDesc')}
</p>
<form class="submission-form" id="contentSubmissionForm">
<div class="form-group">
<label for="contentType">{t("join.submissionType")}</label>
<select id="contentType" name="contentType" required>
<option value="mission" selected={defaultType === "mission"}
>{t("join.submissionMission")}</option
>
<option value="tutorial" selected={defaultType === "tutorial"}
>{t("join.submissionTutorial")}</option
>
<option value="vision" selected={defaultType === "vision"}
>{t("join.submissionVision")}</option
>
</select>
</div>
<form class="submission-form" id="contentSubmissionForm">
<div class="form-group">
<label for="contentType">{t('join.submissionType')}</label>
<select id="contentType" name="contentType" required>
<option value="mission" selected={defaultType === 'mission'}
>{t('join.submissionMission')}</option
>
<option value="tutorial" selected={defaultType === 'tutorial'}
>{t('join.submissionTutorial')}</option
>
<option value="vision" selected={defaultType === 'vision'}
>{t('join.submissionVision')}</option
>
</select>
</div>
<div class="form-group">
<label for="title">{t("join.submissionTitle")}</label>
<input
type="text"
id="title"
name="title"
placeholder={t("join.submissionTitlePlaceholder")}
required
/>
</div>
<div class="form-group">
<label for="title">{t('join.submissionTitle')}</label>
<input
type="text"
id="title"
name="title"
placeholder={t('join.submissionTitlePlaceholder')}
required
/>
</div>
<div class="form-group">
<label for="description">{t("join.submissionDesc")}</label>
<textarea
id="description"
name="description"
placeholder={t("join.submissionDescPlaceholder")}
rows="4"
required></textarea>
</div>
<div class="form-group">
<label for="description">{t('join.submissionDesc')}</label>
<textarea
id="description"
name="description"
placeholder={t('join.submissionDescPlaceholder')}
rows="4"
required></textarea>
</div>
<div class="form-group">
<label for="email">{t("join.submissionEmail")}</label>
<input
type="email"
id="email"
name="email"
placeholder={t("join.submissionEmailPlaceholder")}
required
/>
</div>
<div class="form-group">
<label for="email">{t('join.submissionEmail')}</label>
<input
type="email"
id="email"
name="email"
placeholder={t('join.submissionEmailPlaceholder')}
required
/>
</div>
<button type="submit" class="submit-button"
>{t("join.submissionSubmit")}</button
>
</form>
<button type="submit" class="submit-button">{t('join.submissionSubmit')}</button>
</form>
<div id="submissionSuccess" class="submission-success" style="display: none;">
<p class="success-title">{t("join.submissionSuccessTitle")}</p>
<p class="success-message">{t("join.submissionSuccessMessage")}</p>
</div>
<div id="submissionSuccess" class="submission-success" style="display: none;">
<p class="success-title">{t('join.submissionSuccessTitle')}</p>
<p class="success-message">{t('join.submissionSuccessMessage')}</p>
</div>
</div>
<style>
.content-submission {
background-color: var(--card-bg);
border-radius: 1rem;
padding: 2.5rem;
border: 1px solid var(--border-color);
margin-bottom: 2rem;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05);
transition: box-shadow 0.3s ease;
}
.content-submission {
background-color: var(--card-bg);
border-radius: 1rem;
padding: 2.5rem;
border: 1px solid var(--border-color);
margin-bottom: 2rem;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05);
transition: box-shadow 0.3s ease;
}
/* Heller Modus */
:root:not(.dark) .content-submission {
background-color: var(--card-bg);
}
/* Heller Modus */
:root:not(.dark) .content-submission {
background-color: var(--card-bg);
}
.content-submission:hover {
box-shadow: 0 15px 30px -5px rgba(0, 0, 0, 0.08);
}
.content-submission:hover {
box-shadow: 0 15px 30px -5px rgba(0, 0, 0, 0.08);
}
h2 {
margin-top: 0;
margin-bottom: 1rem;
font-size: 1.75rem;
background: linear-gradient(90deg, var(--accent-color), #fb923c);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
display: inline-block;
position: relative;
}
h2 {
margin-top: 0;
margin-bottom: 1rem;
font-size: 1.75rem;
background: linear-gradient(90deg, var(--accent-color), #fb923c);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
display: inline-block;
position: relative;
}
h2::after {
content: "";
position: absolute;
left: 0;
bottom: -0.25rem;
width: 100%;
height: 3px;
background: linear-gradient(90deg, var(--accent-color), #fb923c);
border-radius: 1.5px;
}
h2::after {
content: '';
position: absolute;
left: 0;
bottom: -0.25rem;
width: 100%;
height: 3px;
background: linear-gradient(90deg, var(--accent-color), #fb923c);
border-radius: 1.5px;
}
.submission-description {
margin-bottom: 2rem;
color: var(--text-muted);
font-size: 1.1rem;
line-height: 1.6;
max-width: 600px;
}
.submission-description {
margin-bottom: 2rem;
color: var(--text-muted);
font-size: 1.1rem;
line-height: 1.6;
max-width: 600px;
}
.submission-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.submission-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
label {
font-weight: 600;
font-size: 0.95rem;
color: var(--text-color);
}
label {
font-weight: 600;
font-size: 0.95rem;
color: var(--text-color);
}
input,
select,
textarea {
padding: 0.9rem 1rem;
border-radius: 0.5rem;
border: 1.5px solid var(--border-color);
background-color: var(--background-color);
color: var(--text-color);
font-size: 1rem;
font-family: inherit;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02);
}
input,
select,
textarea {
padding: 0.9rem 1rem;
border-radius: 0.5rem;
border: 1.5px solid var(--border-color);
background-color: var(--background-color);
color: var(--text-color);
font-size: 1rem;
font-family: inherit;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02);
}
/* Heller Modus für Eingabefelder */
:root:not(.dark) input,
:root:not(.dark) select,
:root:not(.dark) textarea {
background-color: var(--background-color);
}
/* Heller Modus für Eingabefelder */
:root:not(.dark) input,
:root:not(.dark) select,
:root:not(.dark) textarea {
background-color: var(--background-color);
}
input:hover,
select:hover,
textarea:hover {
border-color: var(--accent-color);
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.04);
}
input:hover,
select:hover,
textarea:hover {
border-color: var(--accent-color);
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.04);
}
input:focus,
select:focus,
textarea:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 3px rgba(var(--accent-color-rgb), 0.15);
}
input:focus,
select:focus,
textarea:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 3px rgba(var(--accent-color-rgb), 0.15);
}
textarea {
min-height: 120px;
resize: vertical;
}
textarea {
min-height: 120px;
resize: vertical;
}
.submit-button {
background: linear-gradient(90deg, var(--accent-color), #fb923c);
color: white;
border: none;
border-radius: 0.5rem;
padding: 0.9rem 2rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
margin-top: 1rem;
align-self: flex-start;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.submit-button {
background: linear-gradient(90deg, var(--accent-color), #fb923c);
color: white;
border: none;
border-radius: 0.5rem;
padding: 0.9rem 2rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
margin-top: 1rem;
align-self: flex-start;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.submit-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15);
}
.submit-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15);
}
.submit-button:active {
transform: translateY(1px);
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1);
}
.submit-button:active {
transform: translateY(1px);
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1);
}
.submission-success {
background-color: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.3);
border-radius: 0.75rem;
padding: 2rem;
margin-top: 2rem;
text-align: center;
animation: fadeIn 0.5s ease-in-out;
box-shadow: 0 4px 10px rgba(16, 185, 129, 0.1);
}
.submission-success {
background-color: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.3);
border-radius: 0.75rem;
padding: 2rem;
margin-top: 2rem;
text-align: center;
animation: fadeIn 0.5s ease-in-out;
box-shadow: 0 4px 10px rgba(16, 185, 129, 0.1);
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.submission-success .success-title {
color: #10b981;
margin: 0 0 1rem 0;
font-weight: 700;
font-size: 1.5rem;
}
.submission-success .success-title {
color: #10b981;
margin: 0 0 1rem 0;
font-weight: 700;
font-size: 1.5rem;
}
.submission-success .success-message {
color: var(--text-color);
margin: 0;
font-size: 1.05rem;
line-height: 1.7;
}
.submission-success .success-message {
color: var(--text-color);
margin: 0;
font-size: 1.05rem;
line-height: 1.7;
}
@media (max-width: 768px) {
.content-submission {
padding: 1.5rem;
}
@media (max-width: 768px) {
.content-submission {
padding: 1.5rem;
}
h2 {
font-size: 1.5rem;
}
h2 {
font-size: 1.5rem;
}
.submission-description {
font-size: 1rem;
}
.submission-description {
font-size: 1rem;
}
.submit-button {
width: 100%;
justify-content: center;
text-align: center;
display: flex;
}
}
.submit-button {
width: 100%;
justify-content: center;
text-align: center;
display: flex;
}
}
</style>
<script>
declare global {
interface Window {
plausible: (
eventName: string,
options?: { props?: Record<string, string> }
) => void;
}
}
declare global {
interface Window {
plausible: (eventName: string, options?: { props?: Record<string, string> }) => void;
}
}
document.addEventListener("DOMContentLoaded", () => {
const form = document.getElementById(
"contentSubmissionForm"
) as HTMLFormElement;
const successMessage = document.getElementById("submissionSuccess");
const contentType = document.getElementById(
"contentType"
) as HTMLSelectElement;
const title = document.getElementById("title") as HTMLInputElement;
const description = document.getElementById(
"description"
) as HTMLTextAreaElement;
const email = document.getElementById("email") as HTMLInputElement;
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('contentSubmissionForm') as HTMLFormElement;
const successMessage = document.getElementById('submissionSuccess');
const contentType = document.getElementById('contentType') as HTMLSelectElement;
const title = document.getElementById('title') as HTMLInputElement;
const description = document.getElementById('description') as HTMLTextAreaElement;
const email = document.getElementById('email') as HTMLInputElement;
if (
!form ||
!successMessage ||
!contentType ||
!title ||
!description ||
!email
)
return;
if (!form || !successMessage || !contentType || !title || !description || !email) return;
form.addEventListener("submit", async (e) => {
e.preventDefault();
form.addEventListener('submit', async (e) => {
e.preventDefault();
// Sammle Formulardaten
const formData = {
contentType: contentType.value,
title: title.value,
description: description.value,
email: email.value,
};
// Sammle Formulardaten
const formData = {
contentType: contentType.value,
title: title.value,
description: description.value,
email: email.value,
};
// Track content submission attempt
if (typeof window.plausible === "function") {
window.plausible("content-submit", {
props: {
type: contentType.value,
title: title.value,
email: email.value,
},
});
}
// Track content submission attempt
if (typeof window.plausible === 'function') {
window.plausible('content-submit', {
props: {
type: contentType.value,
title: title.value,
email: email.value,
},
});
}
try {
// Sende die Daten an die Netlify-Funktion
const response = await fetch("/.netlify/functions/content-submission", {
method: "POST",
body: JSON.stringify(formData),
});
try {
// Sende die Daten an die Netlify-Funktion
const response = await fetch('/.netlify/functions/content-submission', {
method: 'POST',
body: JSON.stringify(formData),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || "Submission failed");
}
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Submission failed');
}
// Bei erfolgreicher Übermittlung
form.reset();
successMessage.style.display = "block";
// Bei erfolgreicher Übermittlung
form.reset();
successMessage.style.display = 'block';
// Track successful submission
if (typeof window.plausible === "function") {
window.plausible("content-success", {
props: {
type: contentType.value,
title: title.value,
email: email.value,
},
});
}
// Track successful submission
if (typeof window.plausible === 'function') {
window.plausible('content-success', {
props: {
type: contentType.value,
title: title.value,
email: email.value,
},
});
}
// Erfolgs-Nachricht nach 8 Sekunden ausblenden
setTimeout(() => {
successMessage.style.display = "none";
}, 8000);
} catch (error: unknown) {
// Track error
if (typeof window.plausible === "function") {
window.plausible("content-error", {
props: {
type: contentType.value,
title: title.value,
email: email.value,
error: error instanceof Error ? error.message : String(error),
},
});
}
// Erfolgs-Nachricht nach 8 Sekunden ausblenden
setTimeout(() => {
successMessage.style.display = 'none';
}, 8000);
} catch (error: unknown) {
// Track error
if (typeof window.plausible === 'function') {
window.plausible('content-error', {
props: {
type: contentType.value,
title: title.value,
email: email.value,
error: error instanceof Error ? error.message : String(error),
},
});
}
// Fehlermeldung anzeigen
alert(
"Es gab einen Fehler bei der Übermittlung. Bitte versuche es später erneut."
);
}
});
});
// Fehlermeldung anzeigen
alert('Es gab einen Fehler bei der Übermittlung. Bitte versuche es später erneut.');
}
});
});
</script>

View file

@ -1,108 +1,122 @@
---
// Component for tracking content interactions
interface Props {
contentType: 'tutorial' | 'project' | 'tool' | 'model' | 'mission' | 'vision';
contentId?: string;
contentTitle?: string;
contentType: 'tutorial' | 'project' | 'tool' | 'model' | 'mission' | 'vision';
contentId?: string;
contentTitle?: string;
}
const { contentType, contentId, contentTitle } = Astro.props;
---
<script define:vars={{ contentType, contentId, contentTitle }}>
import { trackEvent, EVENTS } from '../scripts/analytics';
// Track page view for content
document.addEventListener('DOMContentLoaded', () => {
const eventMap = {
'tutorial': EVENTS.TUTORIAL_VIEW,
'project': EVENTS.PROJECT_VIEW,
'tool': EVENTS.TOOL_VIEW,
'model': EVENTS.MODEL_VIEW,
'mission': EVENTS.MISSION_VIEW,
'vision': 'vision-view'
};
const eventName = eventMap[contentType];
if (eventName) {
trackEvent(eventName, {
id: contentId || 'unknown',
title: contentTitle || document.title,
language: document.documentElement.lang
});
}
// Track video plays
document.querySelectorAll('iframe[src*="youtube"], iframe[src*="vimeo"]').forEach(video => {
// Create intersection observer to track when video comes into view
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const src = (entry.target as HTMLIFrameElement).src;
trackEvent(EVENTS.VIDEO_PLAY, {
source: src.includes('youtube') ? 'youtube' : 'vimeo',
url: src,
contentType,
contentId
});
observer.unobserve(entry.target);
}
});
}, { threshold: 0.5 });
observer.observe(video);
});
// Track Figma embeds
document.querySelectorAll('iframe[src*="figma.com"]').forEach(embed => {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
trackEvent(EVENTS.FIGMA_EMBED_VIEW, {
url: (entry.target as HTMLIFrameElement).src,
contentType,
contentId
});
observer.unobserve(entry.target);
}
});
}, { threshold: 0.5 });
observer.observe(embed);
});
// Track code copy (if there are code blocks with copy buttons)
document.querySelectorAll('.copy-code-button, [data-copy]').forEach(button => {
button.addEventListener('click', () => {
trackEvent(EVENTS.CODE_COPY, {
contentType,
contentId,
location: window.location.pathname
});
});
});
// Track card clicks on listing pages
document.querySelectorAll('.tutorial-card, .project-card, .tool-card, .model-card, .mission-card').forEach(card => {
card.addEventListener('click', (e) => {
const target = e.currentTarget as HTMLElement;
const link = target.querySelector('a');
const title = target.querySelector('h3, h4')?.textContent?.trim() || '';
if (link) {
const cardType = target.className.includes('tutorial') ? 'tutorial' :
target.className.includes('project') ? 'project' :
target.className.includes('tool') ? 'tool' :
target.className.includes('model') ? 'model' :
target.className.includes('mission') ? 'mission' : 'unknown';
trackEvent(`${cardType}-card-click`, {
title,
destination: link.href,
fromPage: window.location.pathname
});
}
});
});
});
</script>
import { trackEvent, EVENTS } from '../scripts/analytics';
// Track page view for content
document.addEventListener('DOMContentLoaded', () => {
const eventMap = {
tutorial: EVENTS.TUTORIAL_VIEW,
project: EVENTS.PROJECT_VIEW,
tool: EVENTS.TOOL_VIEW,
model: EVENTS.MODEL_VIEW,
mission: EVENTS.MISSION_VIEW,
vision: 'vision-view',
};
const eventName = eventMap[contentType];
if (eventName) {
trackEvent(eventName, {
id: contentId || 'unknown',
title: contentTitle || document.title,
language: document.documentElement.lang,
});
}
// Track video plays
document.querySelectorAll('iframe[src*="youtube"], iframe[src*="vimeo"]').forEach((video) => {
// Create intersection observer to track when video comes into view
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const src = (entry.target as HTMLIFrameElement).src;
trackEvent(EVENTS.VIDEO_PLAY, {
source: src.includes('youtube') ? 'youtube' : 'vimeo',
url: src,
contentType,
contentId,
});
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.5 }
);
observer.observe(video);
});
// Track Figma embeds
document.querySelectorAll('iframe[src*="figma.com"]').forEach((embed) => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
trackEvent(EVENTS.FIGMA_EMBED_VIEW, {
url: (entry.target as HTMLIFrameElement).src,
contentType,
contentId,
});
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.5 }
);
observer.observe(embed);
});
// Track code copy (if there are code blocks with copy buttons)
document.querySelectorAll('.copy-code-button, [data-copy]').forEach((button) => {
button.addEventListener('click', () => {
trackEvent(EVENTS.CODE_COPY, {
contentType,
contentId,
location: window.location.pathname,
});
});
});
// Track card clicks on listing pages
document
.querySelectorAll('.tutorial-card, .project-card, .tool-card, .model-card, .mission-card')
.forEach((card) => {
card.addEventListener('click', (e) => {
const target = e.currentTarget as HTMLElement;
const link = target.querySelector('a');
const title = target.querySelector('h3, h4')?.textContent?.trim() || '';
if (link) {
const cardType = target.className.includes('tutorial')
? 'tutorial'
: target.className.includes('project')
? 'project'
: target.className.includes('tool')
? 'tool'
: target.className.includes('model')
? 'model'
: target.className.includes('mission')
? 'mission'
: 'unknown';
trackEvent(`${cardType}-card-click`, {
title,
destination: link.href,
fromPage: window.location.pathname,
});
}
});
});
});
</script>

View file

@ -1,61 +1,75 @@
---
interface Props {
figmaUrl: string;
title?: string;
height?: string;
figmaUrl: string;
title?: string;
height?: string;
}
const { figmaUrl, title = "Figma Design", height = "450px" } = Astro.props;
const { figmaUrl, title = 'Figma Design', height = '450px' } = Astro.props;
---
<div class="figma-container">
<h3 class="figma-title">{title}</h3>
<a href={figmaUrl} target="_blank" rel="noopener noreferrer" class="figma-button">
<svg width="20" height="20" viewBox="0 0 38 57" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 28.5C19 25.9804 20.0009 23.5641 21.7825 21.7825C23.5641 20.0009 25.9804 19 28.5 19C31.0196 19 33.4359 20.0009 35.2175 21.7825C36.9991 23.5641 38 25.9804 38 28.5C38 31.0196 36.9991 33.4359 35.2175 35.2175C33.4359 36.9991 31.0196 38 28.5 38C25.9804 38 23.5641 36.9991 21.7825 35.2175C20.0009 33.4359 19 31.0196 19 28.5Z" fill="#1ABCFE"/>
<path d="M0 47.5C0 44.9804 1.00089 42.5641 2.78249 40.7825C4.56408 39.0009 6.98044 38 9.5 38H19V47.5C19 50.0196 17.9991 52.4359 16.2175 54.2175C14.4359 55.9991 12.0196 57 9.5 57C6.98044 57 4.56408 55.9991 2.78249 54.2175C1.00089 52.4359 0 50.0196 0 47.5Z" fill="#0ACF83"/>
<path d="M19 0V19H28.5C31.0196 19 33.4359 17.9991 35.2175 16.2175C36.9991 14.4359 38 12.0196 38 9.5C38 6.98044 36.9991 4.56408 35.2175 2.78249C33.4359 1.00089 31.0196 0 28.5 0H19Z" fill="#FF7262"/>
<path d="M0 9.5C0 12.0196 1.00089 14.4359 2.78249 16.2175C4.56408 17.9991 6.98044 19 9.5 19H19V0H9.5C6.98044 0 4.56408 1.00089 2.78249 2.78249C1.00089 4.56408 0 6.98044 0 9.5Z" fill="#F24E1E"/>
<path d="M0 28.5C0 31.0196 1.00089 33.4359 2.78249 35.2175C4.56408 36.9991 6.98044 38 9.5 38H19V19H9.5C6.98044 19 4.56408 20.0009 2.78249 21.7825C1.00089 23.5641 0 25.9804 0 28.5Z" fill="#A259FF"/>
</svg>
Open in Figma
</a>
<h3 class="figma-title">{title}</h3>
<a href={figmaUrl} target="_blank" rel="noopener noreferrer" class="figma-button">
<svg width="20" height="20" viewBox="0 0 38 57" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M19 28.5C19 25.9804 20.0009 23.5641 21.7825 21.7825C23.5641 20.0009 25.9804 19 28.5 19C31.0196 19 33.4359 20.0009 35.2175 21.7825C36.9991 23.5641 38 25.9804 38 28.5C38 31.0196 36.9991 33.4359 35.2175 35.2175C33.4359 36.9991 31.0196 38 28.5 38C25.9804 38 23.5641 36.9991 21.7825 35.2175C20.0009 33.4359 19 31.0196 19 28.5Z"
fill="#1ABCFE"></path>
<path
d="M0 47.5C0 44.9804 1.00089 42.5641 2.78249 40.7825C4.56408 39.0009 6.98044 38 9.5 38H19V47.5C19 50.0196 17.9991 52.4359 16.2175 54.2175C14.4359 55.9991 12.0196 57 9.5 57C6.98044 57 4.56408 55.9991 2.78249 54.2175C1.00089 52.4359 0 50.0196 0 47.5Z"
fill="#0ACF83"></path>
<path
d="M19 0V19H28.5C31.0196 19 33.4359 17.9991 35.2175 16.2175C36.9991 14.4359 38 12.0196 38 9.5C38 6.98044 36.9991 4.56408 35.2175 2.78249C33.4359 1.00089 31.0196 0 28.5 0H19Z"
fill="#FF7262"></path>
<path
d="M0 9.5C0 12.0196 1.00089 14.4359 2.78249 16.2175C4.56408 17.9991 6.98044 19 9.5 19H19V0H9.5C6.98044 0 4.56408 1.00089 2.78249 2.78249C1.00089 4.56408 0 6.98044 0 9.5Z"
fill="#F24E1E"></path>
<path
d="M0 28.5C0 31.0196 1.00089 33.4359 2.78249 35.2175C4.56408 36.9991 6.98044 38 9.5 38H19V19H9.5C6.98044 19 4.56408 20.0009 2.78249 21.7825C1.00089 23.5641 0 25.9804 0 28.5Z"
fill="#A259FF"></path>
</svg>
Open in Figma
</a>
</div>
<style define:vars={{ height }}>
.figma-container {
margin: 2.5rem 0;
text-align: center;
}
.figma-title {
font-size: 1.25rem;
margin-bottom: 1rem;
color: var(--text-color);
}
.figma-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
background-color: #0acf83;
color: white;
border-radius: 0.5rem;
font-weight: 600;
text-decoration: none;
transition: transform 0.2s, background-color 0.2s;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.figma-button:hover {
background-color: #09b975;
transform: translateY(-2px);
}
.figma-button svg {
width: 20px;
height: 20px;
}
</style>
.figma-container {
margin: 2.5rem 0;
text-align: center;
}
.figma-title {
font-size: 1.25rem;
margin-bottom: 1rem;
color: var(--text-color);
}
.figma-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
background-color: #0acf83;
color: white;
border-radius: 0.5rem;
font-weight: 600;
text-decoration: none;
transition:
transform 0.2s,
background-color 0.2s;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.figma-button:hover {
background-color: #09b975;
transform: translateY(-2px);
}
.figma-button svg {
width: 20px;
height: 20px;
}
</style>

View file

@ -1,149 +1,176 @@
---
interface Props {
title: string;
description?: string;
image?: string;
imageAlt?: string;
title: string;
description?: string;
image?: string;
imageAlt?: string;
}
const {
title,
description,
image,
imageAlt = "Hero image"
} = Astro.props;
const { title, description, image, imageAlt = 'Hero image' } = Astro.props;
---
<div class="hero-section">
<div class="hero-text">
<svg width="60" height="60" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" class="hero-logo-svg" style="display:block;margin:0 auto 2rem auto;"><rect x="0.666667" y="7.33334" width="10.6667" height="2.66667" stroke="currentColor" stroke-width="0.8" fill="none"/><rect x="2" y="4.66666" width="8" height="2.66667" stroke="currentColor" stroke-width="0.8" fill="none"/><rect x="3.33333" y="2" width="5.33333" height="2.66667" stroke="currentColor" stroke-width="0.8" fill="none"/></svg>
<h1>{title}</h1>
{description && <p class="hero-description">{description}</p>}
<slot name="content" />
</div>
{image && (
<div class="hero-image">
<img src={image} alt={imageAlt} />
</div>
)}
<div class="hero-text">
<svg
width="60"
height="60"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="hero-logo-svg"
style="display:block;margin:0 auto 2rem auto;"
><rect
x="0.666667"
y="7.33334"
width="10.6667"
height="2.66667"
stroke="currentColor"
stroke-width="0.8"
fill="none"></rect><rect
x="2"
y="4.66666"
width="8"
height="2.66667"
stroke="currentColor"
stroke-width="0.8"
fill="none"></rect><rect
x="3.33333"
y="2"
width="5.33333"
height="2.66667"
stroke="currentColor"
stroke-width="0.8"
fill="none"></rect></svg
>
<h1>{title}</h1>
{description && <p class="hero-description">{description}</p>}
<slot name="content" />
</div>
{
image && (
<div class="hero-image">
<img src={image} alt={imageAlt} />
</div>
)
}
</div>
<style is:global>
/* Hero Section Styles */
.hero-section {
display: flex;
align-items: center;
justify-content: space-between;
margin: 120px 0 4rem;
padding: 0.5rem 0 0;
gap: 4rem;
}
/* Hero Section Styles */
.hero-section {
display: flex;
align-items: center;
justify-content: space-between;
margin: 120px 0 4rem;
padding: 0.5rem 0 0;
gap: 4rem;
}
.hero-section .hero-text {
flex: 1;
text-align: center;
}
.hero-logo {
margin-bottom: 2rem;
margin-top: 0;
display: block;
margin-left: auto;
margin-right: auto;
width: 60px;
height: 60px;
transition: filter 0.2s;
}
@media (prefers-color-scheme: dark) {
.hero-logo {
filter: invert(1) brightness(2);
}
}
.hero-section .hero-text {
flex: 1;
text-align: center;
}
.hero-logo {
margin-bottom: 2rem;
margin-top: 0;
display: block;
margin-left: auto;
margin-right: auto;
width: 60px;
height: 60px;
transition: filter 0.2s;
}
@media (prefers-color-scheme: dark) {
.hero-logo {
filter: invert(1) brightness(2);
}
}
.hero-section h1 {
font-size: 2.5rem;
line-height: 1.2;
font-weight: 700;
margin-bottom: 1rem;
background: linear-gradient(90deg, var(--accent-color), #fb923c);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
}
.hero-section h1 {
font-size: 2.5rem;
line-height: 1.2;
font-weight: 700;
margin-bottom: 1rem;
background: linear-gradient(90deg, var(--accent-color), #fb923c);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
}
.hero-section .hero-description {
font-size: 1.25rem;
color: var(--text-muted);
margin-bottom: 1.5rem;
line-height: 1.6;
}
.hero-section .hero-description {
font-size: 1.25rem;
color: var(--text-muted);
margin-bottom: 1.5rem;
line-height: 1.6;
}
.hero-section .hero-image {
flex: 0 0 400px;
height: 400px;
border-radius: 1.5rem;
overflow: hidden;
box-shadow:
0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.hero-section .hero-image {
flex: 0 0 400px;
height: 400px;
border-radius: 1.5rem;
overflow: hidden;
box-shadow:
0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.hero-section .hero-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.hero-section .hero-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.hero-section .hero-image img:hover {
transform: scale(1.05);
}
@media (max-width: 1024px) {
.hero-section {
gap: 2rem;
}
.hero-section .hero-image img:hover {
transform: scale(1.05);
}
.hero-section .hero-image {
flex: 0 0 250px;
height: auto;
aspect-ratio: 1/1;
}
@media (max-width: 1024px) {
.hero-section {
gap: 2rem;
}
.hero-section h1 {
font-size: 2.25rem;
}
.hero-section .hero-image {
flex: 0 0 250px;
height: auto;
aspect-ratio: 1/1;
}
.hero-section .hero-description {
font-size: 1.15rem;
}
}
.hero-section h1 {
font-size: 2.25rem;
}
@media (max-width: 768px) {
.hero-section {
flex-direction: column-reverse;
margin: 100px 0 3.5rem;
gap: 2rem;
}
.hero-section .hero-description {
font-size: 1.15rem;
}
}
.hero-section .hero-text {
width: 100%;
}
@media (max-width: 768px) {
.hero-section {
flex-direction: column-reverse;
margin: 100px 0 3.5rem;
gap: 2rem;
}
.hero-section .hero-text h1 {
font-size: 2rem;
}
.hero-section .hero-text {
width: 100%;
}
.hero-section .hero-description {
font-size: 1.1rem;
}
.hero-section .hero-text h1 {
font-size: 2rem;
}
.hero-section .hero-image {
width: 100%;
flex: 0 0 auto;
aspect-ratio: 3/2;
height: auto;
}
}
</style>
.hero-section .hero-description {
font-size: 1.1rem;
}
.hero-section .hero-image {
width: 100%;
flex: 0 0 auto;
aspect-ratio: 3/2;
height: auto;
}
}
</style>

View file

@ -6,351 +6,379 @@ const lang = getLangFromUrl(Astro.url);
const currentPath = Astro.url.pathname;
const getPathInLang = (targetLang: string) => {
// Split URL path into segments
const segments = currentPath.split('/').filter(Boolean);
// First segment is always the language code
if (segments.length === 0) {
// Homepage case
return `/${targetLang}`;
} else if (Object.keys(languages).includes(segments[0])) {
// Normal case: first segment is a language code
segments[0] = targetLang;
} else {
// Fallback for any non-language URLs
segments.unshift(targetLang);
}
return `/${segments.join('/')}`;
// Split URL path into segments
const segments = currentPath.split('/').filter(Boolean);
// First segment is always the language code
if (segments.length === 0) {
// Homepage case
return `/${targetLang}`;
} else if (Object.keys(languages).includes(segments[0])) {
// Normal case: first segment is a language code
segments[0] = targetLang;
} else {
// Fallback for any non-language URLs
segments.unshift(targetLang);
}
return `/${segments.join('/')}`;
};
// Props for the component
interface Props {
dropdownStyle?: boolean;
dropdownStyle?: boolean;
}
const { dropdownStyle = false } = Astro.props;
---
{
dropdownStyle ? (
<div class="language-dropdown">
<button class="dropdown-button">
<span class="current-lang">{languages[lang]}</span>
<svg class="dropdown-arrow" xmlns="http://www.w3.org/2000/svg" width="12" height="6" viewBox="0 0 12 6" fill="none">
<path d="M1 1L6 5L11 1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
<div class="dropdown-menu">
{Object.entries(languages).map(([code, name]) => (
<a
href={getPathInLang(code)}
class={lang === code ? 'active' : ''}
hreflang={code}
lang={code}
>
{name}
</a>
))}
</div>
</div>
) : (
<div class="language-selector">
{Object.entries(languages).map(([code, name]) => (
<a
href={getPathInLang(code)}
class={lang === code ? 'active' : ''}
hreflang={code}
lang={code}
>
{name}
</a>
))}
</div>
)
dropdownStyle ? (
<div class="language-dropdown">
<button class="dropdown-button">
<span class="current-lang">{languages[lang]}</span>
<svg
class="dropdown-arrow"
xmlns="http://www.w3.org/2000/svg"
width="12"
height="6"
viewBox="0 0 12 6"
fill="none"
>
<path
d="M1 1L6 5L11 1"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
<div class="dropdown-menu">
{Object.entries(languages).map(([code, name]) => (
<a
href={getPathInLang(code)}
class={lang === code ? 'active' : ''}
hreflang={code}
lang={code}
>
{name}
</a>
))}
</div>
</div>
) : (
<div class="language-selector">
{Object.entries(languages).map(([code, name]) => (
<a
href={getPathInLang(code)}
class={lang === code ? 'active' : ''}
hreflang={code}
lang={code}
>
{name}
</a>
))}
</div>
)
}
<style>
/* Shared styles */
.language-selector,
.language-dropdown {
display: flex;
align-items: center;
}
.language-selector a,
.dropdown-menu a {
padding: 0.35rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.875rem;
color: var(--text-color);
text-decoration: none;
transition: background-color 0.2s, color 0.2s;
}
.language-selector a:hover,
.dropdown-menu a:hover {
background-color: var(--hover-bg);
}
.language-selector a.active,
.dropdown-menu a.active {
font-weight: bold;
background-color: var(--hover-bg);
}
/* Horizontal selector styles */
.language-selector {
gap: 0.25rem;
}
/* Dropdown styles */
.language-dropdown {
position: relative;
z-index: 150;
}
.dropdown-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: none;
border: 1px solid var(--border-color);
border-radius: 0.375rem;
font-size: 0.875rem;
color: var(--text-color);
cursor: pointer;
transition: background-color 0.2s;
}
/* Fix text color in dark mode */
:global(:root.dark) .dropdown-button {
color: #f9fafb;
}
.dropdown-button:hover {
background-color: var(--hover-bg);
}
.dropdown-arrow {
transition: transform 0.2s;
}
.dropdown-menu {
position: absolute;
top: calc(100% + 0.25rem);
right: 0;
display: flex;
flex-direction: column;
min-width: 120px;
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 0.375rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
opacity: 0;
transform: translateY(-10px);
visibility: hidden;
transition: opacity 0.2s, transform 0.2s, visibility 0s 0.2s;
z-index: 200; /* Higher z-index to ensure it appears above other elements */
overflow: hidden;
}
/* Initial state for dropdowns */
.dropdown-menu {
opacity: 0;
visibility: hidden;
}
.dropdown-menu.show {
opacity: 1;
transform: translateY(0);
visibility: visible;
transition: opacity 0.2s, transform 0.2s;
}
.dropdown-menu a {
padding: 0.75rem 1rem;
border-radius: 0;
width: 100%;
text-align: left;
}
/* Fix link color in dark mode */
:global(:root.dark) .dropdown-menu a {
color: #f9fafb;
}
/* Only show dropdown on desktop hover, not mobile */
@media (min-width: 769px) {
.language-dropdown:hover .dropdown-menu,
.dropdown-button:focus-within .dropdown-menu {
opacity: 1;
transform: translateY(0);
visibility: visible;
transition: opacity 0.2s, transform 0.2s;
}
.language-dropdown:hover .dropdown-arrow,
.dropdown-button:focus-within .dropdown-arrow,
.dropdown-button[aria-expanded="true"] .dropdown-arrow {
transform: rotate(180deg);
}
}
/* Show dropdown in mobile when active */
.dropdown-menu.show {
opacity: 1;
transform: translateY(0);
visibility: visible;
transition: opacity 0.2s, transform 0.2s;
}
/* Standard animation for all dropdowns */
.mobile-menu-controls .dropdown-menu.show {
transform: none !important;
opacity: 1;
visibility: visible;
}
/* Mobile styles */
@media (max-width: 768px) {
.language-selector {
flex-direction: row;
width: 100%;
justify-content: flex-start;
gap: 1rem;
}
.language-selector a {
font-size: 1rem;
padding: 0.5rem 0.75rem;
}
.language-dropdown {
width: 100%;
position: relative; /* Keep relative positioning for proper dropdown placement */
}
.dropdown-button {
width: 100%;
justify-content: space-between;
padding: 1rem 1.25rem;
font-size: 1.1rem;
text-align: center;
}
/* Important: absolute positioning for mobile dropdown */
.dropdown-menu {
position: absolute;
width: 100%;
left: 0;
top: 100%; /* Show below the button */
z-index: 2000; /* Extra high z-index */
max-height: calc(100vh - 200px);
overflow-y: auto;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
/* Style for mobile menu dropdown - directly attached to button */
.mobile-menu-controls .dropdown-menu {
position: absolute;
width: 100%; /* Match button width */
left: 0;
top: 100%; /* Position below the button */
bottom: auto;
margin-top: 2px; /* Small gap */
max-height: 180px;
border-radius: 8px;
z-index: 2000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
background-color: var(--card-bg);
}
.dropdown-menu a {
padding: 1rem;
font-size: 1rem;
}
}
/* Shared styles */
.language-selector,
.language-dropdown {
display: flex;
align-items: center;
}
.language-selector a,
.dropdown-menu a {
padding: 0.35rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.875rem;
color: var(--text-color);
text-decoration: none;
transition:
background-color 0.2s,
color 0.2s;
}
.language-selector a:hover,
.dropdown-menu a:hover {
background-color: var(--hover-bg);
}
.language-selector a.active,
.dropdown-menu a.active {
font-weight: bold;
background-color: var(--hover-bg);
}
/* Horizontal selector styles */
.language-selector {
gap: 0.25rem;
}
/* Dropdown styles */
.language-dropdown {
position: relative;
z-index: 150;
}
.dropdown-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: none;
border: 1px solid var(--border-color);
border-radius: 0.375rem;
font-size: 0.875rem;
color: var(--text-color);
cursor: pointer;
transition: background-color 0.2s;
}
/* Fix text color in dark mode */
:global(:root.dark) .dropdown-button {
color: #f9fafb;
}
.dropdown-button:hover {
background-color: var(--hover-bg);
}
.dropdown-arrow {
transition: transform 0.2s;
}
.dropdown-menu {
position: absolute;
top: calc(100% + 0.25rem);
right: 0;
display: flex;
flex-direction: column;
min-width: 120px;
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 0.375rem;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
opacity: 0;
transform: translateY(-10px);
visibility: hidden;
transition:
opacity 0.2s,
transform 0.2s,
visibility 0s 0.2s;
z-index: 200; /* Higher z-index to ensure it appears above other elements */
overflow: hidden;
}
/* Initial state for dropdowns */
.dropdown-menu {
opacity: 0;
visibility: hidden;
}
.dropdown-menu.show {
opacity: 1;
transform: translateY(0);
visibility: visible;
transition:
opacity 0.2s,
transform 0.2s;
}
.dropdown-menu a {
padding: 0.75rem 1rem;
border-radius: 0;
width: 100%;
text-align: left;
}
/* Fix link color in dark mode */
:global(:root.dark) .dropdown-menu a {
color: #f9fafb;
}
/* Only show dropdown on desktop hover, not mobile */
@media (min-width: 769px) {
.language-dropdown:hover .dropdown-menu,
.dropdown-button:focus-within .dropdown-menu {
opacity: 1;
transform: translateY(0);
visibility: visible;
transition:
opacity 0.2s,
transform 0.2s;
}
.language-dropdown:hover .dropdown-arrow,
.dropdown-button:focus-within .dropdown-arrow,
.dropdown-button[aria-expanded='true'] .dropdown-arrow {
transform: rotate(180deg);
}
}
/* Show dropdown in mobile when active */
.dropdown-menu.show {
opacity: 1;
transform: translateY(0);
visibility: visible;
transition:
opacity 0.2s,
transform 0.2s;
}
/* Standard animation for all dropdowns */
.mobile-menu-controls .dropdown-menu.show {
transform: none !important;
opacity: 1;
visibility: visible;
}
/* Mobile styles */
@media (max-width: 768px) {
.language-selector {
flex-direction: row;
width: 100%;
justify-content: flex-start;
gap: 1rem;
}
.language-selector a {
font-size: 1rem;
padding: 0.5rem 0.75rem;
}
.language-dropdown {
width: 100%;
position: relative; /* Keep relative positioning for proper dropdown placement */
}
.dropdown-button {
width: 100%;
justify-content: space-between;
padding: 1rem 1.25rem;
font-size: 1.1rem;
text-align: center;
}
/* Important: absolute positioning for mobile dropdown */
.dropdown-menu {
position: absolute;
width: 100%;
left: 0;
top: 100%; /* Show below the button */
z-index: 2000; /* Extra high z-index */
max-height: calc(100vh - 200px);
overflow-y: auto;
box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
/* Style for mobile menu dropdown - directly attached to button */
.mobile-menu-controls .dropdown-menu {
position: absolute;
width: 100%; /* Match button width */
left: 0;
top: 100%; /* Position below the button */
bottom: auto;
margin-top: 2px; /* Small gap */
max-height: 180px;
border-radius: 8px;
z-index: 2000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
background-color: var(--card-bg);
}
.dropdown-menu a {
padding: 1rem;
font-size: 1rem;
}
}
</style>
<script>
// For accessibility - allow keyboard navigation
document.addEventListener('DOMContentLoaded', () => {
// Handle all dropdown buttons in the document, not just the first one
const dropdownButtons = document.querySelectorAll('.dropdown-button');
dropdownButtons.forEach(dropdownButton => {
if (!dropdownButton) return;
// Open/close dropdown on click
dropdownButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const isExpanded = dropdownButton.getAttribute('aria-expanded') === 'true';
dropdownButton.setAttribute('aria-expanded', isExpanded ? 'false' : 'true');
const menu = dropdownButton.nextElementSibling;
if (menu && menu.classList.contains('dropdown-menu')) {
menu.classList.toggle('show');
if (!isExpanded) {
// Focus the first menu item
const firstItem = menu.querySelector('a');
if (firstItem) firstItem.focus();
}
}
});
dropdownButton.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
dropdownButton.click();
}
// Close when escape is pressed
if (e.key === 'Escape') {
const menu = dropdownButton.nextElementSibling;
if (menu && menu.classList.contains('show')) {
menu.classList.remove('show');
dropdownButton.setAttribute('aria-expanded', 'false');
dropdownButton.focus();
}
}
});
// Initialize ARIA attributes
dropdownButton.setAttribute('aria-haspopup', 'true');
dropdownButton.setAttribute('aria-expanded', 'false');
});
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
dropdownButtons.forEach(dropdownButton => {
if (!dropdownButton.contains(e.target)) {
const menu = dropdownButton.nextElementSibling;
if (menu && menu.classList.contains('show')) {
menu.classList.remove('show');
dropdownButton.setAttribute('aria-expanded', 'false');
}
}
});
});
// Add click listener to all dropdown menu items
const dropdownMenuItems = document.querySelectorAll('.dropdown-menu a');
dropdownMenuItems.forEach(item => {
item.addEventListener('click', (e) => {
const menu = item.closest('.dropdown-menu');
if (menu) {
menu.classList.remove('show');
const button = menu.previousElementSibling;
if (button) {
button.setAttribute('aria-expanded', 'false');
}
}
});
});
});
</script>
// For accessibility - allow keyboard navigation
document.addEventListener('DOMContentLoaded', () => {
// Handle all dropdown buttons in the document, not just the first one
const dropdownButtons = document.querySelectorAll('.dropdown-button');
dropdownButtons.forEach((dropdownButton) => {
if (!dropdownButton) return;
// Open/close dropdown on click
dropdownButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const isExpanded = dropdownButton.getAttribute('aria-expanded') === 'true';
dropdownButton.setAttribute('aria-expanded', isExpanded ? 'false' : 'true');
const menu = dropdownButton.nextElementSibling;
if (menu && menu.classList.contains('dropdown-menu')) {
menu.classList.toggle('show');
if (!isExpanded) {
// Focus the first menu item
const firstItem = menu.querySelector('a');
if (firstItem) firstItem.focus();
}
}
});
dropdownButton.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
dropdownButton.click();
}
// Close when escape is pressed
if (e.key === 'Escape') {
const menu = dropdownButton.nextElementSibling;
if (menu && menu.classList.contains('show')) {
menu.classList.remove('show');
dropdownButton.setAttribute('aria-expanded', 'false');
dropdownButton.focus();
}
}
});
// Initialize ARIA attributes
dropdownButton.setAttribute('aria-haspopup', 'true');
dropdownButton.setAttribute('aria-expanded', 'false');
});
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
dropdownButtons.forEach((dropdownButton) => {
if (!dropdownButton.contains(e.target)) {
const menu = dropdownButton.nextElementSibling;
if (menu && menu.classList.contains('show')) {
menu.classList.remove('show');
dropdownButton.setAttribute('aria-expanded', 'false');
}
}
});
});
// Add click listener to all dropdown menu items
const dropdownMenuItems = document.querySelectorAll('.dropdown-menu a');
dropdownMenuItems.forEach((item) => {
item.addEventListener('click', (e) => {
const menu = item.closest('.dropdown-menu');
if (menu) {
menu.classList.remove('show');
const button = menu.previousElementSibling;
if (button) {
button.setAttribute('aria-expanded', 'false');
}
}
});
});
});
</script>

View file

@ -1,385 +1,381 @@
---
interface Props {
height?: string;
height?: string;
}
const { height = "85vh" } = Astro.props;
const { height = '85vh' } = Astro.props;
---
<div class="matrix-hero" style={`height: ${height};`}>
<div class="matrix-canvas-container">
<canvas id="matrixCanvas"></canvas>
<div class="fade-bottom"></div>
</div>
<div class="content">
<div class="title-container">
<p class="hero-subtitle">Custom build future</p>
<button id="scroll-down" class="scroll-down-btn" aria-label="Scroll down">
<svg
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="chevron-down"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
</div>
</div>
<div class="matrix-canvas-container">
<canvas id="matrixCanvas"></canvas>
<div class="fade-bottom"></div>
</div>
<div class="content">
<div class="title-container">
<p class="hero-subtitle">Custom build future</p>
<button id="scroll-down" class="scroll-down-btn" aria-label="Scroll down">
<svg
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="chevron-down"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
</div>
</div>
</div>
<style>
.matrix-hero {
position: relative;
width: 100vw;
margin-left: calc(-50vw + 50%);
margin-top: -69px; /* Match the navigation height to extend behind it */
padding-top: 69px; /* Add padding to ensure content starts below nav */
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
background-color: #000; /* Ensure black background */
}
.matrix-hero {
position: relative;
width: 100vw;
margin-left: calc(-50vw + 50%);
margin-top: -69px; /* Match the navigation height to extend behind it */
padding-top: 69px; /* Add padding to ensure content starts below nav */
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
background-color: #000; /* Ensure black background */
}
.matrix-canvas-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2; /* Higher z-index to place it above content */
pointer-events: auto; /* Allow interaction with matrix elements */
}
.matrix-canvas-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2; /* Higher z-index to place it above content */
pointer-events: auto; /* Allow interaction with matrix elements */
}
.fade-bottom {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 150px;
background: linear-gradient(
to top,
var(--background-color) 0%,
rgba(0, 0, 0, 0) 100%
);
z-index: 3; /* Above matrix canvas */
}
.fade-bottom {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 150px;
background: linear-gradient(to top, var(--background-color) 0%, rgba(0, 0, 0, 0) 100%);
z-index: 3; /* Above matrix canvas */
}
canvas {
width: 100%;
height: 100%;
display: block;
}
canvas {
width: 100%;
height: 100%;
display: block;
}
.content {
position: relative;
z-index: 2; /* Same z-index as the matrix canvas to interleave */
text-align: center;
color: white;
padding: 0 1rem;
transform: translateY(-50px); /* Move content up */
}
.content {
position: relative;
z-index: 2; /* Same z-index as the matrix canvas to interleave */
text-align: center;
color: white;
padding: 0 1rem;
transform: translateY(-50px); /* Move content up */
}
.title-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.title-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.hero-subtitle {
font-size: 5rem;
font-weight: 600;
letter-spacing: 0.05em;
color: rgba(255, 255, 255, 0.95);
text-align: center;
text-transform: uppercase;
line-height: 1.2;
max-width: 80%;
text-shadow:
0 0 20px rgba(255, 255, 255, 0.25),
0 0 35px rgba(255, 165, 0, 0.18),
0 0 60px rgba(255, 255, 255, 0.12),
0 0 100px rgba(255, 255, 255, 0.08);
margin-bottom: 0.5rem;
}
.hero-subtitle {
font-size: 5rem;
font-weight: 600;
letter-spacing: 0.05em;
color: rgba(255, 255, 255, 0.95);
text-align: center;
text-transform: uppercase;
line-height: 1.2;
max-width: 80%;
text-shadow:
0 0 20px rgba(255, 255, 255, 0.25),
0 0 35px rgba(255, 165, 0, 0.18),
0 0 60px rgba(255, 255, 255, 0.12),
0 0 100px rgba(255, 255, 255, 0.08);
margin-bottom: 0.5rem;
}
.scroll-down-btn {
background: none;
border: none;
cursor: pointer;
padding: 0;
margin-top: 0.5rem;
color: rgba(255, 255, 255, 1);
transition: transform 0.3s ease;
}
.scroll-down-btn {
background: none;
border: none;
cursor: pointer;
padding: 0;
margin-top: 0.5rem;
color: rgba(255, 255, 255, 1);
transition: transform 0.3s ease;
}
.scroll-down-btn:hover {
transform: scale(1.1);
}
.scroll-down-btn:hover {
transform: scale(1.1);
}
.chevron-down {
filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.3));
}
.chevron-down {
filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.3));
}
.matrix-ball {
display: none; /* Hide the ball element instead of removing it completely */
}
.matrix-ball {
display: none; /* Hide the ball element instead of removing it completely */
}
@media (max-width: 768px) {
.matrix-hero {
margin-top: -57px; /* Adjust for smaller mobile nav height */
padding-top: 57px;
}
@media (max-width: 768px) {
.matrix-hero {
margin-top: -57px; /* Adjust for smaller mobile nav height */
padding-top: 57px;
}
.content {
transform: translateY(-30px); /* Less shift on mobile */
}
.content {
transform: translateY(-30px); /* Less shift on mobile */
}
.hero-subtitle {
font-size: 2.8rem;
max-width: 90%;
}
.hero-subtitle {
font-size: 2.8rem;
max-width: 90%;
}
.scroll-down-btn svg {
width: 48px;
height: 48px;
}
.scroll-down-btn svg {
width: 48px;
height: 48px;
}
.fade-bottom {
height: 100px;
}
}
.fade-bottom {
height: 100px;
}
}
</style>
<script defer>
// Defer the matrix animation initialization
function initMatrixAnimation() {
// Scroll to first card when chevron is clicked
const scrollBtn = document.getElementById("scroll-down");
if (scrollBtn) {
scrollBtn.addEventListener("click", () => {
// Find the first card section after the hero
const featureSection = document.querySelector(".feature-section");
if (featureSection && window.scrollToElement) {
// Use the global scroll helper
window.scrollToElement(featureSection, 20); // 20px additional offset
}
});
}
// Defer the matrix animation initialization
function initMatrixAnimation() {
// Scroll to first card when chevron is clicked
const scrollBtn = document.getElementById('scroll-down');
if (scrollBtn) {
scrollBtn.addEventListener('click', () => {
// Find the first card section after the hero
const featureSection = document.querySelector('.feature-section');
if (featureSection && window.scrollToElement) {
// Use the global scroll helper
window.scrollToElement(featureSection, 20); // 20px additional offset
}
});
}
// Matrix animation (optimized)
const canvas = document.getElementById("matrixCanvas");
if (!canvas) return;
// Matrix animation (optimized)
const canvas = document.getElementById('matrixCanvas');
if (!canvas) return;
const ctx = canvas.getContext("2d", { alpha: false }); // Optimize context by disabling alpha
if (!ctx) return;
const ctx = canvas.getContext('2d', { alpha: false }); // Optimize context by disabling alpha
if (!ctx) return;
// Make canvas full size of container but at lower resolution for performance
function resizeCanvas() {
const container = canvas.parentElement;
if (!container) return;
// Use a smaller canvas resolution for better performance
const scale = window.devicePixelRatio * 0.5; // Half the device pixel ratio
canvas.width = container.clientWidth * scale;
canvas.height = container.clientHeight * scale;
// Scale the context to make it look right
ctx.scale(scale, scale);
// Reset CSS dimensions
canvas.style.width = container.clientWidth + 'px';
canvas.style.height = container.clientHeight + 'px';
}
// Make canvas full size of container but at lower resolution for performance
function resizeCanvas() {
const container = canvas.parentElement;
if (!container) return;
// Initial setup
resizeCanvas();
const debouncedResize = debounce(resizeCanvas, 250);
window.addEventListener("resize", debouncedResize);
// Use a smaller canvas resolution for better performance
const scale = window.devicePixelRatio * 0.5; // Half the device pixel ratio
canvas.width = container.clientWidth * scale;
canvas.height = container.clientHeight * scale;
// Clear with solid black to start
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Scale the context to make it look right
ctx.scale(scale, scale);
// Pre-calculate some values for optimization
const codeSnippets = [
"const x = 0;",
"let y = true;",
"if (x > 0) {",
"function() {",
"export const",
"import { }",
];
// Pre-calculate colors for better performance
const colorPalette = [];
for (let i = 0; i < 20; i++) {
const hue = 20 + Math.floor(Math.random() * 20);
const saturation = 80 + Math.floor(Math.random() * 20);
const lightness = 50 + Math.floor(Math.random() * 20);
colorPalette.push(`hsl(${hue}, ${saturation}%, ${lightness}%)`);
}
// Reset CSS dimensions
canvas.style.width = container.clientWidth + 'px';
canvas.style.height = container.clientHeight + 'px';
}
// Matrix code effect - simplified for better performance
class Column {
constructor(x, fontSize, canvasHeight) {
this.x = x;
this.fontSize = fontSize;
this.canvasHeight = canvasHeight;
this.rises = [];
this.chars = [];
this.colorIndexes = [];
// Even slower speed for better performance
this.speed = 0.02 + Math.random() * 0.01;
// Initial setup
resizeCanvas();
const debouncedResize = debounce(resizeCanvas, 250);
window.addEventListener('resize', debouncedResize);
// Initialize with positions from bottom - use less characters
const snippet = codeSnippets[Math.floor(Math.random() * codeSnippets.length)];
for (let i = 0; i < snippet.length; i++) {
// Position strings at different positions below the canvas
const y = canvasHeight + Math.random() * 200;
this.rises.push(y);
this.chars.push(snippet[i]);
this.colorIndexes.push(Math.floor(Math.random() * colorPalette.length));
}
}
// Clear with solid black to start
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
draw(ctx) {
for (let i = 0; i < this.rises.length; i++) {
// Calculate positions for current character
const x = this.x + i * this.fontSize * 0.6;
const y = this.rises[i];
// Pre-calculate some values for optimization
const codeSnippets = [
'const x = 0;',
'let y = true;',
'if (x > 0) {',
'function() {',
'export const',
'import { }',
];
// Draw only if in visible area (performance optimization)
if (y > 0 && y < this.canvasHeight + 50) {
// Draw the current character
ctx.fillStyle = colorPalette[this.colorIndexes[i]];
ctx.font = `${this.fontSize}px 'Courier New', monospace`;
ctx.fillText(this.chars[i], x, y);
}
// Pre-calculate colors for better performance
const colorPalette = [];
for (let i = 0; i < 20; i++) {
const hue = 20 + Math.floor(Math.random() * 20);
const saturation = 80 + Math.floor(Math.random() * 20);
const lightness = 50 + Math.floor(Math.random() * 20);
colorPalette.push(`hsl(${hue}, ${saturation}%, ${lightness}%)`);
}
// Move the rise up slower
this.rises[i] -= this.fontSize * this.speed;
// Matrix code effect - simplified for better performance
class Column {
constructor(x, fontSize, canvasHeight) {
this.x = x;
this.fontSize = fontSize;
this.canvasHeight = canvasHeight;
this.rises = [];
this.chars = [];
this.colorIndexes = [];
// Even slower speed for better performance
this.speed = 0.02 + Math.random() * 0.01;
// Reset when off the top of the screen with random delay
if (this.rises[i] < -50 && Math.random() > 0.99) {
this.rises[i] = this.canvasHeight + this.fontSize * (10 + Math.random() * 20);
}
}
}
}
// Initialize with positions from bottom - use less characters
const snippet = codeSnippets[Math.floor(Math.random() * codeSnippets.length)];
for (let i = 0; i < snippet.length; i++) {
// Position strings at different positions below the canvas
const y = canvasHeight + Math.random() * 200;
this.rises.push(y);
this.chars.push(snippet[i]);
this.colorIndexes.push(Math.floor(Math.random() * colorPalette.length));
}
}
// Create columns of code - reduced number for better performance
let columns = [];
let animationFrameId;
let lastFrameTime = 0;
const targetFPS = 30; // Lower target FPS for better performance
const frameInterval = 1000 / targetFPS;
draw(ctx) {
for (let i = 0; i < this.rises.length; i++) {
// Calculate positions for current character
const x = this.x + i * this.fontSize * 0.6;
const y = this.rises[i];
function initColumns() {
columns = [];
const fontSize = canvas.width > 768 ? 16 : 12;
// Draw only if in visible area (performance optimization)
if (y > 0 && y < this.canvasHeight + 50) {
// Draw the current character
ctx.fillStyle = colorPalette[this.colorIndexes[i]];
ctx.font = `${this.fontSize}px 'Courier New', monospace`;
ctx.fillText(this.chars[i], x, y);
}
// Use much fewer columns for better performance
const maxColumns = canvas.width > 768 ? 15 : 8;
// Move the rise up slower
this.rises[i] -= this.fontSize * this.speed;
// Create columns at optimal positions
for (let i = 0; i < maxColumns; i++) {
const x = (i / maxColumns) * canvas.width;
columns.push(new Column(x, fontSize, canvas.height));
}
}
// Reset when off the top of the screen with random delay
if (this.rises[i] < -50 && Math.random() > 0.99) {
this.rises[i] = this.canvasHeight + this.fontSize * (10 + Math.random() * 20);
}
}
}
}
// Animation loop - optimized with frame limiting
function animate(currentTime) {
// Frame limiting for better performance
const elapsed = currentTime - lastFrameTime;
if (elapsed < frameInterval) {
animationFrameId = requestAnimationFrame(animate);
return;
}
lastFrameTime = currentTime - (elapsed % frameInterval);
// Create columns of code - reduced number for better performance
let columns = [];
let animationFrameId;
let lastFrameTime = 0;
const targetFPS = 30; // Lower target FPS for better performance
const frameInterval = 1000 / targetFPS;
// Use alpha for trail effect
ctx.fillStyle = "rgba(0, 0, 0, 0.08)"; // Slower fade for better effect
ctx.fillRect(0, 0, canvas.width, canvas.height);
function initColumns() {
columns = [];
const fontSize = canvas.width > 768 ? 16 : 12;
// Draw all columns
columns.forEach((column) => column.draw(ctx));
// Use much fewer columns for better performance
const maxColumns = canvas.width > 768 ? 15 : 8;
// Continue animation
animationFrameId = requestAnimationFrame(animate);
}
// Create columns at optimal positions
for (let i = 0; i < maxColumns; i++) {
const x = (i / maxColumns) * canvas.width;
columns.push(new Column(x, fontSize, canvas.height));
}
}
// Initialize columns after a short delay
setTimeout(() => {
initColumns();
lastFrameTime = performance.now();
animate(lastFrameTime);
}, 500); // 500ms delay to ensure other critical resources load first
// Animation loop - optimized with frame limiting
function animate(currentTime) {
// Frame limiting for better performance
const elapsed = currentTime - lastFrameTime;
if (elapsed < frameInterval) {
animationFrameId = requestAnimationFrame(animate);
return;
}
lastFrameTime = currentTime - (elapsed % frameInterval);
// Debounce function for resize event
function debounce(func, wait) {
let timeout;
return function() {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
};
}
// Use alpha for trail effect
ctx.fillStyle = 'rgba(0, 0, 0, 0.08)'; // Slower fade for better effect
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Handle visibility change to pause animation when tab is not visible
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// Pause animation to save resources
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
} else if (!animationFrameId) {
// Resume animation
lastFrameTime = performance.now();
animationFrameId = requestAnimationFrame(animate);
}
});
// Draw all columns
columns.forEach((column) => column.draw(ctx));
// Reinitialize on resize, but debounced
window.addEventListener("resize", () => {
// Cancel existing animation to prevent memory leaks
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
// Continue animation
animationFrameId = requestAnimationFrame(animate);
}
// We already have a debounced resize for canvas dimensions
// Clear canvas
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Initialize columns after a short delay
setTimeout(() => {
initColumns();
lastFrameTime = performance.now();
animate(lastFrameTime);
}, 500); // 500ms delay to ensure other critical resources load first
// Reinitialize columns
initColumns();
lastFrameTime = performance.now();
animate(lastFrameTime);
});
}
// Debounce function for resize event
function debounce(func, wait) {
let timeout;
return function () {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
};
}
// Use requestIdleCallback or setTimeout to defer initialization
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
initMatrixAnimation();
});
} else {
// Fallback for browsers without requestIdleCallback
setTimeout(initMatrixAnimation, 1000); // Wait for 1 second after page load
}
// Handle visibility change to pause animation when tab is not visible
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// Pause animation to save resources
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
} else if (!animationFrameId) {
// Resume animation
lastFrameTime = performance.now();
animationFrameId = requestAnimationFrame(animate);
}
});
// Reinitialize on resize, but debounced
window.addEventListener('resize', () => {
// Cancel existing animation to prevent memory leaks
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
// We already have a debounced resize for canvas dimensions
// Clear canvas
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Reinitialize columns
initColumns();
lastFrameTime = performance.now();
animate(lastFrameTime);
});
}
// Use requestIdleCallback or setTimeout to defer initialization
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
initMatrixAnimation();
});
} else {
// Fallback for browsers without requestIdleCallback
setTimeout(initMatrixAnimation, 1000); // Wait for 1 second after page load
}
</script>

View file

@ -3,7 +3,7 @@ import { getLangFromUrl, useTranslations } from '../utils/i18n';
import type { CollectionEntry } from 'astro:content';
interface Props {
mission: CollectionEntry<'missions'>;
mission: CollectionEntry<'missions'>;
}
const { mission } = Astro.props;
@ -14,9 +14,9 @@ const t = useTranslations(lang);
// Format date based on language
const formattedDate = new Intl.DateTimeFormat(lang === 'de' ? 'de-DE' : 'en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(data.pubDate);
// Generate mission URL - always include language segment
@ -31,253 +31,262 @@ const statusKey = `missions.status.${data.status}` as const;
---
<a href={missionUrl} class="mission-card-link">
<article class:list={["mission-card", { "featured": data.featured }]}>
{data.image && (
<div class="mission-image-container">
<img src={data.image} alt={data.title} class="mission-image" />
<div class="status-badge" data-status={data.status}>
{t(statusKey)}
</div>
</div>
)}
<div class="content">
<div class="meta">
<span class="difficulty" data-difficulty={data.difficulty}>{t(difficultyKey)}</span>
<span class="date">{formattedDate}</span>
</div>
<h3>{data.title}</h3>
<p>{data.description}</p>
<div class="details">
<div class="detail-item">
<span class="detail-label">{t('missions.duration')}</span>
<span class="detail-value">{data.duration}</span>
</div>
<div class="detail-item">
<span class="detail-label">{t('missions.skills')}</span>
<div class="skills-list">
{data.skills.map(skill => (
<span class="skill-tag">{skill}</span>
))}
</div>
</div>
</div>
<div class="footer">
{data.participants && data.participants.length > 0 && (
<div class="participants">
<span class="participants-count">{data.participants.length}</span>
</div>
)}
<span class="read-more">{t('missions.readMore')} <span class="arrow">→</span></span>
</div>
</div>
</article>
<article class:list={['mission-card', { featured: data.featured }]}>
{
data.image && (
<div class="mission-image-container">
<img src={data.image} alt={data.title} class="mission-image" />
<div class="status-badge" data-status={data.status}>
{t(statusKey)}
</div>
</div>
)
}
<div class="content">
<div class="meta">
<span class="difficulty" data-difficulty={data.difficulty}>{t(difficultyKey)}</span>
<span class="date">{formattedDate}</span>
</div>
<h3>{data.title}</h3>
<p>{data.description}</p>
<div class="details">
<div class="detail-item">
<span class="detail-label">{t('missions.duration')}</span>
<span class="detail-value">{data.duration}</span>
</div>
<div class="detail-item">
<span class="detail-label">{t('missions.skills')}</span>
<div class="skills-list">
{data.skills.map((skill) => <span class="skill-tag">{skill}</span>)}
</div>
</div>
</div>
<div class="footer">
{
data.participants && data.participants.length > 0 && (
<div class="participants">
<span class="participants-count">{data.participants.length}</span>
</div>
)
}
<span class="read-more">{t('missions.readMore')} <span class="arrow">→</span></span>
</div>
</div>
</article>
</a>
<style>
.mission-card-link {
display: block;
text-decoration: none;
color: inherit;
height: 100%;
}
.mission-card {
display: flex;
flex-direction: column;
border-radius: 0.75rem;
overflow: hidden;
background-color: var(--card-bg);
border: 2px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.06), 0 1px 2px -1px rgba(0, 0, 0, 0.03);
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
height: 100%;
}
.mission-card-link:hover .mission-card {
transform: translateY(-2px);
border-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.5);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 2px 4px -2px rgba(0, 0, 0, 0.04);
}
.mission-card.featured {
border: 2px solid rgba(var(--accent-color-rgb, 249, 115, 22), 0.3);
}
.mission-image-container {
width: 100%;
padding-top: 66.67%; /* Aspect ratio 3:2 */
position: relative;
overflow: hidden;
}
.mission-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
.status-badge {
position: absolute;
top: 1rem;
right: 1rem;
padding: 0.35rem 0.75rem;
border-radius: 2rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
z-index: 2;
color: white;
}
.status-badge[data-status="active"] {
background-color: #10b981; /* Green */
}
.status-badge[data-status="completed"] {
background-color: #6366f1; /* Purple */
}
.status-badge[data-status="upcoming"] {
background-color: #f59e0b; /* Amber */
}
.content {
display: flex;
flex-direction: column;
padding: 1.5rem;
flex-grow: 1;
}
.meta {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
margin-bottom: 0.75rem;
}
.difficulty {
font-weight: 600;
padding: 0.2rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
}
.difficulty[data-difficulty="beginner"] {
background-color: #dcfce7; /* Light green */
color: #16a34a;
}
.difficulty[data-difficulty="intermediate"] {
background-color: #e0f2fe; /* Light blue */
color: #0284c7;
}
.difficulty[data-difficulty="advanced"] {
background-color: #fee2e2; /* Light red */
color: #dc2626;
}
.date {
color: var(--text-muted);
}
h3 {
margin: 0 0 0.75rem 0;
font-size: 1.25rem;
line-height: 1.4;
}
p {
margin: 0;
color: var(--text-muted);
line-height: 1.6;
margin-bottom: 1.5rem;
}
.details {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.detail-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.detail-label {
font-size: 0.75rem;
color: var(--text-muted);
font-weight: 500;
}
.detail-value {
font-size: 0.875rem;
}
.skills-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.skill-tag {
font-size: 0.75rem;
background-color: rgba(var(--border-color-rgb), 0.3);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.participants {
display: flex;
align-items: center;
}
.participants-count {
font-size: 0.875rem;
color: var(--text-muted);
display: flex;
align-items: center;
}
.participants-count::before {
content: '👥 ';
margin-right: 0.25rem;
}
.read-more {
display: inline-flex;
align-items: center;
color: var(--accent-color);
font-weight: 600;
font-size: 0.875rem;
}
.arrow {
display: inline-block;
margin-left: 4px;
transition: transform 0.2s ease;
}
.mission-card-link:hover .arrow {
transform: translateX(4px);
}
</style>
.mission-card-link {
display: block;
text-decoration: none;
color: inherit;
height: 100%;
}
.mission-card {
display: flex;
flex-direction: column;
border-radius: 0.75rem;
overflow: hidden;
background-color: var(--card-bg);
border: 2px solid rgba(255, 255, 255, 0.3);
box-shadow:
0 2px 4px -1px rgba(0, 0, 0, 0.06),
0 1px 2px -1px rgba(0, 0, 0, 0.03);
transition:
transform 0.2s ease,
border-color 0.2s ease,
box-shadow 0.2s ease;
height: 100%;
}
.mission-card-link:hover .mission-card {
transform: translateY(-2px);
border-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.5);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.08),
0 2px 4px -2px rgba(0, 0, 0, 0.04);
}
.mission-card.featured {
border: 2px solid rgba(var(--accent-color-rgb, 249, 115, 22), 0.3);
}
.mission-image-container {
width: 100%;
padding-top: 66.67%; /* Aspect ratio 3:2 */
position: relative;
overflow: hidden;
}
.mission-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
.status-badge {
position: absolute;
top: 1rem;
right: 1rem;
padding: 0.35rem 0.75rem;
border-radius: 2rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
z-index: 2;
color: white;
}
.status-badge[data-status='active'] {
background-color: #10b981; /* Green */
}
.status-badge[data-status='completed'] {
background-color: #6366f1; /* Purple */
}
.status-badge[data-status='upcoming'] {
background-color: #f59e0b; /* Amber */
}
.content {
display: flex;
flex-direction: column;
padding: 1.5rem;
flex-grow: 1;
}
.meta {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
margin-bottom: 0.75rem;
}
.difficulty {
font-weight: 600;
padding: 0.2rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
}
.difficulty[data-difficulty='beginner'] {
background-color: #dcfce7; /* Light green */
color: #16a34a;
}
.difficulty[data-difficulty='intermediate'] {
background-color: #e0f2fe; /* Light blue */
color: #0284c7;
}
.difficulty[data-difficulty='advanced'] {
background-color: #fee2e2; /* Light red */
color: #dc2626;
}
.date {
color: var(--text-muted);
}
h3 {
margin: 0 0 0.75rem 0;
font-size: 1.25rem;
line-height: 1.4;
}
p {
margin: 0;
color: var(--text-muted);
line-height: 1.6;
margin-bottom: 1.5rem;
}
.details {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.detail-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.detail-label {
font-size: 0.75rem;
color: var(--text-muted);
font-weight: 500;
}
.detail-value {
font-size: 0.875rem;
}
.skills-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.skill-tag {
font-size: 0.75rem;
background-color: rgba(var(--border-color-rgb), 0.3);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.participants {
display: flex;
align-items: center;
}
.participants-count {
font-size: 0.875rem;
color: var(--text-muted);
display: flex;
align-items: center;
}
.participants-count::before {
content: '👥 ';
margin-right: 0.25rem;
}
.read-more {
display: inline-flex;
align-items: center;
color: var(--accent-color);
font-weight: 600;
font-size: 0.875rem;
}
.arrow {
display: inline-block;
margin-left: 4px;
transition: transform 0.2s ease;
}
.mission-card-link:hover .arrow {
transform: translateX(4px);
}
</style>

View file

@ -2,150 +2,318 @@
import TableImproved from './TableImproved.astro';
const models = [
{ name: "o1", provider: "OpenAI", inputCost: 15, cachedInputCost: 7.5, outputCost: 60, inputLimit: 200000, outputLimit: 100000, batchAPI: "50% for 24 hours", hoster: "OpenAI, Azure", trainingCutoff: "October 2023", totalCost: 45 },
{ name: "gpt 4.1", provider: "OpenAI", inputCost: 2, cachedInputCost: 0.5, outputCost: 8, inputLimit: 1000000, outputLimit: 32768, batchAPI: "50% for 24 hours", hoster: "OpenAI, Azure", trainingCutoff: "June 2024", totalCost: 6 },
{ name: "GPT-4o", provider: "OpenAI", inputCost: 2.5, cachedInputCost: 1.25, outputCost: 10, inputLimit: 128000, outputLimit: 16384, batchAPI: "50% for 24 hours", hoster: "OpenAI, Azure", trainingCutoff: "October 2023", totalCost: 7.5 },
{ name: "gpt 4.1-mini", provider: "OpenAI", inputCost: 0.4, cachedInputCost: 0.1, outputCost: 1.6, inputLimit: 1000000, outputLimit: 32768, batchAPI: "50% for 24 hours", hoster: "OpenAI, Azure", trainingCutoff: "June 2024", totalCost: 1.2 },
{ name: "gpt 4.1-nano", provider: "OpenAI", inputCost: 0.1, cachedInputCost: 0.025, outputCost: 0.4, inputLimit: 1000000, outputLimit: 32768, batchAPI: "50% for 24 hours", hoster: "OpenAI, Azure", trainingCutoff: "June 2024", totalCost: 0.3 },
{ name: "o3-mini", provider: "OpenAI", inputCost: 1.1, cachedInputCost: 0.55, outputCost: 4.4, inputLimit: 200000, outputLimit: 100000, batchAPI: "50% for 24 hours", hoster: "OpenAI, Azure", trainingCutoff: "October 2023", totalCost: 3.3 },
{ name: "Claude Sonnet 3.7", provider: "Antrophic", inputCost: 3, cachedInputCost: "Up to 90%", outputCost: 15, inputLimit: 200000, outputLimit: 8192, batchAPI: "50% for 24 hours", hoster: "Google, Amazon, Antrophic", trainingCutoff: "April 2024", totalCost: 10.5 },
{ name: "Claude Haiku 3.5", provider: "Antrophic", inputCost: 0.8, cachedInputCost: "Up to 90%", outputCost: 4, inputLimit: 200000, outputLimit: 8192, batchAPI: "50% for 24 hours", hoster: "Google, Amazon, Antrophic", trainingCutoff: "July 2024", totalCost: 2.8 },
{ name: "Deepseek R1", provider: "Deepseek", inputCost: 0.55, cachedInputCost: 0.14, outputCost: 2.19, inputLimit: 64000, outputLimit: 8192, batchAPI: "", hoster: "Deepseek", trainingCutoff: "July 2024", totalCost: 1.645 },
{ name: "Deepseek V3", provider: "Deepseek", inputCost: 0.27, cachedInputCost: 0.07, outputCost: 1.1, inputLimit: 64000, outputLimit: 8192, batchAPI: "", hoster: "Deepseek", trainingCutoff: "December 2024", totalCost: 0.82 },
{ name: "GPT-4o mini", provider: "OpenAI", inputCost: 0.15, cachedInputCost: 0.075, outputCost: 0.6, inputLimit: 128000, outputLimit: 16384, batchAPI: "50% for 24 hours", hoster: "OpenAI, Azure", trainingCutoff: "October 2023", totalCost: 0.45 },
{ name: "Gemini 2.5 Pro", provider: "", inputCost: 1.25, cachedInputCost: "", outputCost: 10, inputLimit: 1048576, outputLimit: 65536, batchAPI: "", hoster: "Google", trainingCutoff: "January 2025", totalCost: 6.25 },
{ name: "Gemini 2.0 Flash", provider: "Google", inputCost: 0.1, cachedInputCost: 0.025, outputCost: 0.4, inputLimit: 1048576, outputLimit: 8192, batchAPI: "", hoster: "Google", trainingCutoff: "June 2024", totalCost: 0.3 },
{ name: "Gemini 2.0 Flash-Lite", provider: "Google", inputCost: 0.075, cachedInputCost: 0.01875, outputCost: 0.3, inputLimit: 1048576, outputLimit: 8192, batchAPI: "", hoster: "Google", trainingCutoff: "June 2024", totalCost: 0.225 },
{
name: 'o1',
provider: 'OpenAI',
inputCost: 15,
cachedInputCost: 7.5,
outputCost: 60,
inputLimit: 200000,
outputLimit: 100000,
batchAPI: '50% for 24 hours',
hoster: 'OpenAI, Azure',
trainingCutoff: 'October 2023',
totalCost: 45,
},
{
name: 'gpt 4.1',
provider: 'OpenAI',
inputCost: 2,
cachedInputCost: 0.5,
outputCost: 8,
inputLimit: 1000000,
outputLimit: 32768,
batchAPI: '50% for 24 hours',
hoster: 'OpenAI, Azure',
trainingCutoff: 'June 2024',
totalCost: 6,
},
{
name: 'GPT-4o',
provider: 'OpenAI',
inputCost: 2.5,
cachedInputCost: 1.25,
outputCost: 10,
inputLimit: 128000,
outputLimit: 16384,
batchAPI: '50% for 24 hours',
hoster: 'OpenAI, Azure',
trainingCutoff: 'October 2023',
totalCost: 7.5,
},
{
name: 'gpt 4.1-mini',
provider: 'OpenAI',
inputCost: 0.4,
cachedInputCost: 0.1,
outputCost: 1.6,
inputLimit: 1000000,
outputLimit: 32768,
batchAPI: '50% for 24 hours',
hoster: 'OpenAI, Azure',
trainingCutoff: 'June 2024',
totalCost: 1.2,
},
{
name: 'gpt 4.1-nano',
provider: 'OpenAI',
inputCost: 0.1,
cachedInputCost: 0.025,
outputCost: 0.4,
inputLimit: 1000000,
outputLimit: 32768,
batchAPI: '50% for 24 hours',
hoster: 'OpenAI, Azure',
trainingCutoff: 'June 2024',
totalCost: 0.3,
},
{
name: 'o3-mini',
provider: 'OpenAI',
inputCost: 1.1,
cachedInputCost: 0.55,
outputCost: 4.4,
inputLimit: 200000,
outputLimit: 100000,
batchAPI: '50% for 24 hours',
hoster: 'OpenAI, Azure',
trainingCutoff: 'October 2023',
totalCost: 3.3,
},
{
name: 'Claude Sonnet 3.7',
provider: 'Antrophic',
inputCost: 3,
cachedInputCost: 'Up to 90%',
outputCost: 15,
inputLimit: 200000,
outputLimit: 8192,
batchAPI: '50% for 24 hours',
hoster: 'Google, Amazon, Antrophic',
trainingCutoff: 'April 2024',
totalCost: 10.5,
},
{
name: 'Claude Haiku 3.5',
provider: 'Antrophic',
inputCost: 0.8,
cachedInputCost: 'Up to 90%',
outputCost: 4,
inputLimit: 200000,
outputLimit: 8192,
batchAPI: '50% for 24 hours',
hoster: 'Google, Amazon, Antrophic',
trainingCutoff: 'July 2024',
totalCost: 2.8,
},
{
name: 'Deepseek R1',
provider: 'Deepseek',
inputCost: 0.55,
cachedInputCost: 0.14,
outputCost: 2.19,
inputLimit: 64000,
outputLimit: 8192,
batchAPI: '',
hoster: 'Deepseek',
trainingCutoff: 'July 2024',
totalCost: 1.645,
},
{
name: 'Deepseek V3',
provider: 'Deepseek',
inputCost: 0.27,
cachedInputCost: 0.07,
outputCost: 1.1,
inputLimit: 64000,
outputLimit: 8192,
batchAPI: '',
hoster: 'Deepseek',
trainingCutoff: 'December 2024',
totalCost: 0.82,
},
{
name: 'GPT-4o mini',
provider: 'OpenAI',
inputCost: 0.15,
cachedInputCost: 0.075,
outputCost: 0.6,
inputLimit: 128000,
outputLimit: 16384,
batchAPI: '50% for 24 hours',
hoster: 'OpenAI, Azure',
trainingCutoff: 'October 2023',
totalCost: 0.45,
},
{
name: 'Gemini 2.5 Pro',
provider: '',
inputCost: 1.25,
cachedInputCost: '',
outputCost: 10,
inputLimit: 1048576,
outputLimit: 65536,
batchAPI: '',
hoster: 'Google',
trainingCutoff: 'January 2025',
totalCost: 6.25,
},
{
name: 'Gemini 2.0 Flash',
provider: 'Google',
inputCost: 0.1,
cachedInputCost: 0.025,
outputCost: 0.4,
inputLimit: 1048576,
outputLimit: 8192,
batchAPI: '',
hoster: 'Google',
trainingCutoff: 'June 2024',
totalCost: 0.3,
},
{
name: 'Gemini 2.0 Flash-Lite',
provider: 'Google',
inputCost: 0.075,
cachedInputCost: 0.01875,
outputCost: 0.3,
inputLimit: 1048576,
outputLimit: 8192,
batchAPI: '',
hoster: 'Google',
trainingCutoff: 'June 2024',
totalCost: 0.225,
},
];
// Formatierungsfunktionen für verschiedene Spaltentypen
const formatCurrency = (value: number | undefined | null): string => {
if (value === undefined || value === null) return "-";
return `${value}$`;
if (value === undefined || value === null) return '-';
return `${value}$`;
};
const formatNumber = (value: number | undefined | null): string => {
if (value === undefined || value === null) return "-";
return value.toLocaleString();
if (value === undefined || value === null) return '-';
return value.toLocaleString();
};
// Spaltendefinitionen mit festen Breiten
const columns = [
{
key: 'name',
title: 'Model',
width: '150px',
sticky: true
},
{
key: 'provider',
title: 'Provider',
width: '120px'
},
{
key: 'inputCost',
title: 'Input Cost',
subtitle: 'per 1M tokens',
formatter: formatCurrency,
align: 'right',
width: '120px'
},
{
key: 'cachedInputCost',
title: 'Cached Input Cost',
subtitle: 'per 1M tokens',
formatter: (value: any): string => typeof value === 'number' ? formatCurrency(value) : value,
align: 'right',
width: '150px'
},
{
key: 'outputCost',
title: 'Output Cost',
subtitle: 'per 1M tokens',
formatter: formatCurrency,
align: 'right',
width: '120px'
},
{
key: 'inputLimit',
title: 'Input Token Limit',
formatter: formatNumber,
align: 'right',
width: '140px'
},
{
key: 'outputLimit',
title: 'Output Token Limit',
formatter: formatNumber,
align: 'right',
width: '140px'
},
{
key: 'batchAPI',
title: 'Batch API',
width: '150px'
},
{
key: 'hoster',
title: 'Hoster',
width: '200px'
},
{
key: 'trainingCutoff',
title: 'Training Cut-off',
width: '140px'
},
{
key: 'totalCost',
title: 'Total Cost',
subtitle: '1M Input + 0.5M Output',
formatter: formatCurrency,
align: 'right',
width: '120px'
}
{
key: 'name',
title: 'Model',
width: '150px',
sticky: true,
},
{
key: 'provider',
title: 'Provider',
width: '120px',
},
{
key: 'inputCost',
title: 'Input Cost',
subtitle: 'per 1M tokens',
formatter: formatCurrency,
align: 'right',
width: '120px',
},
{
key: 'cachedInputCost',
title: 'Cached Input Cost',
subtitle: 'per 1M tokens',
formatter: (value: any): string => (typeof value === 'number' ? formatCurrency(value) : value),
align: 'right',
width: '150px',
},
{
key: 'outputCost',
title: 'Output Cost',
subtitle: 'per 1M tokens',
formatter: formatCurrency,
align: 'right',
width: '120px',
},
{
key: 'inputLimit',
title: 'Input Token Limit',
formatter: formatNumber,
align: 'right',
width: '140px',
},
{
key: 'outputLimit',
title: 'Output Token Limit',
formatter: formatNumber,
align: 'right',
width: '140px',
},
{
key: 'batchAPI',
title: 'Batch API',
width: '150px',
},
{
key: 'hoster',
title: 'Hoster',
width: '200px',
},
{
key: 'trainingCutoff',
title: 'Training Cut-off',
width: '140px',
},
{
key: 'totalCost',
title: 'Total Cost',
subtitle: '1M Input + 0.5M Output',
formatter: formatCurrency,
align: 'right',
width: '120px',
},
];
---
<section class="models-comparison-table">
<h2>AI Models Comparison</h2>
<TableImproved
data={models}
columns={columns}
stickyHeader={true}
stickyFirstColumn={true}
maxHeight="70vh"
sortable={true}
searchable={true}
className="models-table"
/>
<h2>AI Models Comparison</h2>
<TableImproved
data={models}
columns={columns}
stickyHeader={true}
stickyFirstColumn={true}
maxHeight="70vh"
sortable={true}
searchable={true}
className="models-table"
/>
</section>
<style>
.models-comparison-table {
margin-bottom: 2.5rem;
background: var(--background-secondary);
border-radius: 1rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
padding: 2rem 0;
width: calc(100vw - 2rem);
position: relative;
left: 50%;
right: 50%;
margin-left: calc(-50vw + 1rem);
margin-right: calc(-50vw + 1rem);
overflow: hidden;
}
h2 {
text-align: center;
margin-bottom: 2rem;
}
/* Zusätzliche Anpassungen für die Tabelle */
:global(.models-table) {
--background-secondary: var(--background-secondary, #f8fafc);
--background-tertiary: var(--background-tertiary, #edf2f7);
--border-color: var(--border-color, #e2e8f0);
}
.models-comparison-table {
margin-bottom: 2.5rem;
background: var(--background-secondary);
border-radius: 1rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
padding: 2rem 0;
width: calc(100vw - 2rem);
position: relative;
left: 50%;
right: 50%;
margin-left: calc(-50vw + 1rem);
margin-right: calc(-50vw + 1rem);
overflow: hidden;
}
h2 {
text-align: center;
margin-bottom: 2rem;
}
/* Zusätzliche Anpassungen für die Tabelle */
:global(.models-table) {
--background-secondary: var(--background-secondary, #f8fafc);
--background-tertiary: var(--background-tertiary, #edf2f7);
--border-color: var(--border-color, #e2e8f0);
}
</style>

View file

@ -2,148 +2,316 @@
import Table from './Table.astro';
const models = [
{ name: "o1", provider: "OpenAI", inputCost: 15, cachedInputCost: 7.5, outputCost: 60, inputLimit: 200000, outputLimit: 100000, batchAPI: "50% for 24 hours", hoster: "OpenAI, Azure", trainingCutoff: "October 2023", totalCost: 45 },
{ name: "gpt 4.1", provider: "OpenAI", inputCost: 2, cachedInputCost: 0.5, outputCost: 8, inputLimit: 1000000, outputLimit: 32768, batchAPI: "50% for 24 hours", hoster: "OpenAI, Azure", trainingCutoff: "June 2024", totalCost: 6 },
{ name: "GPT-4o", provider: "OpenAI", inputCost: 2.5, cachedInputCost: 1.25, outputCost: 10, inputLimit: 128000, outputLimit: 16384, batchAPI: "50% for 24 hours", hoster: "OpenAI, Azure", trainingCutoff: "October 2023", totalCost: 7.5 },
{ name: "gpt 4.1-mini", provider: "OpenAI", inputCost: 0.4, cachedInputCost: 0.1, outputCost: 1.6, inputLimit: 1000000, outputLimit: 32768, batchAPI: "50% for 24 hours", hoster: "OpenAI, Azure", trainingCutoff: "June 2024", totalCost: 1.2 },
{ name: "gpt 4.1-nano", provider: "OpenAI", inputCost: 0.1, cachedInputCost: 0.025, outputCost: 0.4, inputLimit: 1000000, outputLimit: 32768, batchAPI: "50% for 24 hours", hoster: "OpenAI, Azure", trainingCutoff: "June 2024", totalCost: 0.3 },
{ name: "o3-mini", provider: "OpenAI", inputCost: 1.1, cachedInputCost: 0.55, outputCost: 4.4, inputLimit: 200000, outputLimit: 100000, batchAPI: "50% for 24 hours", hoster: "OpenAI, Azure", trainingCutoff: "October 2023", totalCost: 3.3 },
{ name: "Claude Sonnet 3.7", provider: "Antrophic", inputCost: 3, cachedInputCost: "Up to 90%", outputCost: 15, inputLimit: 200000, outputLimit: 8192, batchAPI: "50% for 24 hours", hoster: "Google, Amazon, Antrophic", trainingCutoff: "April 2024", totalCost: 10.5 },
{ name: "Claude Haiku 3.5", provider: "Antrophic", inputCost: 0.8, cachedInputCost: "Up to 90%", outputCost: 4, inputLimit: 200000, outputLimit: 8192, batchAPI: "50% for 24 hours", hoster: "Google, Amazon, Antrophic", trainingCutoff: "July 2024", totalCost: 2.8 },
{ name: "Deepseek R1", provider: "Deepseek", inputCost: 0.55, cachedInputCost: 0.14, outputCost: 2.19, inputLimit: 64000, outputLimit: 8192, batchAPI: "", hoster: "Deepseek", trainingCutoff: "July 2024", totalCost: 1.645 },
{ name: "Deepseek V3", provider: "Deepseek", inputCost: 0.27, cachedInputCost: 0.07, outputCost: 1.1, inputLimit: 64000, outputLimit: 8192, batchAPI: "", hoster: "Deepseek", trainingCutoff: "December 2024", totalCost: 0.82 },
{ name: "GPT-4o mini", provider: "OpenAI", inputCost: 0.15, cachedInputCost: 0.075, outputCost: 0.6, inputLimit: 128000, outputLimit: 16384, batchAPI: "50% for 24 hours", hoster: "OpenAI, Azure", trainingCutoff: "October 2023", totalCost: 0.45 },
{ name: "Gemini 2.5 Pro", provider: "", inputCost: 1.25, cachedInputCost: "", outputCost: 10, inputLimit: 1048576, outputLimit: 65536, batchAPI: "", hoster: "Google", trainingCutoff: "January 2025", totalCost: 6.25 },
{ name: "Gemini 2.0 Flash", provider: "Google", inputCost: 0.1, cachedInputCost: 0.025, outputCost: 0.4, inputLimit: 1048576, outputLimit: 8192, batchAPI: "", hoster: "Google", trainingCutoff: "June 2024", totalCost: 0.3 },
{ name: "Gemini 2.0 Flash-Lite", provider: "Google", inputCost: 0.075, cachedInputCost: 0.01875, outputCost: 0.3, inputLimit: 1048576, outputLimit: 8192, batchAPI: "", hoster: "Google", trainingCutoff: "June 2024", totalCost: 0.225 },
{
name: 'o1',
provider: 'OpenAI',
inputCost: 15,
cachedInputCost: 7.5,
outputCost: 60,
inputLimit: 200000,
outputLimit: 100000,
batchAPI: '50% for 24 hours',
hoster: 'OpenAI, Azure',
trainingCutoff: 'October 2023',
totalCost: 45,
},
{
name: 'gpt 4.1',
provider: 'OpenAI',
inputCost: 2,
cachedInputCost: 0.5,
outputCost: 8,
inputLimit: 1000000,
outputLimit: 32768,
batchAPI: '50% for 24 hours',
hoster: 'OpenAI, Azure',
trainingCutoff: 'June 2024',
totalCost: 6,
},
{
name: 'GPT-4o',
provider: 'OpenAI',
inputCost: 2.5,
cachedInputCost: 1.25,
outputCost: 10,
inputLimit: 128000,
outputLimit: 16384,
batchAPI: '50% for 24 hours',
hoster: 'OpenAI, Azure',
trainingCutoff: 'October 2023',
totalCost: 7.5,
},
{
name: 'gpt 4.1-mini',
provider: 'OpenAI',
inputCost: 0.4,
cachedInputCost: 0.1,
outputCost: 1.6,
inputLimit: 1000000,
outputLimit: 32768,
batchAPI: '50% for 24 hours',
hoster: 'OpenAI, Azure',
trainingCutoff: 'June 2024',
totalCost: 1.2,
},
{
name: 'gpt 4.1-nano',
provider: 'OpenAI',
inputCost: 0.1,
cachedInputCost: 0.025,
outputCost: 0.4,
inputLimit: 1000000,
outputLimit: 32768,
batchAPI: '50% for 24 hours',
hoster: 'OpenAI, Azure',
trainingCutoff: 'June 2024',
totalCost: 0.3,
},
{
name: 'o3-mini',
provider: 'OpenAI',
inputCost: 1.1,
cachedInputCost: 0.55,
outputCost: 4.4,
inputLimit: 200000,
outputLimit: 100000,
batchAPI: '50% for 24 hours',
hoster: 'OpenAI, Azure',
trainingCutoff: 'October 2023',
totalCost: 3.3,
},
{
name: 'Claude Sonnet 3.7',
provider: 'Antrophic',
inputCost: 3,
cachedInputCost: 'Up to 90%',
outputCost: 15,
inputLimit: 200000,
outputLimit: 8192,
batchAPI: '50% for 24 hours',
hoster: 'Google, Amazon, Antrophic',
trainingCutoff: 'April 2024',
totalCost: 10.5,
},
{
name: 'Claude Haiku 3.5',
provider: 'Antrophic',
inputCost: 0.8,
cachedInputCost: 'Up to 90%',
outputCost: 4,
inputLimit: 200000,
outputLimit: 8192,
batchAPI: '50% for 24 hours',
hoster: 'Google, Amazon, Antrophic',
trainingCutoff: 'July 2024',
totalCost: 2.8,
},
{
name: 'Deepseek R1',
provider: 'Deepseek',
inputCost: 0.55,
cachedInputCost: 0.14,
outputCost: 2.19,
inputLimit: 64000,
outputLimit: 8192,
batchAPI: '',
hoster: 'Deepseek',
trainingCutoff: 'July 2024',
totalCost: 1.645,
},
{
name: 'Deepseek V3',
provider: 'Deepseek',
inputCost: 0.27,
cachedInputCost: 0.07,
outputCost: 1.1,
inputLimit: 64000,
outputLimit: 8192,
batchAPI: '',
hoster: 'Deepseek',
trainingCutoff: 'December 2024',
totalCost: 0.82,
},
{
name: 'GPT-4o mini',
provider: 'OpenAI',
inputCost: 0.15,
cachedInputCost: 0.075,
outputCost: 0.6,
inputLimit: 128000,
outputLimit: 16384,
batchAPI: '50% for 24 hours',
hoster: 'OpenAI, Azure',
trainingCutoff: 'October 2023',
totalCost: 0.45,
},
{
name: 'Gemini 2.5 Pro',
provider: '',
inputCost: 1.25,
cachedInputCost: '',
outputCost: 10,
inputLimit: 1048576,
outputLimit: 65536,
batchAPI: '',
hoster: 'Google',
trainingCutoff: 'January 2025',
totalCost: 6.25,
},
{
name: 'Gemini 2.0 Flash',
provider: 'Google',
inputCost: 0.1,
cachedInputCost: 0.025,
outputCost: 0.4,
inputLimit: 1048576,
outputLimit: 8192,
batchAPI: '',
hoster: 'Google',
trainingCutoff: 'June 2024',
totalCost: 0.3,
},
{
name: 'Gemini 2.0 Flash-Lite',
provider: 'Google',
inputCost: 0.075,
cachedInputCost: 0.01875,
outputCost: 0.3,
inputLimit: 1048576,
outputLimit: 8192,
batchAPI: '',
hoster: 'Google',
trainingCutoff: 'June 2024',
totalCost: 0.225,
},
];
// Formatierungsfunktionen für verschiedene Spaltentypen
const formatCurrency = (value) => {
if (value === undefined || value === null) return "-";
return `${value}$`;
if (value === undefined || value === null) return '-';
return `${value}$`;
};
const formatNumber = (value) => {
if (value === undefined || value === null) return "-";
return value.toLocaleString();
if (value === undefined || value === null) return '-';
return value.toLocaleString();
};
// Spaltendefinitionen
const columns = [
{
key: 'name',
title: 'Model',
width: '150px',
sticky: true
},
{
key: 'provider',
title: 'Provider'
},
{
key: 'inputCost',
title: 'Input Cost',
subtitle: 'per 1M tokens',
formatter: formatCurrency,
align: 'right'
},
{
key: 'cachedInputCost',
title: 'Cached Input Cost',
subtitle: 'per 1M tokens',
formatter: (value) => typeof value === 'number' ? formatCurrency(value) : value,
align: 'right'
},
{
key: 'outputCost',
title: 'Output Cost',
subtitle: 'per 1M tokens',
formatter: formatCurrency,
align: 'right'
},
{
key: 'inputLimit',
title: 'Input Token Limit',
formatter: formatNumber,
align: 'right'
},
{
key: 'outputLimit',
title: 'Output Token Limit',
formatter: formatNumber,
align: 'right'
},
{
key: 'batchAPI',
title: 'Batch API'
},
{
key: 'hoster',
title: 'Hoster'
},
{
key: 'trainingCutoff',
title: 'Training Cut-off'
},
{
key: 'totalCost',
title: 'Total Cost',
subtitle: '1M Input + 0.5M Output',
formatter: formatCurrency,
align: 'right'
}
{
key: 'name',
title: 'Model',
width: '150px',
sticky: true,
},
{
key: 'provider',
title: 'Provider',
},
{
key: 'inputCost',
title: 'Input Cost',
subtitle: 'per 1M tokens',
formatter: formatCurrency,
align: 'right',
},
{
key: 'cachedInputCost',
title: 'Cached Input Cost',
subtitle: 'per 1M tokens',
formatter: (value) => (typeof value === 'number' ? formatCurrency(value) : value),
align: 'right',
},
{
key: 'outputCost',
title: 'Output Cost',
subtitle: 'per 1M tokens',
formatter: formatCurrency,
align: 'right',
},
{
key: 'inputLimit',
title: 'Input Token Limit',
formatter: formatNumber,
align: 'right',
},
{
key: 'outputLimit',
title: 'Output Token Limit',
formatter: formatNumber,
align: 'right',
},
{
key: 'batchAPI',
title: 'Batch API',
},
{
key: 'hoster',
title: 'Hoster',
},
{
key: 'trainingCutoff',
title: 'Training Cut-off',
},
{
key: 'totalCost',
title: 'Total Cost',
subtitle: '1M Input + 0.5M Output',
formatter: formatCurrency,
align: 'right',
},
];
---
<section class="models-comparison-page">
<h1>AI Models Comparison</h1>
<Table
data={models}
columns={columns}
stickyHeader={true}
stickyFirstColumn={true}
maxHeight="70vh"
sortable={true}
searchable={true}
className="models-table"
/>
<div class="table-info">
<p>
<strong>Hinweis:</strong> Die Preise sind in US-Dollar angegeben und können sich ändern.
Die Gesamtkosten basieren auf 1 Million Input-Token und 0,5 Millionen Output-Token.
</p>
</div>
<h1>AI Models Comparison</h1>
<Table
data={models}
columns={columns}
stickyHeader={true}
stickyFirstColumn={true}
maxHeight="70vh"
sortable={true}
searchable={true}
className="models-table"
/>
<div class="table-info">
<p>
<strong>Hinweis:</strong> Die Preise sind in US-Dollar angegeben und können sich ändern. Die Gesamtkosten
basieren auf 1 Million Input-Token und 0,5 Millionen Output-Token.
</p>
</div>
</section>
<style>
.models-comparison-page {
margin: 2rem 0;
width: calc(100vw - 2rem);
position: relative;
left: 50%;
right: 50%;
margin-left: calc(-50vw + 1rem);
margin-right: calc(-50vw + 1rem);
}
h1 {
text-align: center;
margin-bottom: 2rem;
}
.table-info {
margin: 1.5rem 2rem;
font-size: 0.9rem;
color: var(--text-secondary);
}
/* Zusätzliche Anpassungen für die Tabelle */
:global(.models-table) {
--background-secondary: var(--color-background-secondary, #f8fafc);
--background-tertiary: var(--color-background-tertiary, #edf2f7);
--border-color: var(--color-border, #e2e8f0);
}
.models-comparison-page {
margin: 2rem 0;
width: calc(100vw - 2rem);
position: relative;
left: 50%;
right: 50%;
margin-left: calc(-50vw + 1rem);
margin-right: calc(-50vw + 1rem);
}
h1 {
text-align: center;
margin-bottom: 2rem;
}
.table-info {
margin: 1.5rem 2rem;
font-size: 0.9rem;
color: var(--text-secondary);
}
/* Zusätzliche Anpassungen für die Tabelle */
:global(.models-table) {
--background-secondary: var(--color-background-secondary, #f8fafc);
--background-tertiary: var(--color-background-tertiary, #edf2f7);
--border-color: var(--color-border, #e2e8f0);
}
</style>

File diff suppressed because it is too large Load diff

View file

@ -3,93 +3,92 @@
---
<script>
import { trackEvent, EVENTS } from '../scripts/analytics';
document.addEventListener('DOMContentLoaded', () => {
// Track navigation clicks
document.querySelectorAll('.nav-links a, .mobile-tab-bar a').forEach(link => {
link.addEventListener('click', (e) => {
const target = e.currentTarget as HTMLAnchorElement;
const text = target.textContent?.trim() || '';
const href = target.getAttribute('href') || '';
trackEvent(EVENTS.NAV_CLICK, {
label: text,
destination: href
});
});
});
// Track footer navigation clicks
document.querySelectorAll('footer a').forEach(link => {
link.addEventListener('click', (e) => {
const target = e.currentTarget as HTMLAnchorElement;
const text = target.textContent?.trim() || '';
const href = target.getAttribute('href') || '';
// Skip external links (they're tracked separately)
if (!href.startsWith('http')) {
trackEvent(EVENTS.FOOTER_CLICK, {
label: text,
destination: href
});
}
});
});
// Track language switcher
document.querySelectorAll('.language-switcher a, .language-option').forEach(link => {
link.addEventListener('click', (e) => {
const target = e.currentTarget as HTMLElement;
const lang = target.getAttribute('href')?.split('/')[1] ||
target.dataset.lang || '';
trackEvent(EVENTS.LANGUAGE_SWITCH, {
from: document.documentElement.lang,
to: lang
});
});
});
// Track theme toggle
const themeToggle = document.querySelector('.theme-toggle');
if (themeToggle) {
themeToggle.addEventListener('click', () => {
const currentTheme = document.documentElement.classList.contains('dark') ? 'light' : 'dark';
trackEvent(EVENTS.THEME_TOGGLE, {
theme: currentTheme
});
});
}
// Track CTA buttons
document.querySelectorAll('a[href*="/join"], .cta-button').forEach(button => {
button.addEventListener('click', (e) => {
const target = e.currentTarget as HTMLElement;
const text = target.textContent?.trim() || '';
const href = target.getAttribute('href') || '';
trackEvent(EVENTS.JOIN_CLICK, {
label: text,
destination: href,
location: target.closest('section')?.id || 'unknown'
});
});
});
// Track support/coffee buttons
document.querySelectorAll('a[href*="/support"], .support-link').forEach(button => {
button.addEventListener('click', (e) => {
const target = e.currentTarget as HTMLElement;
const text = target.textContent?.trim() || '';
const href = target.getAttribute('href') || '';
trackEvent(EVENTS.SUPPORT_CLICK, {
label: text,
destination: href,
location: target.closest('footer') ? 'footer' : 'page'
});
});
});
});
</script>
import { trackEvent, EVENTS } from '../scripts/analytics';
document.addEventListener('DOMContentLoaded', () => {
// Track navigation clicks
document.querySelectorAll('.nav-links a, .mobile-tab-bar a').forEach((link) => {
link.addEventListener('click', (e) => {
const target = e.currentTarget as HTMLAnchorElement;
const text = target.textContent?.trim() || '';
const href = target.getAttribute('href') || '';
trackEvent(EVENTS.NAV_CLICK, {
label: text,
destination: href,
});
});
});
// Track footer navigation clicks
document.querySelectorAll('footer a').forEach((link) => {
link.addEventListener('click', (e) => {
const target = e.currentTarget as HTMLAnchorElement;
const text = target.textContent?.trim() || '';
const href = target.getAttribute('href') || '';
// Skip external links (they're tracked separately)
if (!href.startsWith('http')) {
trackEvent(EVENTS.FOOTER_CLICK, {
label: text,
destination: href,
});
}
});
});
// Track language switcher
document.querySelectorAll('.language-switcher a, .language-option').forEach((link) => {
link.addEventListener('click', (e) => {
const target = e.currentTarget as HTMLElement;
const lang = target.getAttribute('href')?.split('/')[1] || target.dataset.lang || '';
trackEvent(EVENTS.LANGUAGE_SWITCH, {
from: document.documentElement.lang,
to: lang,
});
});
});
// Track theme toggle
const themeToggle = document.querySelector('.theme-toggle');
if (themeToggle) {
themeToggle.addEventListener('click', () => {
const currentTheme = document.documentElement.classList.contains('dark') ? 'light' : 'dark';
trackEvent(EVENTS.THEME_TOGGLE, {
theme: currentTheme,
});
});
}
// Track CTA buttons
document.querySelectorAll('a[href*="/join"], .cta-button').forEach((button) => {
button.addEventListener('click', (e) => {
const target = e.currentTarget as HTMLElement;
const text = target.textContent?.trim() || '';
const href = target.getAttribute('href') || '';
trackEvent(EVENTS.JOIN_CLICK, {
label: text,
destination: href,
location: target.closest('section')?.id || 'unknown',
});
});
});
// Track support/coffee buttons
document.querySelectorAll('a[href*="/support"], .support-link').forEach((button) => {
button.addEventListener('click', (e) => {
const target = e.currentTarget as HTMLElement;
const text = target.textContent?.trim() || '';
const href = target.getAttribute('href') || '';
trackEvent(EVENTS.SUPPORT_CLICK, {
label: text,
destination: href,
location: target.closest('footer') ? 'footer' : 'page',
});
});
});
});
</script>

View file

@ -1,12 +1,12 @@
---
import { getLangFromUrl, useTranslations } from "../utils/i18n";
import type { CollectionEntry } from "astro:content";
import { getLangFromUrl, useTranslations } from '../utils/i18n';
import type { CollectionEntry } from 'astro:content';
// Define allowed collections
type AllowedCollections = "news" | "models";
type AllowedCollections = 'news' | 'models';
interface Props {
news: CollectionEntry<AllowedCollections>;
news: CollectionEntry<AllowedCollections>;
}
const { news } = Astro.props;
@ -17,17 +17,17 @@ const t = useTranslations(lang);
// Format date based on language
const formattedDate = new Intl.DateTimeFormat(
lang === "de" ? "de-DE" : lang === "it" ? "it-IT" : "en-US",
{
year: "numeric",
month: "long",
day: "numeric",
}
lang === 'de' ? 'de-DE' : lang === 'it' ? 'it-IT' : 'en-US',
{
year: 'numeric',
month: 'long',
day: 'numeric',
}
).format(data.pubDate);
// Generate URL with language prefix
let pageUrl;
const slugParts = news.slug.split("/");
const slugParts = news.slug.split('/');
const fileName = slugParts[slugParts.length - 1];
// Determine collection for URL
const collection = news.collection as AllowedCollections;
@ -38,168 +38,166 @@ const categoryKey = `${collection}.categories.${data.category}` as const;
---
<a href={pageUrl} class="news-card-link">
<article class:list={["news-card", { featured: data.featured }]}>
{
data.image && (
<div class="news-image-container">
<img src={data.image} alt={data.title} class="news-image" />
</div>
)
}
<article class:list={['news-card', { featured: data.featured }]}>
{
data.image && (
<div class="news-image-container">
<img src={data.image} alt={data.title} class="news-image" />
</div>
)
}
<div class="content">
<div class="meta">
<span class="category">{t(categoryKey)}</span>
<span class="date">{formattedDate}</span>
</div>
<div class="content">
<div class="meta">
<span class="category">{t(categoryKey)}</span>
<span class="date">{formattedDate}</span>
</div>
<h3>{data.title}</h3>
<p>{data.description}</p>
<h3>{data.title}</h3>
<p>{data.description}</p>
<div class="footer">
<span class="author">{data.author}</span>
<span class="read-more"
>{t(`${collection}.readMore`)} <span class="arrow">→</span></span
>
</div>
</div>
</article>
<div class="footer">
<span class="author">{data.author}</span>
<span class="read-more">{t(`${collection}.readMore`)} <span class="arrow">→</span></span>
</div>
</div>
</article>
</a>
<style>
.news-card-link {
display: block;
text-decoration: none;
color: inherit;
height: 100%;
}
.news-card-link {
display: block;
text-decoration: none;
color: inherit;
height: 100%;
}
.news-card {
display: flex;
flex-direction: column;
border-radius: 0.75rem;
overflow: hidden;
background-color: var(--card-bg);
border: 2px solid rgba(255, 255, 255, 0.3);
box-shadow:
0 2px 4px -1px rgba(0, 0, 0, 0.06),
0 1px 2px -1px rgba(0, 0, 0, 0.03);
transition:
transform 0.2s ease,
border-color 0.2s ease,
box-shadow 0.2s ease;
height: 100%;
}
.news-card {
display: flex;
flex-direction: column;
border-radius: 0.75rem;
overflow: hidden;
background-color: var(--card-bg);
border: 2px solid rgba(255, 255, 255, 0.3);
box-shadow:
0 2px 4px -1px rgba(0, 0, 0, 0.06),
0 1px 2px -1px rgba(0, 0, 0, 0.03);
transition:
transform 0.2s ease,
border-color 0.2s ease,
box-shadow 0.2s ease;
height: 100%;
}
.news-card-link:hover .news-card {
transform: translateY(-2px);
border-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.5);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.08),
0 2px 4px -2px rgba(0, 0, 0, 0.04);
}
.news-card-link:hover .news-card {
transform: translateY(-2px);
border-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.5);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.08),
0 2px 4px -2px rgba(0, 0, 0, 0.04);
}
.news-card.featured {
border: 2px solid rgba(var(--accent-color-rgb, 249, 115, 22), 0.3);
}
.news-card.featured {
border: 2px solid rgba(var(--accent-color-rgb, 249, 115, 22), 0.3);
}
.news-image-container {
width: 100%;
padding-top: 56.25%; /* Aspect ratio 16:9 */
position: relative;
overflow: hidden;
}
.news-image-container {
width: 100%;
padding-top: 56.25%; /* Aspect ratio 16:9 */
position: relative;
overflow: hidden;
}
.news-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
.news-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
.content {
display: flex;
flex-direction: column;
padding: 1.5rem;
flex-grow: 1;
}
.content {
display: flex;
flex-direction: column;
padding: 1.5rem;
flex-grow: 1;
}
.meta {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
margin-bottom: 0.75rem;
}
.meta {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
margin-bottom: 0.75rem;
}
.category {
color: var(--accent-color);
font-weight: 600;
}
.category {
color: var(--accent-color);
font-weight: 600;
}
.date {
color: var(--text-muted);
}
.date {
color: var(--text-muted);
}
h3 {
margin: 0 0 0.75rem 0;
font-size: 1.25rem;
line-height: 1.4;
}
h3 {
margin: 0 0 0.75rem 0;
font-size: 1.25rem;
line-height: 1.4;
}
p {
margin: 0;
color: var(--text-muted);
line-height: 1.6;
flex-grow: 1;
margin-bottom: 1.5rem;
}
p {
margin: 0;
color: var(--text-muted);
line-height: 1.6;
flex-grow: 1;
margin-bottom: 1.5rem;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.author {
font-size: 0.875rem;
color: var(--text-muted);
}
.author {
font-size: 0.875rem;
color: var(--text-muted);
}
.read-more {
display: inline-flex;
align-items: center;
color: var(--accent-color);
font-weight: 600;
font-size: 0.875rem;
}
.read-more {
display: inline-flex;
align-items: center;
color: var(--accent-color);
font-weight: 600;
font-size: 0.875rem;
}
.arrow {
display: inline-block;
margin-left: 4px;
transition: transform 0.2s ease;
}
.arrow {
display: inline-block;
margin-left: 4px;
transition: transform 0.2s ease;
}
.news-card-link:hover .arrow {
transform: translateX(4px);
}
.news-card-link:hover .arrow {
transform: translateX(4px);
}
/* Tags */
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.75rem;
}
/* Tags */
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.75rem;
}
.tag {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
background-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.1);
color: var(--accent-color);
border-radius: 0.25rem;
}
.tag {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
background-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.1);
color: var(--accent-color);
border-radius: 0.25rem;
}
</style>

View file

@ -1,9 +1,9 @@
---
import { getLangFromUrl, useTranslations } from "../utils/i18n";
import { getLangFromUrl, useTranslations } from '../utils/i18n';
interface Props {
customTitle?: string;
customDescription?: string;
customTitle?: string;
customDescription?: string;
}
const { customTitle, customDescription } = Astro.props;
@ -12,145 +12,145 @@ const t = useTranslations(lang);
---
<div class="newsletter-container">
<h2>{customTitle || t("join.newsletterTitle")}</h2>
<p class="newsletter-description">
{customDescription || t("join.newsletterDesc")}
</p>
<h2>{customTitle || t('join.newsletterTitle')}</h2>
<p class="newsletter-description">
{customDescription || t('join.newsletterDesc')}
</p>
<form class="newsletter-form" id="newsletterForm">
<input
type="email"
id="newsletterEmail"
name="email"
placeholder={t("join.emailPlaceholder")}
required
/>
<button type="submit">{t("join.subscribe")}</button>
</form>
<form class="newsletter-form" id="newsletterForm">
<input
type="email"
id="newsletterEmail"
name="email"
placeholder={t('join.emailPlaceholder')}
required
/>
<button type="submit">{t('join.subscribe')}</button>
</form>
<div id="subscribeSuccess" class="subscribe-success" style="display: none;">
<p>{t("join.success")}</p>
</div>
<div id="subscribeSuccess" class="subscribe-success" style="display: none;">
<p>{t('join.success')}</p>
</div>
</div>
<style>
.newsletter-container {
background-color: var(--card-bg);
border-radius: 0.75rem;
padding: 2rem;
border: 1px solid var(--border-color);
margin-bottom: 2rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.newsletter-container {
background-color: var(--card-bg);
border-radius: 0.75rem;
padding: 2rem;
border: 1px solid var(--border-color);
margin-bottom: 2rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
/* Heller Modus */
:root:not(.dark) .newsletter-container {
background-color: var(--card-bg);
}
/* Heller Modus */
:root:not(.dark) .newsletter-container {
background-color: var(--card-bg);
}
h2 {
margin-top: 0;
margin-bottom: 1rem;
font-size: 1.5rem;
}
h2 {
margin-top: 0;
margin-bottom: 1rem;
font-size: 1.5rem;
}
.newsletter-description {
margin-bottom: 1.5rem;
color: var(--text-muted);
}
.newsletter-description {
margin-bottom: 1.5rem;
color: var(--text-muted);
}
.newsletter-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.newsletter-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
input {
padding: 0.75rem;
border-radius: 0.375rem;
border: 1px solid var(--border-color);
background-color: var(--background-color);
color: var(--text-color);
font-size: 1rem;
transition: border-color 0.2s ease;
width: 100%;
}
input {
padding: 0.75rem;
border-radius: 0.375rem;
border: 1px solid var(--border-color);
background-color: var(--background-color);
color: var(--text-color);
font-size: 1rem;
transition: border-color 0.2s ease;
width: 100%;
}
/* Heller Modus für Eingabefelder */
:root:not(.dark) input {
background-color: var(--background-color);
}
/* Heller Modus für Eingabefelder */
:root:not(.dark) input {
background-color: var(--background-color);
}
input:focus {
outline: none;
border-color: var(--accent-color);
}
input:focus {
outline: none;
border-color: var(--accent-color);
}
button {
background-color: var(--accent-color);
color: white;
border: none;
border-radius: 0.375rem;
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s ease;
width: max-content;
}
button {
background-color: var(--accent-color);
color: white;
border: none;
border-radius: 0.375rem;
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s ease;
width: max-content;
}
button:hover {
background-color: var(--accent-hover);
}
button:hover {
background-color: var(--accent-hover);
}
.subscribe-success {
background-color: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.3);
border-radius: 0.375rem;
padding: 1rem;
margin-top: 1rem;
}
.subscribe-success {
background-color: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.3);
border-radius: 0.375rem;
padding: 1rem;
margin-top: 1rem;
}
.subscribe-success p {
color: #10b981;
margin: 0;
font-weight: 500;
}
.subscribe-success p {
color: #10b981;
margin: 0;
font-weight: 500;
}
/* Responsive layout for larger screens */
@media (min-width: 640px) {
.newsletter-form {
flex-direction: row;
}
/* Responsive layout for larger screens */
@media (min-width: 640px) {
.newsletter-form {
flex-direction: row;
}
input {
flex: 1;
}
input {
flex: 1;
}
button {
flex-shrink: 0;
}
}
button {
flex-shrink: 0;
}
}
</style>
<script>
document.addEventListener("DOMContentLoaded", () => {
const form = document.getElementById("newsletterForm");
const successMessage = document.getElementById("subscribeSuccess");
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('newsletterForm');
const successMessage = document.getElementById('subscribeSuccess');
form.addEventListener("submit", (e) => {
e.preventDefault();
form.addEventListener('submit', (e) => {
e.preventDefault();
// Simulate form submission (in a real app, this would send data to a server)
setTimeout(() => {
form.reset();
successMessage.style.display = "block";
// Simulate form submission (in a real app, this would send data to a server)
setTimeout(() => {
form.reset();
successMessage.style.display = 'block';
// Hide success message after 5 seconds
setTimeout(() => {
successMessage.style.display = "none";
}, 5000);
}, 1000);
});
});
// Hide success message after 5 seconds
setTimeout(() => {
successMessage.style.display = 'none';
}, 5000);
}, 1000);
});
});
</script>

View file

@ -1,8 +1,8 @@
---
import { useTranslations } from "../utils/i18n";
import { useTranslations } from '../utils/i18n';
interface Props {
lang: "de" | "en" | "it";
lang: 'de' | 'en' | 'it';
}
const { lang } = Astro.props;
@ -10,79 +10,75 @@ const t = useTranslations(lang);
---
<div class="payment-container">
<div class="payment-options">
<div class="payment-type-selector">
<button id="one-time" class="payment-type-btn active"
>{t("support.onetime")}</button
>
<button id="recurring" class="payment-type-btn"
>{t("support.recurring")}</button
>
</div>
<div class="payment-options">
<div class="payment-type-selector">
<button id="one-time" class="payment-type-btn active">{t('support.onetime')}</button>
<button id="recurring" class="payment-type-btn">{t('support.recurring')}</button>
</div>
<div class="coffee-options">
<h3>{t("support.buyMeACoffee")}</h3>
<div class="coffee-selector">
<div
class="coffee-option"
data-price="3"
data-price-id-onetime="price_1R8xkmAZjQCYS0ZJk6PZtJUm"
data-price-id-recurring="price_1R8xkmAZjQCYS0ZJSBkFXcE1"
>
<div class="coffee-icon small-coffee">☕</div>
<div class="coffee-details">
<h4>Kleiner Kaffee</h4>
<p>3€</p>
</div>
</div>
<div
class="coffee-option active"
data-price="5"
data-price-id-onetime="price_1R8xmlAZjQCYS0ZJ4QCSZFFN"
data-price-id-recurring="price_1R8xn3AZjQCYS0ZJIXddpl0O"
>
<div class="coffee-icon medium-coffee">☕</div>
<div class="coffee-details">
<h4>Mittlerer Kaffee</h4>
<p>5€</p>
</div>
</div>
<div
class="coffee-option"
data-price="8"
data-price-id-onetime="price_1R8xp4AZjQCYS0ZJCkTA6gCs"
data-price-id-recurring="price_1R8xpIAZjQCYS0ZJ93nqMjop"
>
<div class="coffee-icon large-coffee">☕</div>
<div class="coffee-details">
<h4>Großer Kaffee</h4>
<p>8€</p>
</div>
</div>
</div>
</div>
<div class="coffee-options">
<h3>{t('support.buyMeACoffee')}</h3>
<div class="coffee-selector">
<div
class="coffee-option"
data-price="3"
data-price-id-onetime="price_1R8xkmAZjQCYS0ZJk6PZtJUm"
data-price-id-recurring="price_1R8xkmAZjQCYS0ZJSBkFXcE1"
>
<div class="coffee-icon small-coffee">☕</div>
<div class="coffee-details">
<h4>Kleiner Kaffee</h4>
<p>3€</p>
</div>
</div>
<div
class="coffee-option active"
data-price="5"
data-price-id-onetime="price_1R8xmlAZjQCYS0ZJ4QCSZFFN"
data-price-id-recurring="price_1R8xn3AZjQCYS0ZJIXddpl0O"
>
<div class="coffee-icon medium-coffee">☕</div>
<div class="coffee-details">
<h4>Mittlerer Kaffee</h4>
<p>5€</p>
</div>
</div>
<div
class="coffee-option"
data-price="8"
data-price-id-onetime="price_1R8xp4AZjQCYS0ZJCkTA6gCs"
data-price-id-recurring="price_1R8xpIAZjQCYS0ZJ93nqMjop"
>
<div class="coffee-icon large-coffee">☕</div>
<div class="coffee-details">
<h4>Großer Kaffee</h4>
<p>8€</p>
</div>
</div>
</div>
</div>
<div class="payment-methods">
<p>{t("support.paymentMethod")}</p>
<div class="payment-buttons">
<button id="stripe-button" class="payment-method-btn">
<svg
viewBox="0 0 60 25"
xmlns="http://www.w3.org/2000/svg"
width="60"
height="25"
class="UserLogo variant--"
>
<title>Stripe logo</title>
<path
fill="var(--text-color)"
d="M59.64 14.28h-8.06c.19 1.93 1.6 2.55 3.2 2.55 1.64 0 2.96-.37 4.05-.95v3.32a8.33 8.33 0 0 1-4.56 1.1c-4.01 0-6.83-2.5-6.83-7.48 0-4.19 2.39-7.52 6.3-7.52 3.92 0 5.96 3.28 5.96 7.5 0 .4-.04 1.26-.06 1.48zm-5.92-5.62c-1.03 0-2.17.73-2.17 2.58h4.25c0-1.85-1.07-2.58-2.08-2.58zM40.95 20.3c-1.44 0-2.32-.6-2.9-1.04l-.02 4.63-4.12.87V5.57h3.76l.08 1.02a4.7 4.7 0 0 1 3.23-1.29c2.9 0 5.62 2.6 5.62 7.4 0 5.23-2.7 7.6-5.65 7.6zM40 8.95c-.95 0-1.54.34-1.97.81l.02 6.12c.4.44.98.78 1.95.78 1.52 0 2.54-1.65 2.54-3.87 0-2.15-1.04-3.84-2.54-3.84zM28.24 5.57h4.13v14.44h-4.13V5.57zm0-4.7L32.37 0v3.36l-4.13.88V.88zm-4.32 9.35v9.79H19.8V5.57h3.7l.12 1.22c1-1.77 3.07-1.41 3.62-1.22v3.79c-.52-.17-2.29-.43-3.32.86zm-8.55 4.72c0 2.43 2.6 1.68 3.12 1.46v3.36c-.55.3-1.54.54-2.89.54a4.15 4.15 0 0 1-4.27-4.24l.02-13.17 4.02-.86v3.54h3.14V9.1h-3.14v5.85zm-4.91.7c0 2.97-2.31 4.66-5.73 4.66a11.2 11.2 0 0 1-4.46-.93v-3.93c1.38.75 3.1 1.31 4.46 1.31.92 0 1.53-.24 1.53-1C6.26 13.77 0 14.51 0 9.95 0 7.04 2.28 5.3 5.62 5.3c1.36 0 2.72.2 4.09.75v3.88a9.23 9.23 0 0 0-4.1-1.06c-.86 0-1.44.25-1.44.9 0 1.85 6.29.97 6.29 5.88z"
fill-rule="evenodd"></path>
</svg>
<span>{t("support.payWithStripe")}</span>
</button>
{
/* PayPal Button wird später implementiert
<div class="payment-methods">
<p>{t('support.paymentMethod')}</p>
<div class="payment-buttons">
<button id="stripe-button" class="payment-method-btn">
<svg
viewBox="0 0 60 25"
xmlns="http://www.w3.org/2000/svg"
width="60"
height="25"
class="UserLogo variant--"
>
<title>Stripe logo</title>
<path
fill="var(--text-color)"
d="M59.64 14.28h-8.06c.19 1.93 1.6 2.55 3.2 2.55 1.64 0 2.96-.37 4.05-.95v3.32a8.33 8.33 0 0 1-4.56 1.1c-4.01 0-6.83-2.5-6.83-7.48 0-4.19 2.39-7.52 6.3-7.52 3.92 0 5.96 3.28 5.96 7.5 0 .4-.04 1.26-.06 1.48zm-5.92-5.62c-1.03 0-2.17.73-2.17 2.58h4.25c0-1.85-1.07-2.58-2.08-2.58zM40.95 20.3c-1.44 0-2.32-.6-2.9-1.04l-.02 4.63-4.12.87V5.57h3.76l.08 1.02a4.7 4.7 0 0 1 3.23-1.29c2.9 0 5.62 2.6 5.62 7.4 0 5.23-2.7 7.6-5.65 7.6zM40 8.95c-.95 0-1.54.34-1.97.81l.02 6.12c.4.44.98.78 1.95.78 1.52 0 2.54-1.65 2.54-3.87 0-2.15-1.04-3.84-2.54-3.84zM28.24 5.57h4.13v14.44h-4.13V5.57zm0-4.7L32.37 0v3.36l-4.13.88V.88zm-4.32 9.35v9.79H19.8V5.57h3.7l.12 1.22c1-1.77 3.07-1.41 3.62-1.22v3.79c-.52-.17-2.29-.43-3.32.86zm-8.55 4.72c0 2.43 2.6 1.68 3.12 1.46v3.36c-.55.3-1.54.54-2.89.54a4.15 4.15 0 0 1-4.27-4.24l.02-13.17 4.02-.86v3.54h3.14V9.1h-3.14v5.85zm-4.91.7c0 2.97-2.31 4.66-5.73 4.66a11.2 11.2 0 0 1-4.46-.93v-3.93c1.38.75 3.1 1.31 4.46 1.31.92 0 1.53-.24 1.53-1C6.26 13.77 0 14.51 0 9.95 0 7.04 2.28 5.3 5.62 5.3c1.36 0 2.72.2 4.09.75v3.88a9.23 9.23 0 0 0-4.1-1.06c-.86 0-1.44.25-1.44.9 0 1.85 6.29.97 6.29 5.88z"
fill-rule="evenodd"></path>
</svg>
<span>{t('support.payWithStripe')}</span>
</button>
{
/* PayPal Button wird später implementiert
<button id="paypal-button" class="payment-method-btn">
<svg
width="101"
@ -99,462 +95,449 @@ const t = useTranslations(lang);
<span>{t("support.payWithPayPal")}</span>
</button>
*/
}
</div>
</div>
}
</div>
</div>
<div id="payment-message"></div>
</div>
<div id="payment-message"></div>
</div>
</div>
<style>
.payment-container {
background-color: var(--card-bg);
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
padding: 2.5rem;
width: 100%;
max-width: 550px;
transition: all 0.3s ease;
border: 1px solid rgba(var(--border-color-rgb), 0.1);
}
.payment-container {
background-color: var(--card-bg);
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
padding: 2.5rem;
width: 100%;
max-width: 550px;
transition: all 0.3s ease;
border: 1px solid rgba(var(--border-color-rgb), 0.1);
}
.payment-options {
display: flex;
flex-direction: column;
gap: 2rem;
}
.payment-options {
display: flex;
flex-direction: column;
gap: 2rem;
}
.payment-type-selector {
display: flex;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
border: 1px solid rgba(var(--border-color-rgb), 0.2);
}
.payment-type-selector {
display: flex;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
border: 1px solid rgba(var(--border-color-rgb), 0.2);
}
.payment-type-btn {
flex: 1;
padding: 1.2rem;
border: none;
background: var(--card-bg);
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
color: var(--text-color);
font-size: 1.1rem;
position: relative;
overflow: hidden;
}
.payment-type-btn {
flex: 1;
padding: 1.2rem;
border: none;
background: var(--card-bg);
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
color: var(--text-color);
font-size: 1.1rem;
position: relative;
overflow: hidden;
}
.payment-type-btn:after {
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 3px;
background-color: transparent;
transition: all 0.3s ease;
}
.payment-type-btn:after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 3px;
background-color: transparent;
transition: all 0.3s ease;
}
.payment-type-btn.active {
background-color: rgba(var(--accent-color-rgb), 0.1);
color: var(--accent-color);
}
.payment-type-btn.active {
background-color: rgba(var(--accent-color-rgb), 0.1);
color: var(--accent-color);
}
.payment-type-btn.active:after {
background-color: var(--accent-color);
}
.payment-type-btn.active:after {
background-color: var(--accent-color);
}
/* Coffee Options */
.coffee-options {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
/* Coffee Options */
.coffee-options {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.coffee-options h3 {
text-align: center;
margin-bottom: 0.5rem;
font-size: 1.3rem;
color: var(--text-color);
}
.coffee-options h3 {
text-align: center;
margin-bottom: 0.5rem;
font-size: 1.3rem;
color: var(--text-color);
}
.coffee-selector {
display: flex;
gap: 1.2rem;
justify-content: space-between;
}
.coffee-selector {
display: flex;
gap: 1.2rem;
justify-content: space-between;
}
.coffee-option {
flex: 1;
border: 2px solid transparent;
background-color: rgba(var(--border-color-rgb), 0.05);
border-radius: 12px;
padding: 1.2rem 0.8rem;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.coffee-option {
flex: 1;
border: 2px solid transparent;
background-color: rgba(var(--border-color-rgb), 0.05);
border-radius: 12px;
padding: 1.2rem 0.8rem;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.coffee-option:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.05);
}
.coffee-option:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.05);
}
.coffee-option.active {
border-color: var(--accent-color);
background-color: rgba(var(--accent-color-rgb), 0.08);
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.08);
}
.coffee-option.active {
border-color: var(--accent-color);
background-color: rgba(var(--accent-color-rgb), 0.08);
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.08);
}
.coffee-icon {
margin-bottom: 0.8rem;
transition: transform 0.3s ease;
}
.coffee-icon {
margin-bottom: 0.8rem;
transition: transform 0.3s ease;
}
.coffee-option:hover .coffee-icon {
transform: scale(1.1);
}
.coffee-option:hover .coffee-icon {
transform: scale(1.1);
}
.small-coffee {
font-size: 1.5rem;
}
.small-coffee {
font-size: 1.5rem;
}
.medium-coffee {
font-size: 2rem;
}
.medium-coffee {
font-size: 2rem;
}
.large-coffee {
font-size: 2.5rem;
}
.large-coffee {
font-size: 2.5rem;
}
.coffee-details h4 {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
color: var(--text-color);
}
.coffee-details h4 {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
color: var(--text-color);
}
.coffee-details p {
margin: 0.25rem 0 0 0;
font-weight: 600;
color: var(--accent-color);
font-size: 1.2rem;
}
.coffee-details p {
margin: 0.25rem 0 0 0;
font-weight: 600;
color: var(--accent-color);
font-size: 1.2rem;
}
.payment-methods {
display: flex;
flex-direction: column;
gap: 1rem;
}
.payment-methods {
display: flex;
flex-direction: column;
gap: 1rem;
}
.payment-methods p {
text-align: center;
font-weight: 500;
color: var(--text-muted);
margin: 0 0 0.5rem 0;
}
.payment-methods p {
text-align: center;
font-weight: 500;
color: var(--text-muted);
margin: 0 0 0.5rem 0;
}
.payment-buttons {
display: flex;
flex-direction: column;
gap: 1rem;
}
.payment-buttons {
display: flex;
flex-direction: column;
gap: 1rem;
}
.payment-method-btn {
padding: 1rem 1.5rem;
border-radius: 12px;
border: 1px solid rgba(var(--border-color-rgb), 0.15);
background-color: var(--background-color);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
transition: all 0.3s ease;
color: var(--text-color);
font-weight: 500;
font-size: 1.1rem;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
}
.payment-method-btn {
padding: 1rem 1.5rem;
border-radius: 12px;
border: 1px solid rgba(var(--border-color-rgb), 0.15);
background-color: var(--background-color);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
transition: all 0.3s ease;
color: var(--text-color);
font-weight: 500;
font-size: 1.1rem;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
}
.payment-method-btn:hover {
background-color: var(--hover-bg);
transform: translateY(-2px);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.08);
}
.payment-method-btn:hover {
background-color: var(--hover-bg);
transform: translateY(-2px);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.08);
}
.payment-method-btn span {
font-weight: 600;
}
.payment-method-btn span {
font-weight: 600;
}
#payment-message {
color: var(--accent-color);
text-align: center;
padding: 1.2rem;
margin-top: 1rem;
border-radius: 12px;
display: none;
font-weight: 500;
}
#payment-message {
color: var(--accent-color);
text-align: center;
padding: 1.2rem;
margin-top: 1rem;
border-radius: 12px;
display: none;
font-weight: 500;
}
#payment-message.success {
background-color: rgba(var(--accent-color-rgb), 0.1);
border: 1px solid var(--accent-color);
display: block;
}
#payment-message.success {
background-color: rgba(var(--accent-color-rgb), 0.1);
border: 1px solid var(--accent-color);
display: block;
}
#payment-message.error {
background-color: rgba(255, 0, 0, 0.1);
border: 1px solid red;
color: red;
display: block;
}
#payment-message.error {
background-color: rgba(255, 0, 0, 0.1);
border: 1px solid red;
color: red;
display: block;
}
@media (max-width: 480px) {
.payment-container {
padding: 1.5rem;
}
@media (max-width: 480px) {
.payment-container {
padding: 1.5rem;
}
.coffee-selector {
flex-direction: column;
gap: 0.8rem;
}
.coffee-selector {
flex-direction: column;
gap: 0.8rem;
}
.coffee-option {
padding: 1rem;
}
.coffee-option {
padding: 1rem;
}
.payment-type-btn {
padding: 1rem;
font-size: 1rem;
}
}
.payment-type-btn {
padding: 1rem;
font-size: 1rem;
}
}
</style>
<script>
// Import Stripe and PayPal libraries
import { loadStripe } from "@stripe/stripe-js";
// import { loadScript } from "@paypal/paypal-js"; // Wird später benötigt
import { trackEvent, EVENTS } from '../scripts/analytics';
// Import Stripe and PayPal libraries
import { loadStripe } from '@stripe/stripe-js';
// import { loadScript } from "@paypal/paypal-js"; // Wird später benötigt
import { trackEvent, EVENTS } from '../scripts/analytics';
// TypeScript-Definitionen für Plausible (legacy)
declare global {
interface Window {
plausible?: (
eventName: string,
options?: { props?: Record<string, any> }
) => void;
}
}
// TypeScript-Definitionen für Plausible (legacy)
declare global {
interface Window {
plausible?: (eventName: string, options?: { props?: Record<string, any> }) => void;
}
}
// Interface für Coffee Option Element
interface CoffeeOptionElement extends Element {
dataset: {
price: string;
priceIdOnetime: string;
priceIdRecurring: string;
};
}
// Interface für Coffee Option Element
interface CoffeeOptionElement extends Element {
dataset: {
price: string;
priceIdOnetime: string;
priceIdRecurring: string;
};
}
document.addEventListener("DOMContentLoaded", async () => {
const oneTimeBtn = document.getElementById("one-time");
const recurringBtn = document.getElementById("recurring");
const stripeBtn = document.getElementById("stripe-button");
// const paypalBtn = document.getElementById("paypal-button"); // Wird später benötigt
const coffeeOptions =
document.querySelectorAll<CoffeeOptionElement>(".coffee-option");
const paymentMessage = document.getElementById("payment-message");
document.addEventListener('DOMContentLoaded', async () => {
const oneTimeBtn = document.getElementById('one-time');
const recurringBtn = document.getElementById('recurring');
const stripeBtn = document.getElementById('stripe-button');
// const paypalBtn = document.getElementById("paypal-button"); // Wird später benötigt
const coffeeOptions = document.querySelectorAll<CoffeeOptionElement>('.coffee-option');
const paymentMessage = document.getElementById('payment-message');
if (!paymentMessage) {
console.error("Payment message element not found");
return;
}
if (!paymentMessage) {
console.error('Payment message element not found');
return;
}
// Kaffeegrößen-Auswahl
let selectedCoffeeOption = document.querySelector<CoffeeOptionElement>(
".coffee-option.active"
);
let selectedAmount = Number(selectedCoffeeOption?.dataset.price || 5);
let selectedPriceId = selectedCoffeeOption?.dataset.priceIdOnetime;
// Kaffeegrößen-Auswahl
let selectedCoffeeOption = document.querySelector<CoffeeOptionElement>('.coffee-option.active');
let selectedAmount = Number(selectedCoffeeOption?.dataset.price || 5);
let selectedPriceId = selectedCoffeeOption?.dataset.priceIdOnetime;
// Event-Listener für Kaffee-Optionen
coffeeOptions.forEach((option) => {
option.addEventListener("click", () => {
// Vorherige Auswahl zurücksetzen
coffeeOptions.forEach((opt) => opt.classList.remove("active"));
// Event-Listener für Kaffee-Optionen
coffeeOptions.forEach((option) => {
option.addEventListener('click', () => {
// Vorherige Auswahl zurücksetzen
coffeeOptions.forEach((opt) => opt.classList.remove('active'));
// Neue Auswahl aktivieren
option.classList.add("active");
selectedCoffeeOption = option;
selectedAmount = Number(option.dataset.price);
// Neue Auswahl aktivieren
option.classList.add('active');
selectedCoffeeOption = option;
selectedAmount = Number(option.dataset.price);
// Preis-ID basierend auf dem Zahlungstyp setzen
if (recurringBtn?.classList.contains("active")) {
selectedPriceId = option.dataset.priceIdRecurring;
} else {
selectedPriceId = option.dataset.priceIdOnetime;
}
// Preis-ID basierend auf dem Zahlungstyp setzen
if (recurringBtn?.classList.contains('active')) {
selectedPriceId = option.dataset.priceIdRecurring;
} else {
selectedPriceId = option.dataset.priceIdOnetime;
}
// Plausible Event für Kaffeegrößen-Auswahl
const coffeeTitle = option.querySelector("h4");
if (coffeeTitle) {
trackEvent(EVENTS.COFFEE_SELECT, {
size: coffeeTitle.textContent,
amount: selectedAmount,
type: recurringBtn?.classList.contains("active") ? "recurring" : "one-time"
});
}
});
});
// Plausible Event für Kaffeegrößen-Auswahl
const coffeeTitle = option.querySelector('h4');
if (coffeeTitle) {
trackEvent(EVENTS.COFFEE_SELECT, {
size: coffeeTitle.textContent,
amount: selectedAmount,
type: recurringBtn?.classList.contains('active') ? 'recurring' : 'one-time',
});
}
});
});
// Toggle zwischen Zahlungstypen
oneTimeBtn?.addEventListener("click", () => {
oneTimeBtn.classList.add("active");
recurringBtn?.classList.remove("active");
// Toggle zwischen Zahlungstypen
oneTimeBtn?.addEventListener('click', () => {
oneTimeBtn.classList.add('active');
recurringBtn?.classList.remove('active');
// Preis-ID aktualisieren
if (selectedCoffeeOption) {
selectedPriceId = selectedCoffeeOption.dataset.priceIdOnetime;
}
// Preis-ID aktualisieren
if (selectedCoffeeOption) {
selectedPriceId = selectedCoffeeOption.dataset.priceIdOnetime;
}
// Plausible Event für Einmalzahlung
trackEvent(EVENTS.PAYMENT_TYPE_CHANGE, {
type: "one-time",
amount: selectedAmount
});
});
// Plausible Event für Einmalzahlung
trackEvent(EVENTS.PAYMENT_TYPE_CHANGE, {
type: 'one-time',
amount: selectedAmount,
});
});
recurringBtn?.addEventListener("click", () => {
recurringBtn.classList.add("active");
oneTimeBtn?.classList.remove("active");
recurringBtn?.addEventListener('click', () => {
recurringBtn.classList.add('active');
oneTimeBtn?.classList.remove('active');
// Preis-ID aktualisieren
if (selectedCoffeeOption) {
selectedPriceId = selectedCoffeeOption.dataset.priceIdRecurring;
}
// Preis-ID aktualisieren
if (selectedCoffeeOption) {
selectedPriceId = selectedCoffeeOption.dataset.priceIdRecurring;
}
// Plausible Event für wiederkehrende Zahlung
trackEvent(EVENTS.PAYMENT_TYPE_CHANGE, {
type: "recurring",
amount: selectedAmount
});
});
// Plausible Event für wiederkehrende Zahlung
trackEvent(EVENTS.PAYMENT_TYPE_CHANGE, {
type: 'recurring',
amount: selectedAmount,
});
});
// Dieser Stripe Key wird aus den Netlify Umgebungsvariablen geladen
const stripePromise = loadStripe(
import.meta.env.PUBLIC_STRIPE_PUBLISHABLE_KEY || "pk_test_placeholder"
);
// Dieser Stripe Key wird aus den Netlify Umgebungsvariablen geladen
const stripePromise = loadStripe(
import.meta.env.PUBLIC_STRIPE_PUBLISHABLE_KEY || 'pk_test_placeholder'
);
// Stripe payment processing
stripeBtn?.addEventListener("click", async () => {
const amount = selectedAmount;
const isRecurring = recurringBtn?.classList.contains("active");
const priceId = selectedPriceId;
// Stripe payment processing
stripeBtn?.addEventListener('click', async () => {
const amount = selectedAmount;
const isRecurring = recurringBtn?.classList.contains('active');
const priceId = selectedPriceId;
paymentMessage.textContent = "Verarbeite Zahlung...";
paymentMessage.classList.remove("success", "error");
paymentMessage.style.display = "block";
paymentMessage.textContent = 'Verarbeite Zahlung...';
paymentMessage.classList.remove('success', 'error');
paymentMessage.style.display = 'block';
// Plausible Event für Stripe-Zahlungsstart
const coffeeTitle = selectedCoffeeOption?.querySelector("h4");
trackEvent(EVENTS.CHECKOUT_START, {
provider: "stripe",
amount: amount,
type: isRecurring ? "recurring" : "one-time",
coffeeSize: coffeeTitle?.textContent || "Mittlerer Kaffee",
priceId: selectedPriceId
});
// Plausible Event für Stripe-Zahlungsstart
const coffeeTitle = selectedCoffeeOption?.querySelector('h4');
trackEvent(EVENTS.CHECKOUT_START, {
provider: 'stripe',
amount: amount,
type: isRecurring ? 'recurring' : 'one-time',
coffeeSize: coffeeTitle?.textContent || 'Mittlerer Kaffee',
priceId: selectedPriceId,
});
try {
// Create a payment intent on the server using Netlify Functions
const response = await fetch(
"/.netlify/functions/create-payment-intent",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
amount,
isRecurring,
priceId,
coffeeSize: coffeeTitle?.textContent || "Mittlerer Kaffee",
}),
}
);
try {
// Create a payment intent on the server using Netlify Functions
const response = await fetch('/.netlify/functions/create-payment-intent', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount,
isRecurring,
priceId,
coffeeSize: coffeeTitle?.textContent || 'Mittlerer Kaffee',
}),
});
if (!response.ok) {
throw new Error("Netzwerkantwort nicht ok");
}
if (!response.ok) {
throw new Error('Netzwerkantwort nicht ok');
}
const data = await response.json();
const data = await response.json();
console.log("Payment response data:", data);
console.log('Payment response data:', data);
// Option 1: Direkt zur Stripe URL weiterleiten
if (data.url) {
console.log("Redirecting to checkout URL:", data.url);
// Kurze Verzögerung für UX
setTimeout(() => {
window.location.href = data.url;
}, 500);
return; // Wichtig: Return hier, damit der Rest nicht ausgeführt wird
}
// Option 1: Direkt zur Stripe URL weiterleiten
if (data.url) {
console.log('Redirecting to checkout URL:', data.url);
// Kurze Verzögerung für UX
setTimeout(() => {
window.location.href = data.url;
}, 500);
return; // Wichtig: Return hier, damit der Rest nicht ausgeführt wird
}
// Wenn keine URL vorhanden ist, versuchen wir es mit redirectToCheckout
if (data.sessionId || data.id) {
console.log(
"Using redirectToCheckout with session ID:",
data.sessionId || data.id
);
// Wenn keine URL vorhanden ist, versuchen wir es mit redirectToCheckout
if (data.sessionId || data.id) {
console.log('Using redirectToCheckout with session ID:', data.sessionId || data.id);
// Initialisiere Stripe
const stripe = await stripePromise;
if (!stripe) {
throw new Error("Stripe konnte nicht initialisiert werden");
}
// Initialisiere Stripe
const stripe = await stripePromise;
if (!stripe) {
throw new Error('Stripe konnte nicht initialisiert werden');
}
const { error } = await stripe.redirectToCheckout({
sessionId: data.sessionId || data.id,
});
const { error } = await stripe.redirectToCheckout({
sessionId: data.sessionId || data.id,
});
if (error) {
console.error("Checkout redirect error:", error);
throw new Error(error.message);
}
} else {
throw new Error("Keine Checkout-URL oder Session-ID erhalten");
}
if (error) {
console.error('Checkout redirect error:', error);
throw new Error(error.message);
}
} else {
throw new Error('Keine Checkout-URL oder Session-ID erhalten');
}
// Erfolgreiche Zahlung
paymentMessage.textContent = `Zahlung über ${amount}€ ${isRecurring ? "(regelmäßig)" : "(einmalig)"} erfolgreich!`;
paymentMessage.classList.add("success");
paymentMessage.classList.remove("error");
} catch (error: unknown) {
console.error("Zahlungsfehler:", error);
const errorMessage =
error instanceof Error ? error.message : "Unbekannter Fehler";
paymentMessage.textContent = `Fehler: ${errorMessage}`;
paymentMessage.classList.add("error");
paymentMessage.classList.remove("success");
// Erfolgreiche Zahlung
paymentMessage.textContent = `Zahlung über ${amount}€ ${isRecurring ? '(regelmäßig)' : '(einmalig)'} erfolgreich!`;
paymentMessage.classList.add('success');
paymentMessage.classList.remove('error');
} catch (error: unknown) {
console.error('Zahlungsfehler:', error);
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
paymentMessage.textContent = `Fehler: ${errorMessage}`;
paymentMessage.classList.add('error');
paymentMessage.classList.remove('success');
// Plausible Event für Zahlungsfehler
trackEvent('payment-error', {
provider: "stripe",
error: errorMessage
});
}
});
// Plausible Event für Zahlungsfehler
trackEvent('payment-error', {
provider: 'stripe',
error: errorMessage,
});
}
});
// PayPal payment processing wird später implementiert
/*
// PayPal payment processing wird später implementiert
/*
paypalBtn?.addEventListener("click", async () => {
const amount = selectedAmount;
const isRecurring = recurringBtn?.classList.contains("active");
@ -619,5 +602,5 @@ const t = useTranslations(lang);
}
});
*/
});
});
</script>

View file

@ -3,7 +3,7 @@ import { getLangFromUrl, useTranslations } from '../utils/i18n';
import type { CollectionEntry } from 'astro:content';
interface Props {
project: CollectionEntry<'projects'>;
project: CollectionEntry<'projects'>;
}
const { project } = Astro.props;
@ -13,11 +13,14 @@ const lang = getLangFromUrl(Astro.url);
const t = useTranslations(lang);
// Format date based on language
const formattedDate = new Intl.DateTimeFormat(lang === 'de' ? 'de-DE' : lang === 'it' ? 'it-IT' : 'en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(data.pubDate);
const formattedDate = new Intl.DateTimeFormat(
lang === 'de' ? 'de-DE' : lang === 'it' ? 'it-IT' : 'en-US',
{
year: 'numeric',
month: 'long',
day: 'numeric',
}
).format(data.pubDate);
// Generate project URL with language prefix
let projectUrl;
@ -31,182 +34,193 @@ const statusKey = `projects.status.${data.status}` as const;
---
<a href={projectUrl} class="project-card-link">
<article class:list={["project-card", { "featured": data.featured }]}>
{data.image && (
<div class="project-image-container">
<img src={data.image} alt={data.title} class="project-image" />
</div>
)}
<div class="content">
<div class="meta">
<span class="category">{t(categoryKey)}</span>
<span class="status" class:list={[data.status]}>{t(statusKey)}</span>
</div>
<h3>{data.title}</h3>
<p>{data.description}</p>
{data.technologies && data.technologies.length > 0 && (
<div class="technologies">
{data.technologies.map(tech => (
<span class="tech">{tech}</span>
))}
</div>
)}
<div class="footer">
<span class="author">{data.author}</span>
<span class="read-more">{t('projects.readMore')} <span class="arrow">→</span></span>
</div>
</div>
</article>
<article class:list={['project-card', { featured: data.featured }]}>
{
data.image && (
<div class="project-image-container">
<img src={data.image} alt={data.title} class="project-image" />
</div>
)
}
<div class="content">
<div class="meta">
<span class="category">{t(categoryKey)}</span>
<span class="status" class:list={[data.status]}>{t(statusKey)}</span>
</div>
<h3>{data.title}</h3>
<p>{data.description}</p>
{
data.technologies && data.technologies.length > 0 && (
<div class="technologies">
{data.technologies.map((tech) => (
<span class="tech">{tech}</span>
))}
</div>
)
}
<div class="footer">
<span class="author">{data.author}</span>
<span class="read-more">{t('projects.readMore')} <span class="arrow">→</span></span>
</div>
</div>
</article>
</a>
<style>
.project-card-link {
display: block;
text-decoration: none;
color: inherit;
height: 100%;
}
.project-card {
display: flex;
flex-direction: column;
border-radius: 0.75rem;
overflow: hidden;
background-color: var(--card-bg);
border: 2px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.06), 0 1px 2px -1px rgba(0, 0, 0, 0.03);
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
height: 100%;
}
.project-card-link:hover .project-card {
transform: translateY(-2px);
border-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.5);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 2px 4px -2px rgba(0, 0, 0, 0.04);
}
.project-card.featured {
border: 2px solid rgba(var(--accent-color-rgb, 249, 115, 22), 0.3);
}
.project-image-container {
width: 100%;
padding-top: 56.25%; /* Aspect ratio 16:9 */
position: relative;
overflow: hidden;
}
.project-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
.content {
display: flex;
flex-direction: column;
padding: 1.5rem;
flex-grow: 1;
}
.meta {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
margin-bottom: 0.75rem;
}
.category {
color: var(--accent-color);
font-weight: 600;
}
.status {
padding: 0.125rem 0.5rem;
border-radius: 1rem;
font-size: 0.75rem;
font-weight: 500;
}
.status.active {
background-color: rgba(16, 185, 129, 0.2);
color: rgb(16, 185, 129);
}
.status.completed {
background-color: rgba(79, 70, 229, 0.2);
color: rgb(79, 70, 229);
}
.status.archived {
background-color: rgba(107, 114, 128, 0.2);
color: rgb(107, 114, 128);
}
h3 {
margin: 0 0 0.75rem 0;
font-size: 1.25rem;
line-height: 1.4;
}
p {
margin: 0;
color: var(--text-muted);
line-height: 1.6;
flex-grow: 1;
margin-bottom: 1.5rem;
}
.technologies {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.tech {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
background-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.1);
color: var(--accent-color);
border-radius: 0.25rem;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.author {
font-size: 0.875rem;
color: var(--text-muted);
}
.read-more {
display: inline-flex;
align-items: center;
color: var(--accent-color);
font-weight: 600;
font-size: 0.875rem;
}
.arrow {
display: inline-block;
margin-left: 4px;
transition: transform 0.2s ease;
}
.project-card-link:hover .arrow {
transform: translateX(4px);
}
</style>
.project-card-link {
display: block;
text-decoration: none;
color: inherit;
height: 100%;
}
.project-card {
display: flex;
flex-direction: column;
border-radius: 0.75rem;
overflow: hidden;
background-color: var(--card-bg);
border: 2px solid rgba(255, 255, 255, 0.3);
box-shadow:
0 2px 4px -1px rgba(0, 0, 0, 0.06),
0 1px 2px -1px rgba(0, 0, 0, 0.03);
transition:
transform 0.2s ease,
border-color 0.2s ease,
box-shadow 0.2s ease;
height: 100%;
}
.project-card-link:hover .project-card {
transform: translateY(-2px);
border-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.5);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.08),
0 2px 4px -2px rgba(0, 0, 0, 0.04);
}
.project-card.featured {
border: 2px solid rgba(var(--accent-color-rgb, 249, 115, 22), 0.3);
}
.project-image-container {
width: 100%;
padding-top: 56.25%; /* Aspect ratio 16:9 */
position: relative;
overflow: hidden;
}
.project-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
.content {
display: flex;
flex-direction: column;
padding: 1.5rem;
flex-grow: 1;
}
.meta {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
margin-bottom: 0.75rem;
}
.category {
color: var(--accent-color);
font-weight: 600;
}
.status {
padding: 0.125rem 0.5rem;
border-radius: 1rem;
font-size: 0.75rem;
font-weight: 500;
}
.status.active {
background-color: rgba(16, 185, 129, 0.2);
color: rgb(16, 185, 129);
}
.status.completed {
background-color: rgba(79, 70, 229, 0.2);
color: rgb(79, 70, 229);
}
.status.archived {
background-color: rgba(107, 114, 128, 0.2);
color: rgb(107, 114, 128);
}
h3 {
margin: 0 0 0.75rem 0;
font-size: 1.25rem;
line-height: 1.4;
}
p {
margin: 0;
color: var(--text-muted);
line-height: 1.6;
flex-grow: 1;
margin-bottom: 1.5rem;
}
.technologies {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.tech {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
background-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.1);
color: var(--accent-color);
border-radius: 0.25rem;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.author {
font-size: 0.875rem;
color: var(--text-muted);
}
.read-more {
display: inline-flex;
align-items: center;
color: var(--accent-color);
font-weight: 600;
font-size: 0.875rem;
}
.arrow {
display: inline-block;
margin-left: 4px;
transition: transform 0.2s ease;
}
.project-card-link:hover .arrow {
transform: translateX(4px);
}
</style>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -4,9 +4,9 @@ import { getCollection } from 'astro:content';
import type { CollectionEntry } from 'astro:content';
interface Props {
maxMembers?: number;
showLinks?: boolean;
compact?: boolean;
maxMembers?: number;
showLinks?: boolean;
compact?: boolean;
}
const { maxMembers = 5, showLinks = true, compact = false } = Astro.props;
@ -17,187 +17,267 @@ const t = useTranslations(lang);
// Get members in current language
const allMembers = await getCollection('members', ({ id }) => {
return id.startsWith(lang + '/');
return id.startsWith(lang + '/');
});
// Sort by order, then by name
const sortedMembers = allMembers
.sort((a, b) => a.data.order - b.data.order || a.data.name.localeCompare(b.data.name))
.filter((member, index) => index < maxMembers);
.sort((a, b) => a.data.order - b.data.order || a.data.name.localeCompare(b.data.name))
.filter((member, index) => index < maxMembers);
---
<div class:list={["team-members", { compact }]}>
{sortedMembers.map((member) => (
<div class="member-card">
<div class="member-avatar">
{member.data.image ? (
<img src={member.data.image} alt={member.data.name} />
) : (
<div class="avatar-placeholder">{member.data.name.charAt(0)}</div>
)}
</div>
<div class="member-info">
<h3 class="member-name">{member.data.name}</h3>
<p class="member-role">{member.data.role}</p>
{!compact && <p class="member-bio">{member.data.bio}</p>}
{showLinks && (
<div class="member-links">
{member.data.github && (
<a href={`https://github.com/${member.data.github}`} target="_blank" rel="noopener noreferrer" class="social-link github" title="GitHub">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></svg>
</a>
)}
{member.data.twitter && (
<a href={`https://twitter.com/${member.data.twitter}`} target="_blank" rel="noopener noreferrer" class="social-link twitter" title="Twitter">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"></path></svg>
</a>
)}
{member.data.linkedin && (
<a href={`https://linkedin.com/in/${member.data.linkedin}`} target="_blank" rel="noopener noreferrer" class="social-link linkedin" title="LinkedIn">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path><rect x="2" y="9" width="4" height="12"></rect><circle cx="4" cy="4" r="2"></circle></svg>
</a>
)}
{member.data.website && (
<a href={member.data.website} target="_blank" rel="noopener noreferrer" class="social-link website" title="Website">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>
</a>
)}
</div>
)}
</div>
</div>
))}
<div class:list={['team-members', { compact }]}>
{
sortedMembers.map((member) => (
<div class="member-card">
<div class="member-avatar">
{member.data.image ? (
<img src={member.data.image} alt={member.data.name} />
) : (
<div class="avatar-placeholder">{member.data.name.charAt(0)}</div>
)}
</div>
<div class="member-info">
<h3 class="member-name">{member.data.name}</h3>
<p class="member-role">{member.data.role}</p>
{!compact && <p class="member-bio">{member.data.bio}</p>}
{showLinks && (
<div class="member-links">
{member.data.github && (
<a
href={`https://github.com/${member.data.github}`}
target="_blank"
rel="noopener noreferrer"
class="social-link github"
title="GitHub"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22" />
</svg>
</a>
)}
{member.data.twitter && (
<a
href={`https://twitter.com/${member.data.twitter}`}
target="_blank"
rel="noopener noreferrer"
class="social-link twitter"
title="Twitter"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z" />
</svg>
</a>
)}
{member.data.linkedin && (
<a
href={`https://linkedin.com/in/${member.data.linkedin}`}
target="_blank"
rel="noopener noreferrer"
class="social-link linkedin"
title="LinkedIn"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z" />
<rect x="2" y="9" width="4" height="12" />
<circle cx="4" cy="4" r="2" />
</svg>
</a>
)}
{member.data.website && (
<a
href={member.data.website}
target="_blank"
rel="noopener noreferrer"
class="social-link website"
title="Website"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10" />
<line x1="2" y1="12" x2="22" y2="12" />
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
</svg>
</a>
)}
</div>
)}
</div>
</div>
))
}
</div>
<style>
.team-members {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 2rem;
margin: 2rem 0;
}
.team-members.compact {
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1rem;
}
.member-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 1.5rem;
border-radius: 0.75rem;
background-color: var(--card-bg);
border: 2px solid rgba(255, 255, 255, 0.1);
transition: transform 0.2s ease, border-color 0.2s ease;
}
.member-card:hover {
transform: translateY(-2px);
border-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.2);
}
.member-avatar {
width: 90px;
height: 90px;
border-radius: 50%;
overflow: hidden;
margin-bottom: 1rem;
background-color: rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
}
.compact .member-avatar {
width: 70px;
height: 70px;
}
.member-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 2.5rem;
font-weight: 700;
color: var(--accent-color);
background-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.1);
}
.member-info {
text-align: center;
width: 100%;
}
.member-name {
font-size: 1.1rem;
font-weight: 600;
margin: 0.5rem 0 0.25rem;
}
.compact .member-name {
font-size: 1rem;
}
.member-role {
font-size: 0.9rem;
color: var(--accent-color);
margin: 0 0 0.75rem;
font-weight: 500;
}
.compact .member-role {
font-size: 0.8rem;
margin-bottom: 0.5rem;
}
.member-bio {
font-size: 0.9rem;
line-height: 1.6;
color: var(--text-muted);
margin-bottom: 1rem;
}
.member-links {
display: flex;
justify-content: center;
gap: 0.75rem;
margin-top: 0.75rem;
}
.social-link {
color: var(--text-muted);
transition: color 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.social-link:hover {
color: var(--accent-color);
}
@media (max-width: 768px) {
.team-members {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
.team-members.compact {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
}
</style>
.team-members {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 2rem;
margin: 2rem 0;
}
.team-members.compact {
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1rem;
}
.member-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 1.5rem;
border-radius: 0.75rem;
background-color: var(--card-bg);
border: 2px solid rgba(255, 255, 255, 0.1);
transition:
transform 0.2s ease,
border-color 0.2s ease;
}
.member-card:hover {
transform: translateY(-2px);
border-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.2);
}
.member-avatar {
width: 90px;
height: 90px;
border-radius: 50%;
overflow: hidden;
margin-bottom: 1rem;
background-color: rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
}
.compact .member-avatar {
width: 70px;
height: 70px;
}
.member-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 2.5rem;
font-weight: 700;
color: var(--accent-color);
background-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.1);
}
.member-info {
text-align: center;
width: 100%;
}
.member-name {
font-size: 1.1rem;
font-weight: 600;
margin: 0.5rem 0 0.25rem;
}
.compact .member-name {
font-size: 1rem;
}
.member-role {
font-size: 0.9rem;
color: var(--accent-color);
margin: 0 0 0.75rem;
font-weight: 500;
}
.compact .member-role {
font-size: 0.8rem;
margin-bottom: 0.5rem;
}
.member-bio {
font-size: 0.9rem;
line-height: 1.6;
color: var(--text-muted);
margin-bottom: 1rem;
}
.member-links {
display: flex;
justify-content: center;
gap: 0.75rem;
margin-top: 0.75rem;
}
.social-link {
color: var(--text-muted);
transition: color 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.social-link:hover {
color: var(--accent-color);
}
@media (max-width: 768px) {
.team-members {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
.team-members.compact {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
}
</style>

View file

@ -6,86 +6,94 @@ const t = useTranslations(lang);
---
<button id="themeToggle" aria-label={t('nav.darkMode')}>
<svg width="20" height="20" viewBox="0 0 24 24">
<path
class="sun"
fill-rule="evenodd"
d="M12 17.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zm0 1.5a7 7 0 1 0 0-14 7 7 0 0 0 0 14zm12-7a.8.8 0 0 1-.8.8h-2.4a.8.8 0 0 1 0-1.6h2.4a.8.8 0 0 1 .8.8zM4 12a.8.8 0 0 1-.8.8H.8a.8.8 0 0 1 0-1.6h2.5a.8.8 0 0 1 .8.8zm16.5-8.5a.8.8 0 0 1 0 1l-1.8 1.8a.8.8 0 0 1-1-1l1.7-1.8a.8.8 0 0 1 1.1 0zM6.3 17.7a.8.8 0 0 1 0 1l-1.7 1.8a.8.8 0 1 1-1.1-1l1.7-1.8a.8.8 0 0 1 1.1 0zM12 0a.8.8 0 0 1 .8.8v2.5a.8.8 0 0 1-1.6 0V.8A.8.8 0 0 1 12 0zm0 20a.8.8 0 0 1 .8.8v2.4a.8.8 0 0 1-1.6 0v-2.4a.8.8 0 0 1 .8-.8zM3.5 3.5a.8.8 0 0 1 1 0l1.8 1.8a.8.8 0 1 1-1 1L3.5 4.6a.8.8 0 0 1 0-1.1zm14.2 14.2a.8.8 0 0 1 1 0l1.8 1.7a.8.8 0 0 1-1 1l-1.8-1.7a.8.8 0 0 1 0-1z"
></path>
<path
class="moon"
fill-rule="evenodd"
d="M16.5 6A10.5 10.5 0 0 1 4.7 16.4 8.5 8.5 0 1 0 16.4 4.7l.1 1.3zm-1.7-2a9 9 0 0 1 .2 2 9 9 0 0 1-11 8.8 9.4 9.4 0 0 1-.8-.3c-.4 0-.8.3-.7.7a10 10 0 0 0 .3.8 10 10 0 0 0 9.2 6 10 10 0 0 0 4-19.2 9.7 9.7 0 0 0-.9-.3c-.3-.1-.7.3-.6.7a9 9 0 0 1 .3.8z"
></path>
</svg>
<svg width="20" height="20" viewBox="0 0 24 24">
<path
class="sun"
fill-rule="evenodd"
d="M12 17.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zm0 1.5a7 7 0 1 0 0-14 7 7 0 0 0 0 14zm12-7a.8.8 0 0 1-.8.8h-2.4a.8.8 0 0 1 0-1.6h2.4a.8.8 0 0 1 .8.8zM4 12a.8.8 0 0 1-.8.8H.8a.8.8 0 0 1 0-1.6h2.5a.8.8 0 0 1 .8.8zm16.5-8.5a.8.8 0 0 1 0 1l-1.8 1.8a.8.8 0 0 1-1-1l1.7-1.8a.8.8 0 0 1 1.1 0zM6.3 17.7a.8.8 0 0 1 0 1l-1.7 1.8a.8.8 0 1 1-1.1-1l1.7-1.8a.8.8 0 0 1 1.1 0zM12 0a.8.8 0 0 1 .8.8v2.5a.8.8 0 0 1-1.6 0V.8A.8.8 0 0 1 12 0zm0 20a.8.8 0 0 1 .8.8v2.4a.8.8 0 0 1-1.6 0v-2.4a.8.8 0 0 1 .8-.8zM3.5 3.5a.8.8 0 0 1 1 0l1.8 1.8a.8.8 0 1 1-1 1L3.5 4.6a.8.8 0 0 1 0-1.1zm14.2 14.2a.8.8 0 0 1 1 0l1.8 1.7a.8.8 0 0 1-1 1l-1.8-1.7a.8.8 0 0 1 0-1z"
></path>
<path
class="moon"
fill-rule="evenodd"
d="M16.5 6A10.5 10.5 0 0 1 4.7 16.4 8.5 8.5 0 1 0 16.4 4.7l.1 1.3zm-1.7-2a9 9 0 0 1 .2 2 9 9 0 0 1-11 8.8 9.4 9.4 0 0 1-.8-.3c-.4 0-.8.3-.7.7a10 10 0 0 0 .3.8 10 10 0 0 0 9.2 6 10 10 0 0 0 4-19.2 9.7 9.7 0 0 0-.9-.3c-.3-.1-.7.3-.6.7a9 9 0 0 1 .3.8z"
></path>
</svg>
</button>
<style>
#themeToggle {
border: 0;
background: none;
cursor: pointer;
padding: 0.5rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
z-index: 50;
position: relative;
}
#themeToggle:hover {
background-color: var(--hover-bg);
}
#themeToggle {
border: 0;
background: none;
cursor: pointer;
padding: 0.5rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
z-index: 50;
position: relative;
}
.sun { fill: var(--text-color); }
.moon { fill: transparent; }
:global(.dark) .sun { fill: transparent; }
:global(.dark) .moon { fill: var(--text-color); }
@media (max-width: 768px) {
#themeToggle {
padding: 0.75rem;
width: 100%;
justify-content: flex-start;
}
#themeToggle::after {
content: attr(aria-label);
margin-left: 0.75rem;
font-size: 1rem;
color: var(--text-color);
}
}
#themeToggle:hover {
background-color: var(--hover-bg);
}
.sun {
fill: var(--text-color);
}
.moon {
fill: transparent;
}
:global(.dark) .sun {
fill: transparent;
}
:global(.dark) .moon {
fill: var(--text-color);
}
@media (max-width: 768px) {
#themeToggle {
padding: 0.75rem;
width: 100%;
justify-content: flex-start;
}
#themeToggle::after {
content: attr(aria-label);
margin-left: 0.75rem;
font-size: 1rem;
color: var(--text-color);
}
}
</style>
<script>
const theme = (() => {
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
return localStorage.getItem('theme');
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
})();
if (theme === 'light') {
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
}
const theme = (() => {
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
return localStorage.getItem('theme');
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
})();
window.localStorage.setItem('theme', theme);
if (theme === 'light') {
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
}
const handleToggleClick = () => {
const element = document.documentElement;
element.classList.toggle('dark');
const isDark = element.classList.contains('dark');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
};
window.localStorage.setItem('theme', theme);
document.getElementById('themeToggle').addEventListener('click', handleToggleClick);
</script>
const handleToggleClick = () => {
const element = document.documentElement;
element.classList.toggle('dark');
const isDark = element.classList.contains('dark');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
};
document.getElementById('themeToggle').addEventListener('click', handleToggleClick);
</script>

View file

@ -1,10 +1,10 @@
---
import type { CollectionEntry } from "astro:content";
import { getLangFromUrl, useTranslations } from "../utils/i18n";
import type { CollectionEntry } from 'astro:content';
import { getLangFromUrl, useTranslations } from '../utils/i18n';
interface Props {
tool: CollectionEntry<"tools">;
isFeatured?: boolean;
tool: CollectionEntry<'tools'>;
isFeatured?: boolean;
}
const { tool, isFeatured = false } = Astro.props;
@ -13,7 +13,7 @@ const lang = getLangFromUrl(Astro.url);
const t = useTranslations(lang);
// Generate URL based on lang and only the filename part of the slug
const slugParts = slug.split("/");
const slugParts = slug.split('/');
const fileName = slugParts[slugParts.length - 1];
const url = `/${lang}/tools/${fileName}`;
@ -22,138 +22,141 @@ const categoryKey = `tools.categories.${data.category}` as const;
const pricingKey = `tools.pricing.${data.pricing}` as const;
// Default image if none is provided
const imageSrc = data.image ?? "/images/bauntown-codespilling.png";
const imageSrc = data.image ?? '/images/bauntown-codespilling.png';
---
<article class={`tool-card ${isFeatured ? "featured" : ""}`}>
<a href={url} class="card-link">
<div class="card-image-container">
<img src={imageSrc} alt={data.title} class="card-image" loading="lazy" />
</div>
<div class="card-content">
<div class="card-meta">
<span class="card-category">{t(categoryKey)}</span>
<span class="card-pricing">{t(pricingKey)}</span>
</div>
<h3 class="card-title">{data.title}</h3>
<p class="card-description">{data.description}</p>
</div>
</a>
<article class={`tool-card ${isFeatured ? 'featured' : ''}`}>
<a href={url} class="card-link">
<div class="card-image-container">
<img src={imageSrc} alt={data.title} class="card-image" loading="lazy" />
</div>
<div class="card-content">
<div class="card-meta">
<span class="card-category">{t(categoryKey)}</span>
<span class="card-pricing">{t(pricingKey)}</span>
</div>
<h3 class="card-title">{data.title}</h3>
<p class="card-description">{data.description}</p>
</div>
</a>
</article>
<style>
.tool-card {
border-radius: 0.75rem;
overflow: hidden;
background-color: var(--card-bg);
transition: transform 0.2s ease, box-shadow 0.2s ease;
height: 100%;
display: flex;
flex-direction: column;
border: 1px solid var(--border-color);
}
.tool-card {
border-radius: 0.75rem;
overflow: hidden;
background-color: var(--card-bg);
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
height: 100%;
display: flex;
flex-direction: column;
border: 1px solid var(--border-color);
}
.tool-card:hover {
transform: translateY(-4px);
box-shadow: 0 10px 30px -15px var(--shadow-color);
filter: brightness(1.1);
}
.tool-card:hover {
transform: translateY(-4px);
box-shadow: 0 10px 30px -15px var(--shadow-color);
filter: brightness(1.1);
}
.tool-card.featured {
border: 1px solid var(--accent-color);
}
.tool-card.featured {
border: 1px solid var(--accent-color);
}
.card-link {
display: flex;
flex-direction: column;
height: 100%;
color: var(--text-color);
text-decoration: none;
}
.card-link {
display: flex;
flex-direction: column;
height: 100%;
color: var(--text-color);
text-decoration: none;
}
.card-image-container {
aspect-ratio: 16 / 9;
overflow: hidden;
}
.card-image-container {
aspect-ratio: 16 / 9;
overflow: hidden;
}
.card-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.card-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.card-content {
padding: 1.5rem;
display: flex;
flex-direction: column;
flex-grow: 1;
}
.card-content {
padding: 1.5rem;
display: flex;
flex-direction: column;
flex-grow: 1;
}
.card-meta {
display: flex;
gap: 0.75rem;
margin-bottom: 0.75rem;
flex-wrap: wrap;
}
.card-meta {
display: flex;
gap: 0.75rem;
margin-bottom: 0.75rem;
flex-wrap: wrap;
}
.card-category, .card-pricing {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-weight: 500;
}
.card-category,
.card-pricing {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-weight: 500;
}
.card-category {
background-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.1);
color: var(--accent-color);
}
.card-category {
background-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.1);
color: var(--accent-color);
}
.card-pricing {
background-color: var(--surface-bg);
color: var(--text-muted);
}
.card-pricing {
background-color: var(--surface-bg);
color: var(--text-muted);
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
margin-top: 0.5rem;
margin-bottom: 0rem;
line-height: 1.4;
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
margin-top: 0.5rem;
margin-bottom: 0rem;
line-height: 1.4;
}
.card-description {
font-size: 0.875rem;
color: var(--text-muted);
flex-grow: 1;
line-height: 1.6;
margin-top: 0.25rem;
margin-bottom: 0;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.card-description {
font-size: 0.875rem;
color: var(--text-muted);
flex-grow: 1;
line-height: 1.6;
margin-top: 0.25rem;
margin-bottom: 0;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.card-footer {
margin-top: auto;
display: flex;
align-items: center;
justify-content: space-between;
}
.card-footer {
margin-top: auto;
display: flex;
align-items: center;
justify-content: space-between;
}
.read-more {
color: var(--accent-color);
font-size: 0.875rem;
font-weight: 500;
}
.read-more {
color: var(--accent-color);
font-size: 0.875rem;
font-weight: 500;
}
.tool-card:hover .read-more {
text-decoration: underline;
}
.tool-card:hover .read-more {
text-decoration: underline;
}
@media (max-width: 768px) {
.card-image-container {
aspect-ratio: 3 / 2;
}
}
</style>
@media (max-width: 768px) {
.card-image-container {
aspect-ratio: 3 / 2;
}
}
</style>

View file

@ -3,7 +3,7 @@ import { getLangFromUrl, useTranslations } from '../utils/i18n';
import type { CollectionEntry } from 'astro:content';
interface Props {
tutorial: CollectionEntry<'tutorials'>;
tutorial: CollectionEntry<'tutorials'>;
}
const { tutorial } = Astro.props;
@ -14,9 +14,9 @@ const t = useTranslations(lang);
// Format date based on language
const formattedDate = new Intl.DateTimeFormat(lang === 'de' ? 'de-DE' : 'en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(data.pubDate);
// Generate tutorial URL with language prefix
@ -31,141 +31,150 @@ const categoryKey = `tutorials.categories.${data.category}` as const;
---
<a href={tutorialUrl} class="tutorial-card-link">
<article class:list={["tutorial-card", { "featured": data.featured }]}>
{data.image && (
<div class="tutorial-image-container">
<img src={data.image} alt={data.title} class="tutorial-image" />
</div>
)}
<div class="content">
<div class="meta">
<span class="category">{t(categoryKey)}</span>
<span class="date">{formattedDate}</span>
</div>
<h3>{data.title}</h3>
<p>{data.description}</p>
<div class="footer">
<span class="author">{data.author}</span>
<span class="read-more">{t('tutorials.readMore')} <span class="arrow">→</span></span>
</div>
</div>
</article>
<article class:list={['tutorial-card', { featured: data.featured }]}>
{
data.image && (
<div class="tutorial-image-container">
<img src={data.image} alt={data.title} class="tutorial-image" />
</div>
)
}
<div class="content">
<div class="meta">
<span class="category">{t(categoryKey)}</span>
<span class="date">{formattedDate}</span>
</div>
<h3>{data.title}</h3>
<p>{data.description}</p>
<div class="footer">
<span class="author">{data.author}</span>
<span class="read-more">{t('tutorials.readMore')} <span class="arrow">→</span></span>
</div>
</div>
</article>
</a>
<style>
.tutorial-card-link {
display: block;
text-decoration: none;
color: inherit;
height: 100%;
}
.tutorial-card {
display: flex;
flex-direction: column;
border-radius: 0.75rem;
overflow: hidden;
background-color: var(--card-bg);
border: 2px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.06), 0 1px 2px -1px rgba(0, 0, 0, 0.03);
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
height: 100%;
}
.tutorial-card-link:hover .tutorial-card {
transform: translateY(-2px);
border-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.5);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 2px 4px -2px rgba(0, 0, 0, 0.04);
}
.tutorial-card.featured {
border: 2px solid rgba(var(--accent-color-rgb, 249, 115, 22), 0.3);
}
.tutorial-image-container {
width: 100%;
padding-top: 66.67%; /* Aspect ratio 3:2 */
position: relative;
overflow: hidden;
}
.tutorial-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
.content {
display: flex;
flex-direction: column;
padding: 1.5rem;
flex-grow: 1;
}
.meta {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
margin-bottom: 0.75rem;
}
.category {
color: var(--accent-color);
font-weight: 600;
}
.date {
color: var(--text-muted);
}
h3 {
margin: 0 0 0.75rem 0;
font-size: 1.25rem;
line-height: 1.4;
}
p {
margin: 0;
color: var(--text-muted);
line-height: 1.6;
flex-grow: 1;
margin-bottom: 1.5rem;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.author {
font-size: 0.875rem;
color: var(--text-muted);
}
.read-more {
display: inline-flex;
align-items: center;
color: var(--accent-color);
font-weight: 600;
font-size: 0.875rem;
}
.arrow {
display: inline-block;
margin-left: 4px;
transition: transform 0.2s ease;
}
.tutorial-card-link:hover .arrow {
transform: translateX(4px);
}
</style>
.tutorial-card-link {
display: block;
text-decoration: none;
color: inherit;
height: 100%;
}
.tutorial-card {
display: flex;
flex-direction: column;
border-radius: 0.75rem;
overflow: hidden;
background-color: var(--card-bg);
border: 2px solid rgba(255, 255, 255, 0.3);
box-shadow:
0 2px 4px -1px rgba(0, 0, 0, 0.06),
0 1px 2px -1px rgba(0, 0, 0, 0.03);
transition:
transform 0.2s ease,
border-color 0.2s ease,
box-shadow 0.2s ease;
height: 100%;
}
.tutorial-card-link:hover .tutorial-card {
transform: translateY(-2px);
border-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.5);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.08),
0 2px 4px -2px rgba(0, 0, 0, 0.04);
}
.tutorial-card.featured {
border: 2px solid rgba(var(--accent-color-rgb, 249, 115, 22), 0.3);
}
.tutorial-image-container {
width: 100%;
padding-top: 66.67%; /* Aspect ratio 3:2 */
position: relative;
overflow: hidden;
}
.tutorial-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
.content {
display: flex;
flex-direction: column;
padding: 1.5rem;
flex-grow: 1;
}
.meta {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
margin-bottom: 0.75rem;
}
.category {
color: var(--accent-color);
font-weight: 600;
}
.date {
color: var(--text-muted);
}
h3 {
margin: 0 0 0.75rem 0;
font-size: 1.25rem;
line-height: 1.4;
}
p {
margin: 0;
color: var(--text-muted);
line-height: 1.6;
flex-grow: 1;
margin-bottom: 1.5rem;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.author {
font-size: 0.875rem;
color: var(--text-muted);
}
.read-more {
display: inline-flex;
align-items: center;
color: var(--accent-color);
font-weight: 600;
font-size: 0.875rem;
}
.arrow {
display: inline-block;
margin-left: 4px;
transition: transform 0.2s ease;
}
.tutorial-card-link:hover .arrow {
transform: translateX(4px);
}
</style>

View file

@ -3,7 +3,7 @@ import { getLangFromUrl, useTranslations } from '../utils/i18n';
import type { CollectionEntry } from 'astro:content';
interface Props {
vision: CollectionEntry<'vision'>;
vision: CollectionEntry<'vision'>;
}
const { vision } = Astro.props;
@ -14,9 +14,9 @@ const t = useTranslations(lang);
// Format date based on language
const formattedDate = new Intl.DateTimeFormat(lang === 'de' ? 'de-DE' : 'en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(data.pubDate);
// Generate vision URL with language prefix
@ -32,233 +32,244 @@ const statusKey = `vision.status.${data.status}` as const;
---
<a href={visionUrl} class="vision-card-link">
<article class:list={["vision-card", { "featured": data.featured }]}>
{data.image && (
<div class="vision-image-container">
<img src={data.image} alt={data.title} class="vision-image" />
<div class="status-badge" data-status={data.status}>
{t(statusKey)}
</div>
</div>
)}
<div class="content">
<div class="meta">
<span class="category" data-category={data.category}>{t(categoryKey)}</span>
<span class="date">{formattedDate}</span>
</div>
<h3>{data.title}</h3>
<p>{data.description}</p>
<div class="details">
<div class="timeline">
<span class="timeline-icon">⏱️</span>
<span class="timeline-text">{data.timeline}</span>
</div>
</div>
<div class="footer">
{data.contributors && data.contributors.length > 0 && (
<div class="contributors">
<span class="contributors-count">{data.contributors.length}</span>
</div>
)}
<span class="read-more">{t('vision.readMore')} <span class="arrow">→</span></span>
</div>
</div>
</article>
<article class:list={['vision-card', { featured: data.featured }]}>
{
data.image && (
<div class="vision-image-container">
<img src={data.image} alt={data.title} class="vision-image" />
<div class="status-badge" data-status={data.status}>
{t(statusKey)}
</div>
</div>
)
}
<div class="content">
<div class="meta">
<span class="category" data-category={data.category}>{t(categoryKey)}</span>
<span class="date">{formattedDate}</span>
</div>
<h3>{data.title}</h3>
<p>{data.description}</p>
<div class="details">
<div class="timeline">
<span class="timeline-icon">⏱️</span>
<span class="timeline-text">{data.timeline}</span>
</div>
</div>
<div class="footer">
{
data.contributors && data.contributors.length > 0 && (
<div class="contributors">
<span class="contributors-count">{data.contributors.length}</span>
</div>
)
}
<span class="read-more">{t('vision.readMore')} <span class="arrow">→</span></span>
</div>
</div>
</article>
</a>
<style>
.vision-card-link {
display: block;
text-decoration: none;
color: inherit;
height: 100%;
}
.vision-card {
display: flex;
flex-direction: column;
border-radius: 0.75rem;
overflow: hidden;
background-color: var(--card-bg);
border: 2px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.06), 0 1px 2px -1px rgba(0, 0, 0, 0.03);
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
height: 100%;
}
.vision-card-link:hover .vision-card {
transform: translateY(-2px);
border-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.5);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 2px 4px -2px rgba(0, 0, 0, 0.04);
}
.vision-card.featured {
border: 2px solid rgba(var(--accent-color-rgb, 249, 115, 22), 0.3);
}
.vision-image-container {
width: 100%;
padding-top: 66.67%; /* Aspect ratio 3:2 */
position: relative;
overflow: hidden;
}
.vision-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
.status-badge {
position: absolute;
top: 1rem;
right: 1rem;
padding: 0.35rem 0.75rem;
border-radius: 2rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
z-index: 2;
color: white;
}
.status-badge[data-status="current"] {
background-color: #10b981; /* Green */
}
.status-badge[data-status="planned"] {
background-color: #6366f1; /* Purple */
}
.status-badge[data-status="exploring"] {
background-color: #f59e0b; /* Amber */
}
.content {
display: flex;
flex-direction: column;
padding: 1.5rem;
flex-grow: 1;
}
.meta {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
margin-bottom: 0.75rem;
}
.category {
font-weight: 600;
padding: 0.2rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
}
.category[data-category="product"] {
background-color: #dcfce7; /* Light green */
color: #16a34a;
}
.category[data-category="technology"] {
background-color: #e0f2fe; /* Light blue */
color: #0284c7;
}
.category[data-category="community"] {
background-color: #fef3c7; /* Light yellow */
color: #b45309;
}
.category[data-category="future"] {
background-color: #ede9fe; /* Light purple */
color: #7c3aed;
}
.date {
color: var(--text-muted);
}
h3 {
margin: 0 0 0.75rem 0;
font-size: 1.25rem;
line-height: 1.4;
}
p {
margin: 0;
color: var(--text-muted);
line-height: 1.6;
margin-bottom: 1.5rem;
}
.details {
margin-bottom: 1.5rem;
}
.timeline {
display: flex;
align-items: center;
gap: 0.5rem;
}
.timeline-icon {
font-size: 1rem;
}
.timeline-text {
font-size: 0.875rem;
color: var(--text-color);
font-weight: 500;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.contributors {
display: flex;
align-items: center;
}
.contributors-count {
font-size: 0.875rem;
color: var(--text-muted);
display: flex;
align-items: center;
}
.contributors-count::before {
content: '👥 ';
margin-right: 0.25rem;
}
.read-more {
display: inline-flex;
align-items: center;
color: var(--accent-color);
font-weight: 600;
font-size: 0.875rem;
}
.arrow {
display: inline-block;
margin-left: 4px;
transition: transform 0.2s ease;
}
.vision-card-link:hover .arrow {
transform: translateX(4px);
}
</style>
.vision-card-link {
display: block;
text-decoration: none;
color: inherit;
height: 100%;
}
.vision-card {
display: flex;
flex-direction: column;
border-radius: 0.75rem;
overflow: hidden;
background-color: var(--card-bg);
border: 2px solid rgba(255, 255, 255, 0.3);
box-shadow:
0 2px 4px -1px rgba(0, 0, 0, 0.06),
0 1px 2px -1px rgba(0, 0, 0, 0.03);
transition:
transform 0.2s ease,
border-color 0.2s ease,
box-shadow 0.2s ease;
height: 100%;
}
.vision-card-link:hover .vision-card {
transform: translateY(-2px);
border-color: rgba(var(--accent-color-rgb, 249, 115, 22), 0.5);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.08),
0 2px 4px -2px rgba(0, 0, 0, 0.04);
}
.vision-card.featured {
border: 2px solid rgba(var(--accent-color-rgb, 249, 115, 22), 0.3);
}
.vision-image-container {
width: 100%;
padding-top: 66.67%; /* Aspect ratio 3:2 */
position: relative;
overflow: hidden;
}
.vision-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
.status-badge {
position: absolute;
top: 1rem;
right: 1rem;
padding: 0.35rem 0.75rem;
border-radius: 2rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
z-index: 2;
color: white;
}
.status-badge[data-status='current'] {
background-color: #10b981; /* Green */
}
.status-badge[data-status='planned'] {
background-color: #6366f1; /* Purple */
}
.status-badge[data-status='exploring'] {
background-color: #f59e0b; /* Amber */
}
.content {
display: flex;
flex-direction: column;
padding: 1.5rem;
flex-grow: 1;
}
.meta {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
margin-bottom: 0.75rem;
}
.category {
font-weight: 600;
padding: 0.2rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
}
.category[data-category='product'] {
background-color: #dcfce7; /* Light green */
color: #16a34a;
}
.category[data-category='technology'] {
background-color: #e0f2fe; /* Light blue */
color: #0284c7;
}
.category[data-category='community'] {
background-color: #fef3c7; /* Light yellow */
color: #b45309;
}
.category[data-category='future'] {
background-color: #ede9fe; /* Light purple */
color: #7c3aed;
}
.date {
color: var(--text-muted);
}
h3 {
margin: 0 0 0.75rem 0;
font-size: 1.25rem;
line-height: 1.4;
}
p {
margin: 0;
color: var(--text-muted);
line-height: 1.6;
margin-bottom: 1.5rem;
}
.details {
margin-bottom: 1.5rem;
}
.timeline {
display: flex;
align-items: center;
gap: 0.5rem;
}
.timeline-icon {
font-size: 1rem;
}
.timeline-text {
font-size: 0.875rem;
color: var(--text-color);
font-weight: 500;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.contributors {
display: flex;
align-items: center;
}
.contributors-count {
font-size: 0.875rem;
color: var(--text-muted);
display: flex;
align-items: center;
}
.contributors-count::before {
content: '👥 ';
margin-right: 0.25rem;
}
.read-more {
display: inline-flex;
align-items: center;
color: var(--accent-color);
font-weight: 600;
font-size: 0.875rem;
}
.arrow {
display: inline-block;
margin-left: 4px;
transition: transform 0.2s ease;
}
.vision-card-link:hover .arrow {
transform: translateX(4px);
}
</style>

View file

@ -1,40 +1,41 @@
---
interface Props {
videoId: string;
title?: string;
aspectRatio?: string;
videoId: string;
title?: string;
aspectRatio?: string;
}
const { videoId, title = "YouTube video", aspectRatio = "16/9" } = Astro.props;
const { videoId, title = 'YouTube video', aspectRatio = '16/9' } = Astro.props;
---
<div class="youtube-video-container">
<iframe
src={`https://www.youtube.com/embed/${videoId}`}
title={title}
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen="true"
></iframe>
<iframe
src={`https://www.youtube.com/embed/${videoId}`}
title={title}
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen="true"></iframe>
</div>
<style define:vars={{ aspectRatio }}>
.youtube-video-container {
position: relative;
width: 100%;
aspect-ratio: var(--aspectRatio);
margin: 2rem 0;
border-radius: 0.75rem;
overflow: hidden;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.youtube-video-container {
position: relative;
width: 100%;
aspect-ratio: var(--aspectRatio);
margin: 2rem 0;
border-radius: 0.75rem;
overflow: hidden;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 0;
}
</style>
iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 0;
}
</style>

View file

@ -1,181 +1,199 @@
import { defineCollection, z } from 'astro:content';
const toolsCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
category: z.enum(['Design', 'Development', 'Productivity', 'Hosting']),
image: z.string().optional(),
author: z.string().default('BaunTown'),
featured: z.boolean().default(false),
website: z.string().optional(),
pricing: z.enum(['Free', 'Freemium', 'Paid']).default('Freemium'),
tags: z.array(z.string()).optional(),
externalLinks: z.array(z.object({
title: z.string(),
url: z.string(),
})).optional(),
}),
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
category: z.enum(['Design', 'Development', 'Productivity', 'Hosting']),
image: z.string().optional(),
author: z.string().default('BaunTown'),
featured: z.boolean().default(false),
website: z.string().optional(),
pricing: z.enum(['Free', 'Freemium', 'Paid']).default('Freemium'),
tags: z.array(z.string()).optional(),
externalLinks: z
.array(
z.object({
title: z.string(),
url: z.string(),
})
)
.optional(),
}),
});
const newsCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
category: z.enum(['AI', 'Web', 'Development', 'Design', 'Community', 'Product']),
image: z.string().optional(),
author: z.string().default('BaunTown'),
featured: z.boolean().default(false),
tags: z.array(z.string()).optional(),
externalLinks: z.array(z.object({
title: z.string(),
url: z.string(),
})).optional(),
}),
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
category: z.enum(['AI', 'Web', 'Development', 'Design', 'Community', 'Product']),
image: z.string().optional(),
author: z.string().default('BaunTown'),
featured: z.boolean().default(false),
tags: z.array(z.string()).optional(),
externalLinks: z
.array(
z.object({
title: z.string(),
url: z.string(),
})
)
.optional(),
}),
});
const modelsCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
category: z.enum(['Text', 'Bild']),
image: z.string().optional(),
author: z.string().default('BaunTown'),
featured: z.boolean().default(false),
tags: z.array(z.string()).optional(),
externalLinks: z.array(z.object({
title: z.string(),
url: z.string(),
})).optional(),
}),
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
category: z.enum(['Text', 'Bild']),
image: z.string().optional(),
author: z.string().default('BaunTown'),
featured: z.boolean().default(false),
tags: z.array(z.string()).optional(),
externalLinks: z
.array(
z.object({
title: z.string(),
url: z.string(),
})
)
.optional(),
}),
});
const projectsCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
category: z.enum(['Web', 'Mobile', 'Desktop', 'IoT', 'AI', 'Design']),
image: z.string().optional(),
author: z.string().default('BaunTown'),
featured: z.boolean().default(false),
status: z.enum(['active', 'completed', 'archived']).default('active'),
tags: z.array(z.string()).optional(),
githubUrl: z.string().optional(),
demoUrl: z.string().optional(),
technologies: z.array(z.string()).optional(),
}),
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
category: z.enum(['Web', 'Mobile', 'Desktop', 'IoT', 'AI', 'Design']),
image: z.string().optional(),
author: z.string().default('BaunTown'),
featured: z.boolean().default(false),
status: z.enum(['active', 'completed', 'archived']).default('active'),
tags: z.array(z.string()).optional(),
githubUrl: z.string().optional(),
demoUrl: z.string().optional(),
technologies: z.array(z.string()).optional(),
}),
});
const tutorialsCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
category: z.enum(['UI & UX', 'Business', 'Users', 'Branding', 'Marketing', 'Vibecoding']),
image: z.string().optional(),
author: z.string().default('BaunTown'),
featured: z.boolean().default(false),
// Course related fields
course: z.string().optional(),
courseName: z.string().optional(),
lessonNumber: z.number().optional(),
}),
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
category: z.enum(['UI & UX', 'Business', 'Users', 'Branding', 'Marketing', 'Vibecoding']),
image: z.string().optional(),
author: z.string().default('BaunTown'),
featured: z.boolean().default(false),
// Course related fields
course: z.string().optional(),
courseName: z.string().optional(),
lessonNumber: z.number().optional(),
}),
});
const missionsCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
difficulty: z.enum(['beginner', 'intermediate', 'advanced']),
duration: z.string(), // e.g. "2-3 hours", "1 day"
skills: z.array(z.string()),
image: z.string().optional(),
featured: z.boolean().default(false),
category: z.enum(['UI & UX', 'Business', 'Users', 'Branding', 'Marketing', 'Vibecoding']).default('UI & UX'),
status: z.enum(['active', 'completed', 'upcoming']).default('active'),
participants: z.array(z.string()).optional(),
githubRepo: z.string().optional(),
}),
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
difficulty: z.enum(['beginner', 'intermediate', 'advanced']),
duration: z.string(), // e.g. "2-3 hours", "1 day"
skills: z.array(z.string()),
image: z.string().optional(),
featured: z.boolean().default(false),
category: z
.enum(['UI & UX', 'Business', 'Users', 'Branding', 'Marketing', 'Vibecoding'])
.default('UI & UX'),
status: z.enum(['active', 'completed', 'upcoming']).default('active'),
participants: z.array(z.string()).optional(),
githubRepo: z.string().optional(),
}),
});
const visionCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
image: z.string().optional(),
featured: z.boolean().default(false),
category: z.enum(['product', 'technology', 'community', 'future']),
timeline: z.string(), // e.g. "2025-2030", "Long-term"
status: z.enum(['current', 'planned', 'exploring']),
contributors: z.array(z.string()).optional(),
relatedLinks: z.array(z.object({
title: z.string(),
url: z.string(),
})).optional(),
}),
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
image: z.string().optional(),
featured: z.boolean().default(false),
category: z.enum(['product', 'technology', 'community', 'future']),
timeline: z.string(), // e.g. "2025-2030", "Long-term"
status: z.enum(['current', 'planned', 'exploring']),
contributors: z.array(z.string()).optional(),
relatedLinks: z
.array(
z.object({
title: z.string(),
url: z.string(),
})
)
.optional(),
}),
});
const joinCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
image: z.string().optional(),
heroTitle: z.string().optional(),
heroDescription: z.string().optional(),
newsletterTitle: z.string().optional(),
newsletterDescription: z.string().optional(),
submissionTitle: z.string().optional(),
submissionDescription: z.string().optional(),
}),
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
updatedDate: z.date().optional(),
image: z.string().optional(),
heroTitle: z.string().optional(),
heroDescription: z.string().optional(),
newsletterTitle: z.string().optional(),
newsletterDescription: z.string().optional(),
submissionTitle: z.string().optional(),
submissionDescription: z.string().optional(),
}),
});
const membersCollection = defineCollection({
type: 'content',
schema: z.object({
name: z.string(),
role: z.string(),
bio: z.string(),
image: z.string().optional(),
github: z.string().optional(),
twitter: z.string().optional(),
linkedin: z.string().optional(),
website: z.string().optional(),
featured: z.boolean().default(false),
order: z.number().default(999),
}),
type: 'content',
schema: z.object({
name: z.string(),
role: z.string(),
bio: z.string(),
image: z.string().optional(),
github: z.string().optional(),
twitter: z.string().optional(),
linkedin: z.string().optional(),
website: z.string().optional(),
featured: z.boolean().default(false),
order: z.number().default(999),
}),
});
export const collections = {
'news': newsCollection,
'models': modelsCollection,
'projects': projectsCollection,
'tutorials': tutorialsCollection,
'missions': missionsCollection,
'vision': visionCollection,
'join': joinCollection,
'members': membersCollection,
'tools': toolsCollection,
};
news: newsCollection,
models: modelsCollection,
projects: projectsCollection,
tutorials: tutorialsCollection,
missions: missionsCollection,
vision: visionCollection,
join: joinCollection,
members: membersCollection,
tools: toolsCollection,
};

View file

@ -26,4 +26,4 @@ Als Open-Source-Projekt lebt BaunTown von den Beiträgen der Community. Egal ob
4. Mache deine Änderungen und committe sie
5. Pushe deinen Branch und erstelle einen Pull Request
Wir freuen uns auf deine Beiträge!
Wir freuen uns auf deine Beiträge!

View file

@ -23,4 +23,4 @@ Schau dir die Highlights und Projekte unserer vergangenen Events an und hol dir
### Eigenes Event organisieren
Möchtest du ein lokales BaunTown-Meetup in deiner Stadt organisieren? Wir unterstützen dich gerne dabei! Kontaktiere uns für weitere Informationen und Ressourcen.
Möchtest du ein lokales BaunTown-Meetup in deiner Stadt organisieren? Wir unterstützen dich gerne dabei! Kontaktiere uns für weitere Informationen und Ressourcen.

View file

@ -25,4 +25,4 @@ BaunTown ist ein Projekt, das von Leidenschaft und Community-Engagement angetrie
- **Einmalige Spende**: Auch einmalige Spenden sind willkommen.
- **Unternehmenssponsoring**: Interesse an Unternehmenssponsoring? Kontaktiere uns direkt.
Jeder Beitrag zählt und hilft uns, BaunTown weiterzuentwickeln!
Jeder Beitrag zählt und hilft uns, BaunTown weiterzuentwickeln!

View file

@ -26,4 +26,4 @@ As an open-source project, BaunTown thrives on community contributions. Whether
4. Make your changes and commit them
5. Push your branch and create a pull request
We look forward to your contributions!
We look forward to your contributions!

View file

@ -23,4 +23,4 @@ Check out the highlights and projects from our past events and get inspiration f
### Organize Your Own Event
Would you like to organize a local BaunTown meetup in your city? We're happy to support you! Contact us for more information and resources.
Would you like to organize a local BaunTown meetup in your city? We're happy to support you! Contact us for more information and resources.

View file

@ -25,4 +25,4 @@ BaunTown is a project fueled by passion and community engagement. To advance our
- **One-time Donations**: One-time donations are also welcome.
- **Corporate Sponsorship**: Interested in corporate sponsorship? Contact us directly.
Every contribution counts and helps us continue developing BaunTown!
Every contribution counts and helps us continue developing BaunTown!

View file

@ -26,4 +26,4 @@ Come progetto open-source, BaunTown prospera grazie ai contributi della comunit
4. Fai le tue modifiche e committale
5. Pusha il tuo branch e crea una pull request
Aspettiamo con ansia i tuoi contributi!
Aspettiamo con ansia i tuoi contributi!

View file

@ -23,4 +23,4 @@ Dai un'occhiata ai momenti salienti e ai progetti dei nostri eventi passati e tr
### Organizza il Tuo Evento
Vorresti organizzare un meetup locale di BaunTown nella tua città? Siamo felici di supportarti! Contattaci per maggiori informazioni e risorse.
Vorresti organizzare un meetup locale di BaunTown nella tua città? Siamo felici di supportarti! Contattaci per maggiori informazioni e risorse.

View file

@ -33,4 +33,4 @@ Il tuo feedback ci aiuta a migliorare la piattaforma. Facci sapere cosa ti piace
### Contribuisci al Codice
BaunTown è un progetto open source. Se hai competenze tecniche, puoi contribuire direttamente al codice o aiutare con la documentazione.
BaunTown è un progetto open source. Se hai competenze tecniche, puoi contribuire direttamente al codice o aiutare con la documentazione.

View file

@ -25,4 +25,4 @@ BaunTown è un progetto alimentato dalla passione e dal coinvolgimento della com
- **Donazioni Una Tantum**: Anche le donazioni una tantum sono benvenute.
- **Sponsorizzazione Aziendale**: Interessato alla sponsorizzazione aziendale? Contattaci direttamente.
Ogni contributo conta e ci aiuta a continuare a sviluppare BaunTown!
Ogni contributo conta e ci aiuta a continuare a sviluppare BaunTown!

View file

@ -1,10 +1,10 @@
---
name: "Nils Weiser"
role: "Backend-Entwickler"
bio: "Spezialist für Serverarchitektur und Datenbanken mit Schwerpunkt auf skalierbaren Lösungen. Nils ist verantwortlich für die robuste technische Infrastruktur von BaunTown."
image: "/images/members/avatar-placeholder.png"
github: "nilsweiser"
linkedin: "nils-weiser"
name: 'Nils Weiser'
role: 'Backend-Entwickler'
bio: 'Spezialist für Serverarchitektur und Datenbanken mit Schwerpunkt auf skalierbaren Lösungen. Nils ist verantwortlich für die robuste technische Infrastruktur von BaunTown.'
image: '/images/members/avatar-placeholder.png'
github: 'nilsweiser'
linkedin: 'nils-weiser'
featured: true
order: 3
---
---

View file

@ -1,11 +1,11 @@
---
name: "Till Schneider"
role: "Gründer & Entwickler"
bio: "Full-Stack Entwickler mit Fokus auf Web-Technologien und KI-Integration. Till hat BaunTown gegründet, um eine Plattform für offenes, kollaboratives Lernen zu schaffen."
image: "/images/members/avatar-placeholder.png"
github: "tillschneider"
linkedin: "tillschneider"
website: "https://bauntown.com"
name: 'Till Schneider'
role: 'Gründer & Entwickler'
bio: 'Full-Stack Entwickler mit Fokus auf Web-Technologien und KI-Integration. Till hat BaunTown gegründet, um eine Plattform für offenes, kollaboratives Lernen zu schaffen.'
image: '/images/members/avatar-placeholder.png'
github: 'tillschneider'
linkedin: 'tillschneider'
website: 'https://bauntown.com'
featured: true
order: 1
---
---

View file

@ -1,11 +1,11 @@
---
name: "Tobias Müller"
role: "Design Lead"
bio: "UI/UX Designer mit Erfahrung in der Gestaltung intuitiver Benutzeroberflächen. Tobias bringt seine Expertise für visuelles Design und Benutzererfahrung in das BaunTown-Team ein."
image: "/images/members/avatar-placeholder.png"
github: "tobiasmdesign"
twitter: "tobiasmdesign"
linkedin: "tobias-mueller-design"
name: 'Tobias Müller'
role: 'Design Lead'
bio: 'UI/UX Designer mit Erfahrung in der Gestaltung intuitiver Benutzeroberflächen. Tobias bringt seine Expertise für visuelles Design und Benutzererfahrung in das BaunTown-Team ein.'
image: '/images/members/avatar-placeholder.png'
github: 'tobiasmdesign'
twitter: 'tobiasmdesign'
linkedin: 'tobias-mueller-design'
featured: true
order: 2
---
---

View file

@ -1,10 +1,10 @@
---
name: "Nils Weiser"
role: "Backend Developer"
name: 'Nils Weiser'
role: 'Backend Developer'
bio: "Specialist in server architecture and databases with a focus on scalable solutions. Nils is responsible for BaunTown's robust technical infrastructure."
image: "/images/members/avatar-placeholder.png"
github: "nilsweiser"
linkedin: "nils-weiser"
image: '/images/members/avatar-placeholder.png'
github: 'nilsweiser'
linkedin: 'nils-weiser'
featured: true
order: 3
---
---

View file

@ -1,11 +1,11 @@
---
name: "Till Schneider"
role: "Founder & Developer"
bio: "Full-stack developer with a focus on web technologies and AI integration. Till founded BaunTown to create a platform for open, collaborative learning."
image: "/images/members/avatar-placeholder.png"
github: "tillschneider"
linkedin: "tillschneider"
website: "https://bauntown.com"
name: 'Till Schneider'
role: 'Founder & Developer'
bio: 'Full-stack developer with a focus on web technologies and AI integration. Till founded BaunTown to create a platform for open, collaborative learning.'
image: '/images/members/avatar-placeholder.png'
github: 'tillschneider'
linkedin: 'tillschneider'
website: 'https://bauntown.com'
featured: true
order: 1
---
---

View file

@ -1,11 +1,11 @@
---
name: "Tobias Müller"
role: "Design Lead"
bio: "UI/UX designer with experience crafting intuitive user interfaces. Tobias brings his expertise in visual design and user experience to the BaunTown team."
image: "/images/members/avatar-placeholder.png"
github: "tobiasmdesign"
twitter: "tobiasmdesign"
linkedin: "tobias-mueller-design"
name: 'Tobias Müller'
role: 'Design Lead'
bio: 'UI/UX designer with experience crafting intuitive user interfaces. Tobias brings his expertise in visual design and user experience to the BaunTown team.'
image: '/images/members/avatar-placeholder.png'
github: 'tobiasmdesign'
twitter: 'tobiasmdesign'
linkedin: 'tobias-mueller-design'
featured: true
order: 2
---
---

View file

@ -1,10 +1,10 @@
---
name: "Nils Weiser"
role: "Sviluppatore Backend"
bio: "Specialista in architettura server e database con focus su soluzioni scalabili. Nils è responsabile della robusta infrastruttura tecnica di BaunTown."
image: "/images/members/avatar-placeholder.png"
github: "nilsweiser"
linkedin: "nils-weiser"
name: 'Nils Weiser'
role: 'Sviluppatore Backend'
bio: 'Specialista in architettura server e database con focus su soluzioni scalabili. Nils è responsabile della robusta infrastruttura tecnica di BaunTown.'
image: '/images/members/avatar-placeholder.png'
github: 'nilsweiser'
linkedin: 'nils-weiser'
featured: true
order: 3
---
---

View file

@ -1,11 +1,11 @@
---
name: "Till Schneider"
role: "Fondatore & Sviluppatore"
bio: "Sviluppatore full-stack con focus su tecnologie web e integrazione AI. Till ha fondato BaunTown per creare una piattaforma di apprendimento aperto e collaborativo."
image: "/images/members/avatar-placeholder.png"
github: "tillschneider"
linkedin: "tillschneider"
website: "https://bauntown.com"
name: 'Till Schneider'
role: 'Fondatore & Sviluppatore'
bio: 'Sviluppatore full-stack con focus su tecnologie web e integrazione AI. Till ha fondato BaunTown per creare una piattaforma di apprendimento aperto e collaborativo.'
image: '/images/members/avatar-placeholder.png'
github: 'tillschneider'
linkedin: 'tillschneider'
website: 'https://bauntown.com'
featured: true
order: 1
---
---

View file

@ -1,11 +1,11 @@
---
name: "Tobias Müller"
role: "Lead Designer"
bio: "Designer UI/UX con esperienza nella creazione di interfacce utente intuitive. Tobias porta la sua competenza in design visivo ed esperienza utente al team di BaunTown."
image: "/images/members/avatar-placeholder.png"
github: "tobiasmdesign"
twitter: "tobiasmdesign"
linkedin: "tobias-mueller-design"
name: 'Tobias Müller'
role: 'Lead Designer'
bio: 'Designer UI/UX con esperienza nella creazione di interfacce utente intuitive. Tobias porta la sua competenza in design visivo ed esperienza utente al team di BaunTown.'
image: '/images/members/avatar-placeholder.png'
github: 'tobiasmdesign'
twitter: 'tobiasmdesign'
linkedin: 'tobias-mueller-design'
featured: true
order: 2
---
---

View file

@ -1,17 +1,17 @@
---
title: "Google Gemini 2.5 Pro: Ein Durchbruch in der KI-Technologie"
description: "Google hat kürzlich das Gemini 2.5 Pro-Modell vorgestellt, das als das fortschrittlichste KI-Modell des Unternehmens gilt und mit seiner enormen Kontextfenstergröße neue Maßstäbe setzt."
title: 'Google Gemini 2.5 Pro: Ein Durchbruch in der KI-Technologie'
description: 'Google hat kürzlich das Gemini 2.5 Pro-Modell vorgestellt, das als das fortschrittlichste KI-Modell des Unternehmens gilt und mit seiner enormen Kontextfenstergröße neue Maßstäbe setzt.'
pubDate: 2025-03-25
category: "Text"
category: 'Text'
featured: true
author: "BaunTown"
tags: ["Google", "Gemini", "KI", "Multimodal", "Kontextfenster"]
image: "/images/models/google-gemini-2.5pro-bauntown.png"
author: 'BaunTown'
tags: ['Google', 'Gemini', 'KI', 'Multimodal', 'Kontextfenster']
image: '/images/models/google-gemini-2.5pro-bauntown.png'
externalLinks:
- title: "Offizielle Google Ankündigung"
url: "https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/#gemini-2-5-thinking"
- title: "Gemini - Ausprobieren"
url: "https://gemini.google.com"
- title: 'Offizielle Google Ankündigung'
url: 'https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/#gemini-2-5-thinking'
- title: 'Gemini - Ausprobieren'
url: 'https://gemini.google.com'
---
Google hat kürzlich das Gemini 2.5 Pro-Modell vorgestellt, das als das fortschrittlichste KI-Modell des Unternehmens gilt. Mit bahnbrechenden Funktionen wie multimodaler Verarbeitung, verbesserter Problemlösungsfähigkeit und einer enormen Kontextfenstergröße von bis zu 2 Millionen Tokens setzt dieses Modell neue Maßstäbe in der KI-Entwicklung. Die Reaktionen auf diese Innovation sind weltweit vielfältig und spiegeln sowohl Begeisterung als auch kritische Reflexion wider.

View file

@ -1,19 +1,19 @@
---
title: "Revolution der KI-Bildgenerierung: OpenAIs GPT-4o setzt neue Maßstäbe"
description: "OpenAI hat kürzlich sein neuestes Modell, GPT-4o, vorgestellt, das die Bildgenerierung direkt integriert und damit einen bedeutenden Fortschritt in der KI-Technologie markiert."
title: 'Revolution der KI-Bildgenerierung: OpenAIs GPT-4o setzt neue Maßstäbe'
description: 'OpenAI hat kürzlich sein neuestes Modell, GPT-4o, vorgestellt, das die Bildgenerierung direkt integriert und damit einen bedeutenden Fortschritt in der KI-Technologie markiert.'
pubDate: 2025-03-25
category: "Bild"
category: 'Bild'
featured: true
author: "BaunTown"
tags: ["GPT-4o", "OpenAI", "KI", "Multimodal", "Bildgenerierung"]
image: "/images/models/openai-gpt4o-imagemode-bauntown.png"
author: 'BaunTown'
tags: ['GPT-4o', 'OpenAI', 'KI', 'Multimodal', 'Bildgenerierung']
image: '/images/models/openai-gpt4o-imagemode-bauntown.png'
externalLinks:
- title: "Offizielle OpenAI Ankündigung"
url: "https://openai.com/index/introducing-4o-image-generation/"
- title: "ChatGPT - Bilder erstellen"
url: "https://chat.openai.com"
- title: 'Offizielle OpenAI Ankündigung'
url: 'https://openai.com/index/introducing-4o-image-generation/'
- title: 'ChatGPT - Bilder erstellen'
url: 'https://chat.openai.com'
- title: "Sora - OpenAI's Video & Bild Plattform"
url: "https://sora.com"
url: 'https://sora.com'
---
OpenAI hat kürzlich sein neuestes Modell, GPT-4o, vorgestellt, das die Bildgenerierung direkt integriert und damit einen bedeutenden Fortschritt in der KI-Technologie markiert. Dieses Modell kann Bilder basierend auf Textaufforderungen erstellen, hochgeladene Bilder bearbeiten und sogar mehrere Objekte präzise darstellen. Es zeichnet sich durch eine verbesserte Textdarstellung in Bildern aus, ein Bereich, in dem frühere Modelle wie DALL-E oft Schwierigkeiten hatten.

View file

@ -1,17 +1,17 @@
---
title: "Google Gemini 2.5 Pro: A Breakthrough in AI Technology"
title: 'Google Gemini 2.5 Pro: A Breakthrough in AI Technology'
description: "Google recently unveiled the Gemini 2.5 Pro model, considered the company's most advanced AI model, setting new standards with its massive context window."
pubDate: 2025-03-25
category: "Text"
category: 'Text'
featured: true
author: "BaunTown"
tags: ["Google", "Gemini", "AI", "Multimodal", "Context Window"]
image: "/images/models/google-gemini-2.5pro-bauntown.png"
author: 'BaunTown'
tags: ['Google', 'Gemini', 'AI', 'Multimodal', 'Context Window']
image: '/images/models/google-gemini-2.5pro-bauntown.png'
externalLinks:
- title: "Official Google Announcement"
url: "https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/#gemini-2-5-thinking"
- title: "Try Gemini"
url: "https://gemini.google.com"
- title: 'Official Google Announcement'
url: 'https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/#gemini-2-5-thinking'
- title: 'Try Gemini'
url: 'https://gemini.google.com'
---
Google recently unveiled the Gemini 2.5 Pro model, considered the company's most advanced AI model. With groundbreaking features such as multimodal processing, improved problem-solving capabilities, and an enormous context window of up to 2 million tokens, this model sets new standards in AI development. Reactions to this innovation are diverse worldwide, reflecting both enthusiasm and critical reflection.

View file

@ -1,19 +1,19 @@
---
title: "AI Image Generation Revolution: OpenAI's GPT-4o Sets New Standards"
description: "OpenAI recently unveiled its latest model, GPT-4o, which directly integrates image generation, marking a significant advancement in AI technology."
description: 'OpenAI recently unveiled its latest model, GPT-4o, which directly integrates image generation, marking a significant advancement in AI technology.'
pubDate: 2025-03-25
category: "Bild"
category: 'Bild'
featured: true
author: "BaunTown"
tags: ["GPT-4o", "OpenAI", "AI", "Multimodal", "Image Generation"]
image: "/images/models/openai-gpt4o-imagemode-bauntown.png"
author: 'BaunTown'
tags: ['GPT-4o', 'OpenAI', 'AI', 'Multimodal', 'Image Generation']
image: '/images/models/openai-gpt4o-imagemode-bauntown.png'
externalLinks:
- title: "Official OpenAI Announcement"
url: "https://openai.com/index/introducing-4o-image-generation/"
- title: "ChatGPT - Create Images"
url: "https://chat.openai.com"
- title: 'Official OpenAI Announcement'
url: 'https://openai.com/index/introducing-4o-image-generation/'
- title: 'ChatGPT - Create Images'
url: 'https://chat.openai.com'
- title: "Sora - OpenAI's Video & Image Platform"
url: "https://sora.com"
url: 'https://sora.com'
---
OpenAI recently unveiled its latest model, GPT-4o, which directly integrates image generation, marking a significant advancement in AI technology. This model can create images based on text prompts, edit uploaded images, and even accurately represent multiple objects. It stands out for its improved text rendering in images, an area where previous models like DALL-E often struggled.

View file

@ -1,17 +1,17 @@
---
title: "Google Gemini 2.5 Pro: Una svolta nella tecnologia AI"
title: 'Google Gemini 2.5 Pro: Una svolta nella tecnologia AI'
description: "Google ha recentemente presentato il modello Gemini 2.5 Pro, considerato il modello AI più avanzato dell'azienda, stabilendo nuovi standard con la sua enorme finestra di contesto."
pubDate: 2025-03-25
category: "Text"
category: 'Text'
featured: true
author: "BaunTown"
tags: ["Google", "Gemini", "IA", "Multimodale", "Finestra di contesto"]
image: "/images/models/google-gemini-2.5pro-bauntown.png"
author: 'BaunTown'
tags: ['Google', 'Gemini', 'IA', 'Multimodale', 'Finestra di contesto']
image: '/images/models/google-gemini-2.5pro-bauntown.png'
externalLinks:
- title: "Annuncio ufficiale di Google"
url: "https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/#gemini-2-5-thinking"
- title: "Prova Gemini"
url: "https://gemini.google.com"
- title: 'Annuncio ufficiale di Google'
url: 'https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/#gemini-2-5-thinking'
- title: 'Prova Gemini'
url: 'https://gemini.google.com'
---
Google ha recentemente presentato il modello Gemini 2.5 Pro, considerato il modello di intelligenza artificiale più avanzato dell'azienda. Con caratteristiche rivoluzionarie come l'elaborazione multimodale, migliori capacità di risoluzione dei problemi e un'enorme finestra di contesto fino a 2 milioni di token, questo modello stabilisce nuovi standard nello sviluppo dell'IA. Le reazioni a questa innovazione sono diverse in tutto il mondo, riflettendo sia entusiasmo che riflessione critica.

View file

@ -1,19 +1,19 @@
---
title: "Rivoluzione nella generazione di immagini AI: OpenAI GPT-4o stabilisce nuovi standard"
description: "OpenAI ha recentemente svelato il suo ultimo modello, GPT-4o, che integra direttamente la generazione di immagini, segnando un significativo avanzamento nella tecnologia AI."
title: 'Rivoluzione nella generazione di immagini AI: OpenAI GPT-4o stabilisce nuovi standard'
description: 'OpenAI ha recentemente svelato il suo ultimo modello, GPT-4o, che integra direttamente la generazione di immagini, segnando un significativo avanzamento nella tecnologia AI.'
pubDate: 2025-03-25
category: "Bild"
category: 'Bild'
featured: true
author: "BaunTown"
tags: ["GPT-4o", "OpenAI", "AI", "Multimodale", "Generazione di immagini"]
image: "/images/models/openai-gpt4o-imagemode-bauntown.png"
author: 'BaunTown'
tags: ['GPT-4o', 'OpenAI', 'AI', 'Multimodale', 'Generazione di immagini']
image: '/images/models/openai-gpt4o-imagemode-bauntown.png'
externalLinks:
- title: "Annuncio ufficiale di OpenAI"
url: "https://openai.com/index/introducing-4o-image-generation/"
- title: "ChatGPT - Crea immagini"
url: "https://chat.openai.com"
- title: "Sora - Piattaforma video e immagini di OpenAI"
url: "https://sora.com"
- title: 'Annuncio ufficiale di OpenAI'
url: 'https://openai.com/index/introducing-4o-image-generation/'
- title: 'ChatGPT - Crea immagini'
url: 'https://chat.openai.com'
- title: 'Sora - Piattaforma video e immagini di OpenAI'
url: 'https://sora.com'
---
OpenAI ha recentemente presentato il suo ultimo modello, GPT-4o, che integra direttamente la generazione di immagini, segnando un importante progresso nella tecnologia dell'IA. Questo modello può creare immagini basate su prompt testuali, modificare immagini caricate e persino rappresentare con precisione oggetti multipli. Si distingue per la sua migliore rappresentazione del testo nelle immagini, un'area in cui modelli precedenti come DALL-E spesso avevano difficoltà.

View file

@ -1,15 +1,15 @@
---
title: "BaunTown"
description: "Eine Community-Plattform zum Teilen von Wissen, Tools und Ressourcen, um Entwicklern zu helfen, gemeinsam bessere digitale Produkte zu erstellen."
title: 'BaunTown'
description: 'Eine Community-Plattform zum Teilen von Wissen, Tools und Ressourcen, um Entwicklern zu helfen, gemeinsam bessere digitale Produkte zu erstellen.'
pubDate: 2024-04-04
category: "Web"
category: 'Web'
featured: true
status: "active"
technologies: ["Astro", "TypeScript", "CSS", "Markdown", "Netlify", "Stripe"]
image: "/images/projects/bauntown-project-bauntown-platform.png"
author: "Till Schneider"
githubUrl: "https://github.com/bauntown/website"
demoUrl: "https://baun.town"
status: 'active'
technologies: ['Astro', 'TypeScript', 'CSS', 'Markdown', 'Netlify', 'Stripe']
image: '/images/projects/bauntown-project-bauntown-platform.png'
author: 'Till Schneider'
githubUrl: 'https://github.com/bauntown/website'
demoUrl: 'https://baun.town'
---
# BaunTown Plattform
@ -19,6 +19,7 @@ BaunTown ist unsere zentrale Wissensaustausch-Plattform, die geschaffen wurde, u
## Die Herausforderung
Nach Workshops und Schulungen standen wir immer wieder vor denselben Problemen:
- Teilnehmer fragten nach Assets, Dateien und Dokumentation
- Kein zentraler Ort zur Speicherung und gemeinsamen Nutzung von Materialien
- Schwierigkeiten bei der Nachverfolgung, wer Zugang zu welchen Ressourcen hat
@ -39,6 +40,7 @@ BaunTown bietet:
## Technische Umsetzung
Die Plattform nutzt einen modernen Tech-Stack:
- **Astro** für statische Site-Generierung mit minimalem JavaScript
- **Content Collections** für strukturierte, typsichere Inhalte
- **Markdown/MDX** für einfache Inhaltserstellung
@ -50,10 +52,11 @@ Die Plattform nutzt einen modernen Tech-Stack:
## Zukunftsvision
BaunTown wird sich weiterentwickeln mit:
- Erweiterter Tutorial-Bibliothek zu Entwicklungs- und Design-Themen
- Detailliertere Projektvorstellungen und Fallstudien
- Mitgliederprofile für Mitwirkende
- Verbesserte Community-Funktionen und Diskussionsmöglichkeiten
- Integration mit unseren anderen Produkten und Dienstleistungen
Die Plattform verkörpert unsere Werte des gemeinschaftlichen Lernens, Bauens und Verdienens - sie bietet eine Grundlage für den Wissensaustausch und demonstriert gleichzeitig die modernen Entwicklungsansätze, für die wir eintreten.
Die Plattform verkörpert unsere Werte des gemeinschaftlichen Lernens, Bauens und Verdienens - sie bietet eine Grundlage für den Wissensaustausch und demonstriert gleichzeitig die modernen Entwicklungsansätze, für die wir eintreten.

View file

@ -1,15 +1,16 @@
---
title: "Memoro"
description: "KI-gestützte mobile Anwendung zur Umwandlung von Gesprächen und Gedanken in strukturierte Dokumente."
title: 'Memoro'
description: 'KI-gestützte mobile Anwendung zur Umwandlung von Gesprächen und Gedanken in strukturierte Dokumente.'
pubDate: 2024-04-08
category: "Mobile"
category: 'Mobile'
featured: true
status: "active"
technologies: ["React Native", "Expo", "Supabase", "Azure OpenAI", "Azure Speech", "RevenueCat", "PostHog"]
image: "/images/projects/memoro-project-bauntown-app.png"
author: "Till Schneider"
githubUrl: "https://github.com/bauntown/memoro"
demoUrl: "https://www.memoro.ai"
status: 'active'
technologies:
['React Native', 'Expo', 'Supabase', 'Azure OpenAI', 'Azure Speech', 'RevenueCat', 'PostHog']
image: '/images/projects/memoro-project-bauntown-app.png'
author: 'Till Schneider'
githubUrl: 'https://github.com/bauntown/memoro'
demoUrl: 'https://www.memoro.ai'
---
# Memoro App
@ -77,4 +78,4 @@ Unsere Entwicklung konzentriert sich auf mehrere Schlüsselbereiche:
Wir verbessern außerdem unsere kontextbezogenen Verständnisfähigkeiten, um die Qualität der automatisierten Dokumentation in spezialisierten Domänen und technischen Bereichen weiter zu optimieren.
Memoro repräsentiert unsere Vision für Technologie, die in den Hintergrund tritt und gleichzeitig die menschliche Produktivität steigert - damit Menschen sich auf Gespräche und Ideen konzentrieren können, anstatt auf die Mechanik der Dokumentation.
Memoro repräsentiert unsere Vision für Technologie, die in den Hintergrund tritt und gleichzeitig die menschliche Produktivität steigert - damit Menschen sich auf Gespräche und Ideen konzentrieren können, anstatt auf die Mechanik der Dokumentation.

View file

@ -1,15 +1,15 @@
---
title: "BaunTown"
description: "A community platform for sharing knowledge, tools, and resources to help builders create better digital products together."
title: 'BaunTown'
description: 'A community platform for sharing knowledge, tools, and resources to help builders create better digital products together.'
pubDate: 2024-04-04
category: "Web"
category: 'Web'
featured: true
status: "active"
technologies: ["Astro", "TypeScript", "CSS", "Markdown", "Netlify", "Stripe"]
image: "/images/projects/bauntown-project-bauntown-platform.png"
author: "Till Schneider"
githubUrl: "https://github.com/bauntown/website"
demoUrl: "https://baun.town"
status: 'active'
technologies: ['Astro', 'TypeScript', 'CSS', 'Markdown', 'Netlify', 'Stripe']
image: '/images/projects/bauntown-project-bauntown-platform.png'
author: 'Till Schneider'
githubUrl: 'https://github.com/bauntown/website'
demoUrl: 'https://baun.town'
---
# BaunTown Platform
@ -19,6 +19,7 @@ BaunTown is our central knowledge-sharing platform, created to solve the recurri
## The Challenge
After hosting workshops and training sessions, we consistently faced the same problems:
- Participants asking for assets, files, and documentation
- No centralized place to store and share materials
- Difficulty tracking who has access to what resources
@ -39,6 +40,7 @@ BaunTown provides:
## Technical Implementation
The platform uses a modern tech stack:
- **Astro** for static site generation with minimal JavaScript
- **Content Collections** for structured, type-safe content
- **Markdown/MDX** for easy content creation
@ -50,10 +52,11 @@ The platform uses a modern tech stack:
## Future Vision
BaunTown will continue to evolve with:
- Expanded tutorial library covering development and design topics
- More detailed project showcases and case studies
- Member profiles for contributors
- Enhanced community features and discussion capabilities
- Integration with our other products and services
The platform embodies our values of collaborative learning, building, and earning - providing a foundation for sharing knowledge while demonstrating the modern development approaches we advocate for.
The platform embodies our values of collaborative learning, building, and earning - providing a foundation for sharing knowledge while demonstrating the modern development approaches we advocate for.

View file

@ -1,15 +1,16 @@
---
title: "Memoro"
description: "AI-powered mobile application for transforming conversations and thoughts into structured documents."
title: 'Memoro'
description: 'AI-powered mobile application for transforming conversations and thoughts into structured documents.'
pubDate: 2024-04-08
category: "Mobile"
category: 'Mobile'
featured: true
status: "active"
technologies: ["React Native", "Expo", "Supabase", "Azure OpenAI", "Azure Speech", "RevenueCat", "PostHog"]
image: "/images/projects/memoro-project-bauntown-app.png"
author: "Till Schneider"
githubUrl: "https://github.com/bauntown/memoro"
demoUrl: "https://www.memoro.ai"
status: 'active'
technologies:
['React Native', 'Expo', 'Supabase', 'Azure OpenAI', 'Azure Speech', 'RevenueCat', 'PostHog']
image: '/images/projects/memoro-project-bauntown-app.png'
author: 'Till Schneider'
githubUrl: 'https://github.com/bauntown/memoro'
demoUrl: 'https://www.memoro.ai'
---
# Memoro App
@ -77,4 +78,4 @@ Our development focuses on several key areas:
We're also enhancing our contextual understanding capabilities to further improve the quality of automated documentation across specialized domains and technical fields.
Memoro represents our vision for technology that fades into the background while amplifying human productivity - letting people focus on conversations and ideas rather than the mechanics of documentation.
Memoro represents our vision for technology that fades into the background while amplifying human productivity - letting people focus on conversations and ideas rather than the mechanics of documentation.

View file

@ -1,15 +1,15 @@
---
title: "BaunTown"
description: "Una piattaforma comunitaria per condividere conoscenze, strumenti e risorse per aiutare i costruttori a creare insieme prodotti digitali migliori."
title: 'BaunTown'
description: 'Una piattaforma comunitaria per condividere conoscenze, strumenti e risorse per aiutare i costruttori a creare insieme prodotti digitali migliori.'
pubDate: 2024-04-04
category: "Web"
category: 'Web'
featured: true
status: "active"
technologies: ["Astro", "TypeScript", "CSS", "Markdown", "Netlify", "Stripe"]
image: "/images/projects/bauntown-project-bauntown-platform.png"
author: "Till Schneider"
githubUrl: "https://github.com/bauntown/website"
demoUrl: "https://baun.town"
status: 'active'
technologies: ['Astro', 'TypeScript', 'CSS', 'Markdown', 'Netlify', 'Stripe']
image: '/images/projects/bauntown-project-bauntown-platform.png'
author: 'Till Schneider'
githubUrl: 'https://github.com/bauntown/website'
demoUrl: 'https://baun.town'
---
# Piattaforma BaunTown
@ -19,6 +19,7 @@ BaunTown è la nostra piattaforma centrale per la condivisione delle conoscenze,
## La Sfida
Dopo aver ospitato workshop e sessioni di formazione, abbiamo costantemente affrontato gli stessi problemi:
- Partecipanti che chiedevano assets, file e documentazione
- Nessun luogo centralizzato per archiviare e condividere materiali
- Difficoltà nel tracciare chi ha accesso a quali risorse
@ -39,6 +40,7 @@ BaunTown offre:
## Implementazione Tecnica
La piattaforma utilizza uno stack tecnologico moderno:
- **Astro** per la generazione di siti statici con JavaScript minimo
- **Content Collections** per contenuti strutturati e type-safe
- **Markdown/MDX** per una facile creazione di contenuti
@ -50,10 +52,11 @@ La piattaforma utilizza uno stack tecnologico moderno:
## Visione Futura
BaunTown continuerà a evolversi con:
- Libreria di tutorial ampliata che copre argomenti di sviluppo e design
- Vetrine di progetti più dettagliate e casi di studio
- Profili dei membri per i contributori
- Funzionalità comunitarie migliorate e capacità di discussione
- Integrazione con i nostri altri prodotti e servizi
La piattaforma incarna i nostri valori di apprendimento collaborativo, costruzione e guadagno - fornendo una base per la condivisione delle conoscenze mentre dimostra gli approcci di sviluppo moderni che sosteniamo.
La piattaforma incarna i nostri valori di apprendimento collaborativo, costruzione e guadagno - fornendo una base per la condivisione delle conoscenze mentre dimostra gli approcci di sviluppo moderni che sosteniamo.

View file

@ -1,15 +1,16 @@
---
title: "Memoro"
description: "Applicazione mobile basata su IA per trasformare conversazioni e pensieri in documenti strutturati."
title: 'Memoro'
description: 'Applicazione mobile basata su IA per trasformare conversazioni e pensieri in documenti strutturati.'
pubDate: 2024-04-08
category: "Mobile"
category: 'Mobile'
featured: true
status: "active"
technologies: ["React Native", "Expo", "Supabase", "Azure OpenAI", "Azure Speech", "RevenueCat", "PostHog"]
image: "/images/projects/memoro-project-bauntown-app.png"
author: "Till Schneider"
githubUrl: "https://github.com/bauntown/memoro"
demoUrl: "https://www.memoro.ai"
status: 'active'
technologies:
['React Native', 'Expo', 'Supabase', 'Azure OpenAI', 'Azure Speech', 'RevenueCat', 'PostHog']
image: '/images/projects/memoro-project-bauntown-app.png'
author: 'Till Schneider'
githubUrl: 'https://github.com/bauntown/memoro'
demoUrl: 'https://www.memoro.ai'
---
# App Memoro
@ -77,4 +78,4 @@ Il nostro sviluppo si concentra su diverse aree chiave:
Stiamo anche migliorando le nostre capacità di comprensione contestuale per migliorare ulteriormente la qualità della documentazione automatizzata in domini specializzati e campi tecnici.
Memoro rappresenta la nostra visione di una tecnologia che scompare sullo sfondo mentre amplifica la produttività umana - permettendo alle persone di concentrarsi sulle conversazioni e le idee piuttosto che sulla meccanica della documentazione.
Memoro rappresenta la nostra visione di una tecnologia che scompare sullo sfondo mentre amplifica la produttività umana - permettendo alle persone di concentrarsi sulle conversazioni e le idee piuttosto che sulla meccanica della documentazione.

Some files were not shown because too many files have changed in this diff Show more