chore: overhaul infrastructure and integrate @mintel packages
Some checks failed
🧪 CI (QA) / 🧪 Quality Assurance (push) Failing after 1m3s
Some checks failed
🧪 CI (QA) / 🧪 Quality Assurance (push) Failing after 1m3s
- Restructure to pnpm monorepo (site moved to apps/web) - Integrate @mintel/tsconfig, @mintel/eslint-config, @mintel/husky-config - Implement Docker service architecture (Varnish, Directus, Gatekeeper) - Setup environment-aware Gitea Actions deployment
This commit is contained in:
21
apps/web/.env.example
Normal file
21
apps/web/.env.example
Normal file
@@ -0,0 +1,21 @@
|
||||
# Hetzner Deployment Environment Variables
|
||||
# Copy this file to .env and fill in your values
|
||||
|
||||
# Required Variables
|
||||
DOMAIN=mintel.me
|
||||
ADMIN_EMAIL=marc@mintel.me
|
||||
|
||||
# Optional Variables (with defaults)
|
||||
REDIS_URL=redis://redis:6379
|
||||
|
||||
# Analytics Configuration
|
||||
PLAUSIBLE_DOMAIN=mintel.me
|
||||
PLAUSIBLE_SCRIPT_URL=https://plausible.yourdomain.com/js/script.js
|
||||
|
||||
# Woodpecker CI/CD Variables (for CI deployment)
|
||||
# DEPLOY_HOST=your-hetzner-ip
|
||||
# DEPLOY_USER=root
|
||||
# SSH_PRIVATE_KEY=your-ssh-key
|
||||
|
||||
# Optional: Slack notifications
|
||||
# SLACK_WEBHOOK=https://hooks.slack.com/services/YOUR/WEBHOOK/URL
|
||||
269
apps/web/DEPLOYMENT.md
Normal file
269
apps/web/DEPLOYMENT.md
Normal file
@@ -0,0 +1,269 @@
|
||||
# Hetzner Deployment Guide
|
||||
|
||||
Complete deployment plan for your Astro blog on Hetzner using Docker, with your existing Gitea + Woodpecker setup.
|
||||
|
||||
## 🏗️ Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
||||
│ Hetzner VM │ │ Caddy Reverse │ │ Plausible │
|
||||
│ (Ubuntu) │◄──►│ Proxy │◄──►│ Analytics │
|
||||
│ │ │ (SSL/HTTP2) │ │ (Existing) │
|
||||
└────────┬────────┘ └──────────────────┘ └─────────────────┘
|
||||
│
|
||||
│ Docker Network
|
||||
│
|
||||
┌────▼─────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ Nginx │ │ Redis │ │ Website │
|
||||
│ Static │ │ Cache │ │ (Astro) │
|
||||
│ Server │ │ │ │ │
|
||||
└──────────┘ └──────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
mintel.me/
|
||||
├── docker/
|
||||
│ ├── Dockerfile # Multi-stage build
|
||||
│ ├── nginx.conf # Nginx config with caching
|
||||
│ └── Caddyfile # SSL reverse proxy
|
||||
├── docker-compose.yml # Local & Hetzner deployment
|
||||
├── .woodpecker.yml # CI/CD pipeline
|
||||
├── deploy.sh # Manual deployment script
|
||||
├── src/
|
||||
│ ├── utils/
|
||||
│ │ └── cache/ # Cache abstraction layer
|
||||
│ │ ├── interfaces.ts
|
||||
│ │ ├── memory-adapter.ts
|
||||
│ │ ├── redis-adapter.ts
|
||||
│ │ ├── composite-adapter.ts
|
||||
│ │ └── index.ts
|
||||
│ ├── components/
|
||||
│ │ └── Analytics.astro # Plausible integration
|
||||
│ └── layouts/
|
||||
│ └── BaseLayout.astro # Includes analytics
|
||||
└── package.json # Added ioredis dependency
|
||||
```
|
||||
|
||||
## 🚀 Deployment Options
|
||||
|
||||
### Option 1: Manual Deployment (Quick Start)
|
||||
|
||||
```bash
|
||||
# 1. Provision Hetzner VM (Ubuntu 22.04)
|
||||
# 2. Install Docker + Docker Compose
|
||||
ssh root@YOUR_HETZNER_IP "apt update && apt install -y docker.io docker-compose"
|
||||
|
||||
# 3. Copy files to server
|
||||
scp -r . root@YOUR_HETZNER_IP:/opt/mintel
|
||||
|
||||
# 4. Set environment variables
|
||||
ssh root@YOUR_HETZNER_IP "cat > /opt/mintel/.env << EOF
|
||||
DOMAIN=mintel.me
|
||||
ADMIN_EMAIL=admin@mintel.me
|
||||
REDIS_URL=redis://redis:6379
|
||||
EOF"
|
||||
|
||||
# 5. Deploy
|
||||
ssh root@YOUR_HETZNER_IP "cd /opt/mintel && ./deploy.sh"
|
||||
```
|
||||
|
||||
### Option 2: CI/CD with Woodpecker + Gitea (Recommended)
|
||||
|
||||
Your existing Woodpecker + Gitea setup will handle automatic deployments:
|
||||
|
||||
1. **Configure Woodpecker secrets** in your Gitea repo:
|
||||
- `SSH_PRIVATE_KEY` - Private key for Hetzner access
|
||||
- `DEPLOY_HOST` - Hetzner server IP
|
||||
- `DEPLOY_USER` - Usually `root`
|
||||
- `DOMAIN` - Your domain
|
||||
- `ADMIN_EMAIL` - Contact email
|
||||
|
||||
2. **Push to main branch** triggers automatic deployment
|
||||
|
||||
## 🔧 Environment Variables
|
||||
|
||||
See [ENV_SETUP.md](./ENV_SETUP.md) for detailed configuration guide.
|
||||
|
||||
**Quick setup:**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your values
|
||||
```
|
||||
|
||||
**Required variables:**
|
||||
- `DOMAIN` - Your domain (e.g., mintel.me)
|
||||
- `ADMIN_EMAIL` - SSL certificate contact
|
||||
|
||||
**Optional variables:**
|
||||
- `REDIS_URL` - Redis connection (default: `redis://redis:6379`)
|
||||
- `PLAUSIBLE_DOMAIN` - Analytics domain
|
||||
- `PLAUSIBLE_SCRIPT_URL` - Plausible script URL
|
||||
|
||||
## 🎯 Performance Optimization
|
||||
|
||||
### Redis Caching Strategy
|
||||
|
||||
The cache abstraction layer provides:
|
||||
|
||||
```typescript
|
||||
// Usage in your Astro pages
|
||||
import { withCache } from '../utils/cache';
|
||||
|
||||
// Cache expensive operations
|
||||
const blogPosts = await withCache(
|
||||
'blog:posts',
|
||||
() => fetchBlogPosts(),
|
||||
3600 // 1 hour TTL
|
||||
);
|
||||
```
|
||||
|
||||
**Cache Layers:**
|
||||
1. **Memory Cache** - Fast, single instance
|
||||
2. **Redis Cache** - Distributed, multi-instance
|
||||
3. **Composite Cache** - Auto-fallback Redis → Memory
|
||||
|
||||
### Nginx Caching
|
||||
|
||||
- **Static assets**: 1 year cache
|
||||
- **HTML**: No cache (always fresh)
|
||||
- **Gzip/Brotli**: Enabled
|
||||
|
||||
### Caddy Benefits
|
||||
|
||||
- **Automatic SSL** with Let's Encrypt
|
||||
- **HTTP/2** out of the box
|
||||
- **Automatic redirects** HTTP → HTTPS
|
||||
- **Security headers** configured
|
||||
|
||||
## 📊 Analytics Integration
|
||||
|
||||
### Plausible Setup
|
||||
|
||||
Your existing Plausible instance is already self-hosted. Integration:
|
||||
|
||||
1. **Update Analytics.astro** with your Plausible domain:
|
||||
```astro
|
||||
src="https://plausible.yourdomain.com/js/script.js"
|
||||
```
|
||||
|
||||
2. **Add subdomain** to Caddyfile:
|
||||
```caddy
|
||||
analytics.mintel.me {
|
||||
reverse_proxy http://YOUR_PLAUSIBLE_SERVER:8000
|
||||
}
|
||||
```
|
||||
|
||||
3. **Track custom events**:
|
||||
```javascript
|
||||
window.plausible('Page Load', { props: { loadTime: 123 }});
|
||||
```
|
||||
|
||||
## 🔐 Security
|
||||
|
||||
### Caddy Security Headers
|
||||
```caddy
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-XSS-Protection "1; mode=block"
|
||||
Server "mintel"
|
||||
}
|
||||
```
|
||||
|
||||
### Docker Security
|
||||
- Non-root user in containers
|
||||
- Read-only filesystem where possible
|
||||
- Limited capabilities
|
||||
|
||||
## 📈 Monitoring & Health
|
||||
|
||||
### Health Check Endpoint
|
||||
```
|
||||
https://mintel.me/health
|
||||
```
|
||||
Returns: `healthy`
|
||||
|
||||
### Docker Commands
|
||||
```bash
|
||||
# View logs
|
||||
docker logs -f mintel-website
|
||||
|
||||
# Check Redis
|
||||
docker exec -it mintel-redis redis-cli ping
|
||||
|
||||
# Restart services
|
||||
docker-compose restart
|
||||
|
||||
# Update deployment
|
||||
docker-compose pull && docker-compose up -d
|
||||
```
|
||||
|
||||
## 🔄 CI/CD Pipeline Flow
|
||||
|
||||
1. **Push to Gitea** → Woodpecker triggers
|
||||
2. **Test** → Run smoke tests
|
||||
3. **Build** → Create Docker image
|
||||
4. **Push** → Registry (optional)
|
||||
5. **Deploy** → SSH to Hetzner, pull & restart
|
||||
|
||||
## 🚨 Troubleshooting
|
||||
|
||||
### SSL Issues
|
||||
```bash
|
||||
# Check Caddy logs
|
||||
docker logs mintel-caddy
|
||||
|
||||
# Force certificate renewal
|
||||
docker exec mintel-caddy caddy renew
|
||||
```
|
||||
|
||||
### Redis Connection
|
||||
```bash
|
||||
# Test Redis
|
||||
docker exec mintel-redis redis-cli ping
|
||||
|
||||
# Check Redis logs
|
||||
docker logs mintel-redis
|
||||
```
|
||||
|
||||
### Cache Issues
|
||||
```bash
|
||||
# Clear all cache
|
||||
docker exec mintel-redis redis-cli FLUSHALL
|
||||
```
|
||||
|
||||
## 📦 Dependencies
|
||||
|
||||
### Required
|
||||
- `ioredis` - Redis client
|
||||
- Already have: Astro, React, Tailwind
|
||||
|
||||
### Docker Images
|
||||
- `nginx:alpine` - Web server
|
||||
- `redis:7-alpine` - Cache
|
||||
- `caddy:2-alpine` - Reverse proxy
|
||||
|
||||
## 🎯 Next Steps
|
||||
|
||||
1. ✅ **Configure** environment variables
|
||||
2. ✅ **Test** locally with `docker-compose up`
|
||||
3. ✅ **Setup** Woodpecker secrets
|
||||
4. ✅ **Deploy** to Hetzner
|
||||
5. ✅ **Monitor** with Plausible analytics
|
||||
|
||||
## 💡 Tips
|
||||
|
||||
- **First deployment**: Use manual `deploy.sh` to verify everything works
|
||||
- **Updates**: CI/CD handles everything on git push
|
||||
- **Rollback**: `docker-compose down && docker-compose up -d` with previous image
|
||||
- **Scaling**: Add more web containers, Redis handles distributed cache
|
||||
|
||||
---
|
||||
|
||||
**Total setup time**: ~30 minutes
|
||||
**Maintenance**: Near zero (automated)
|
||||
**Performance**: Excellent (Redis + Nginx caching)
|
||||
**Analytics**: Privacy-focused (Plausible)
|
||||
192
apps/web/DEPLOYMENT_SUMMARY.md
Normal file
192
apps/web/DEPLOYMENT_SUMMARY.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# 🚀 Hetzner Deployment - Complete Plan
|
||||
|
||||
## ✅ What's Ready
|
||||
|
||||
### 1. **Docker Setup**
|
||||
- ✅ Multi-stage Dockerfile (optimized build)
|
||||
- ✅ Nginx config with caching & security headers
|
||||
- ✅ Caddy reverse proxy with automatic SSL
|
||||
- ✅ Redis container for caching
|
||||
- ✅ Docker Compose for local & production
|
||||
|
||||
### 2. **CI/CD Pipeline**
|
||||
- ✅ Woodpecker `.woodpecker.yml` config
|
||||
- ✅ Automatic testing, building, deployment
|
||||
- ✅ Slack notifications (optional)
|
||||
- ✅ Gitea integration ready
|
||||
|
||||
### 3. **Cache Architecture** (Professional DI)
|
||||
```
|
||||
src/utils/cache/
|
||||
├── interfaces.ts # Contracts
|
||||
├── memory-adapter.ts # In-memory implementation
|
||||
├── redis-adapter.ts # Redis implementation
|
||||
└── index.ts # Service + Factory
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
// Choose your adapter at construction
|
||||
const cache = new CacheService(new RedisCacheAdapter(config));
|
||||
// or
|
||||
const cache = new CacheService(new MemoryCacheAdapter(config));
|
||||
|
||||
// Use it
|
||||
await cache.wrap('key', asyncFn, ttl);
|
||||
```
|
||||
|
||||
### 4. **Clean Analytics Architecture** (Constructor DI)
|
||||
```
|
||||
src/utils/analytics/
|
||||
├── interfaces.ts # Contracts
|
||||
├── plausible-adapter.ts # Plausible implementation
|
||||
└── index.ts # Service + Factory
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
// Choose adapter at construction
|
||||
const analytics = new AnalyticsService(new PlausibleAdapter(config));
|
||||
|
||||
// Track events
|
||||
await analytics.trackEvent('Page Load', { loadTime: 123 });
|
||||
await analytics.trackOutboundLink(url, text);
|
||||
await analytics.trackSearch(query, path);
|
||||
```
|
||||
|
||||
**Astro Component:**
|
||||
```astro
|
||||
---
|
||||
import { createPlausibleAnalytics } from '../utils/analytics';
|
||||
const analytics = createPlausibleAnalytics({ domain, scriptUrl });
|
||||
const adapter = analytics.getAdapter();
|
||||
const scriptTag = adapter.getScriptTag?.();
|
||||
---
|
||||
{scriptTag && <Fragment set:html={scriptTag} />}
|
||||
```
|
||||
|
||||
### 5. **Deployment Scripts**
|
||||
- ✅ `deploy.sh` - Manual deployment
|
||||
- ✅ `docker-compose.yml` - Container orchestration
|
||||
- ✅ `DEPLOYMENT.md` - Complete documentation
|
||||
|
||||
## 🎯 Simple Deployment Flow
|
||||
|
||||
### Option A: Manual (5 minutes)
|
||||
```bash
|
||||
# On Hetzner VM
|
||||
git clone your-repo /opt/mintel
|
||||
cd /opt/mintel
|
||||
echo "DOMAIN=mintel.me" > .env
|
||||
echo "ADMIN_EMAIL=admin@mintel.me" >> .env
|
||||
./deploy.sh
|
||||
```
|
||||
|
||||
### Option B: CI/CD (Zero touch)
|
||||
```bash
|
||||
# On your machine
|
||||
git push origin main
|
||||
# Woodpecker handles everything
|
||||
```
|
||||
|
||||
## 📊 Performance Features
|
||||
|
||||
| Feature | Implementation | Benefit |
|
||||
|---------|---------------|---------|
|
||||
| **Static Caching** | Nginx 1-year cache | Instant asset loading |
|
||||
| **Redis Cache** | CacheService wrapper | Fast data retrieval |
|
||||
| **Gzip/Brotli** | Nginx compression | ~70% smaller transfers |
|
||||
| **HTTP/2** | Caddy reverse proxy | Multiplexed requests |
|
||||
| **Auto-SSL** | Let's Encrypt via Caddy | Zero config HTTPS |
|
||||
|
||||
## 🔒 Security
|
||||
|
||||
- ✅ HSTS headers
|
||||
- ✅ XSS protection
|
||||
- ✅ Clickjacking prevention
|
||||
- ✅ Server header hiding
|
||||
- ✅ Automatic SSL renewal
|
||||
|
||||
## 🏗️ Architecture (Clean & Decoupled)
|
||||
|
||||
**Cache Pattern:**
|
||||
```
|
||||
Your Code → CacheService → [Redis | Memory]Adapter
|
||||
↓
|
||||
CacheAdapter Interface
|
||||
```
|
||||
|
||||
**Analytics Pattern:**
|
||||
```
|
||||
Your Code → AnalyticsService → [Plausible]Adapter
|
||||
↓
|
||||
AnalyticsAdapter Interface
|
||||
```
|
||||
|
||||
**Constructor Injection:**
|
||||
```typescript
|
||||
// Cache - Production
|
||||
const cache = new CacheService(new RedisCacheAdapter(config));
|
||||
|
||||
// Cache - Development/Testing
|
||||
const cache = new CacheService(new MemoryCacheAdapter(config));
|
||||
|
||||
// Analytics
|
||||
const analytics = new AnalyticsService(new PlausibleAdapter(config));
|
||||
```
|
||||
|
||||
## 📁 Files Created
|
||||
|
||||
```
|
||||
mintel.me/
|
||||
├── docker/
|
||||
│ ├── Dockerfile
|
||||
│ ├── nginx.conf
|
||||
│ └── Caddyfile
|
||||
├── docker-compose.yml
|
||||
├── .woodpecker.yml
|
||||
├── deploy.sh
|
||||
├── DEPLOYMENT.md
|
||||
├── DEPLOYMENT_SUMMARY.md
|
||||
├── src/
|
||||
│ ├── utils/cache/
|
||||
│ │ ├── interfaces.ts
|
||||
│ │ ├── memory-adapter.ts
|
||||
│ │ ├── redis-adapter.ts
|
||||
│ │ └── index.ts
|
||||
│ ├── utils/analytics/
|
||||
│ │ ├── interfaces.ts
|
||||
│ │ ├── plausible-adapter.ts
|
||||
│ │ └── index.ts
|
||||
│ ├── components/
|
||||
│ │ └── Analytics.astro (clean DI pattern)
|
||||
│ └── layouts/
|
||||
│ └── BaseLayout.astro (includes analytics)
|
||||
└── package.json (ioredis added)
|
||||
```
|
||||
|
||||
## 🎯 Next Steps
|
||||
|
||||
1. **Configure** environment variables
|
||||
2. **Test locally**: `docker-compose up`
|
||||
3. **Setup Woodpecker secrets** in Gitea
|
||||
4. **Deploy** to Hetzner
|
||||
5. **Monitor** with Plausible
|
||||
|
||||
## 🚀 Result
|
||||
|
||||
- **Fast**: Redis + Nginx caching
|
||||
- **Secure**: Auto SSL + security headers
|
||||
- **Simple**: One command deployment
|
||||
- **Clean**: Proper DI architecture
|
||||
- **Smart**: CI/CD automation
|
||||
- **Private**: Self-hosted analytics
|
||||
|
||||
**Total setup time**: ~15 minutes
|
||||
**Maintenance**: Near zero
|
||||
**Performance**: Excellent
|
||||
**Security**: Production-ready
|
||||
|
||||
---
|
||||
|
||||
**Ready to deploy!** 🎉
|
||||
149
apps/web/EMBED_SOLUTION_SUMMARY.md
Normal file
149
apps/web/EMBED_SOLUTION_SUMMARY.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# 🎉 Free Blog Embed Solution - Complete!
|
||||
|
||||
## What You Built
|
||||
|
||||
You now have a **complete, free solution** for embedding rich content from tweets, YouTube, and other platforms in your blog. All components use **build-time generation** for maximum performance and give you **full styling control**.
|
||||
|
||||
## 📁 Files Created
|
||||
|
||||
### Core Components (src/components/)
|
||||
- **YouTubeEmbed.astro** - YouTube videos with lazy loading
|
||||
- **TwitterEmbed.astro** - Twitter/X tweets via oEmbed API
|
||||
- **GenericEmbed.astro** - Universal oEmbed support
|
||||
- **Embeds/index.ts** - Export file for easy imports
|
||||
|
||||
### Supporting Files
|
||||
- **EMBED_USAGE_GUIDE.md** - Complete usage documentation
|
||||
- **src/data/embedDemoPost.ts** - Demo data for testing
|
||||
- **scripts/test-embeds.ts** - Component verification script
|
||||
- **plans/embed-architecture.md** - Technical architecture
|
||||
|
||||
## 🚀 Key Features
|
||||
|
||||
### ✅ Build-Time Generation
|
||||
- All embeds fetched during build, not at runtime
|
||||
- No external API calls on page load
|
||||
- Fast, SEO-friendly static HTML
|
||||
|
||||
### ✅ Full Styling Control
|
||||
- CSS variables for easy customization
|
||||
- Data attributes for style variations
|
||||
- Tailwind-compatible class names
|
||||
- Hover effects and transitions
|
||||
|
||||
### ✅ Performance Optimized
|
||||
- Lazy loading with Intersection Observer
|
||||
- Fallbacks for API failures
|
||||
- Zero client-side dependencies
|
||||
- CDN compatible
|
||||
|
||||
### ✅ Free & Self-Hosted
|
||||
- No paid services required
|
||||
- Uses official APIs only
|
||||
- Complete ownership of code
|
||||
- No vendor lock-in
|
||||
|
||||
## 🎯 Usage Examples
|
||||
|
||||
### YouTube Embed
|
||||
```astro
|
||||
<YouTubeEmbed
|
||||
videoId="dQw4w9WgXcQ"
|
||||
style="minimal"
|
||||
className="my-8"
|
||||
/>
|
||||
```
|
||||
|
||||
### Twitter Embed
|
||||
```astro
|
||||
<TwitterEmbed
|
||||
tweetId="1234567890123456789"
|
||||
theme="dark"
|
||||
align="center"
|
||||
/>
|
||||
```
|
||||
|
||||
### Generic Embed
|
||||
```astro
|
||||
<GenericEmbed
|
||||
url="https://vimeo.com/123456789"
|
||||
type="video"
|
||||
maxWidth="800px"
|
||||
/>
|
||||
```
|
||||
|
||||
## 🎨 Styling Examples
|
||||
|
||||
### Custom CSS Variables
|
||||
```css
|
||||
.youtube-embed {
|
||||
--aspect-ratio: 56.25%;
|
||||
--bg-color: #000000;
|
||||
--border-radius: 12px;
|
||||
--shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
```
|
||||
|
||||
### Data Attribute Styles
|
||||
```css
|
||||
.youtube-embed[data-style="minimal"] { /* ... */ }
|
||||
.youtube-embed[data-aspect="square"] { /* ... */ }
|
||||
.twitter-embed[data-theme="dark"] { /* ... */ }
|
||||
```
|
||||
|
||||
## 📊 Performance Comparison
|
||||
|
||||
| Feature | Paid Services | Your Solution |
|
||||
|---------|---------------|---------------|
|
||||
| **Cost** | $10-50/month | **Free** |
|
||||
| **Build Time** | Runtime API calls | **Build-time fetch** |
|
||||
| **Styling** | Limited | **Full control** |
|
||||
| **Data Privacy** | Third-party | **Self-hosted** |
|
||||
| **Performance** | Good | **Excellent** |
|
||||
| **Customization** | Restricted | **Unlimited** |
|
||||
|
||||
## 🔧 Integration Steps
|
||||
|
||||
1. **Copy components** to your `src/components/` directory
|
||||
2. **Import in blog posts** using standard Astro imports
|
||||
3. **Customize styling** with CSS variables or classes
|
||||
4. **Add to existing posts** by updating `[slug].astro`
|
||||
5. **Test locally** with `npm run dev`
|
||||
|
||||
## 📝 Next Steps
|
||||
|
||||
1. ✅ **Components are ready** - All files created and tested
|
||||
2. 📖 **Documentation complete** - Usage guide with examples
|
||||
3. 🎨 **Styling flexible** - Full control via CSS variables
|
||||
4. 🚀 **Ready to deploy** - Works with your existing setup
|
||||
|
||||
## 💡 Example Blog Post Integration
|
||||
|
||||
```astro
|
||||
---
|
||||
// In src/pages/blog/[slug].astro
|
||||
import YouTubeEmbed from '../components/YouTubeEmbed.astro';
|
||||
import TwitterEmbed from '../components/TwitterEmbed.astro';
|
||||
---
|
||||
|
||||
{post.slug === 'my-tech-post' && (
|
||||
<>
|
||||
<h2>YouTube Demo</h2>
|
||||
<YouTubeEmbed videoId="abc123" style="rounded" />
|
||||
|
||||
<h2>Tweet Example</h2>
|
||||
<TwitterEmbed tweetId="123456789" theme="dark" />
|
||||
</>
|
||||
)}
|
||||
```
|
||||
|
||||
## 🎉 Result
|
||||
|
||||
You now have:
|
||||
- ✅ **No paid services** needed
|
||||
- ✅ **Full styling control** over all embeds
|
||||
- ✅ **Build-time generation** for speed
|
||||
- ✅ **Reusable components** for any platform
|
||||
- ✅ **Complete documentation** and examples
|
||||
|
||||
**Your blog now supports rich content embedding with zero cost and maximum control!** 🚀
|
||||
259
apps/web/EMBED_USAGE_GUIDE.md
Normal file
259
apps/web/EMBED_USAGE_GUIDE.md
Normal file
@@ -0,0 +1,259 @@
|
||||
# Embed Components Usage Guide
|
||||
|
||||
## Overview
|
||||
Your blog now supports rich content embedding from YouTube, Twitter/X, and other platforms with **full styling control** and **build-time generation**.
|
||||
|
||||
## Components Available
|
||||
|
||||
### 1. YouTubeEmbed
|
||||
**Location:** `src/components/YouTubeEmbed.astro`
|
||||
|
||||
**Features:**
|
||||
- Build-time generation (no client-side API calls)
|
||||
- Full styling control via CSS variables
|
||||
- Lazy loading with Intersection Observer
|
||||
- Multiple style variations
|
||||
- Responsive aspect ratios
|
||||
|
||||
**Usage:**
|
||||
```astro
|
||||
---
|
||||
import YouTubeEmbed from '../components/YouTubeEmbed.astro';
|
||||
---
|
||||
|
||||
<YouTubeEmbed
|
||||
videoId="dQw4w9WgXcQ"
|
||||
title="My Video"
|
||||
className="my-custom-class"
|
||||
aspectRatio="56.25%"
|
||||
style="minimal"
|
||||
/>
|
||||
```
|
||||
|
||||
**Props:**
|
||||
- `videoId` (required) - YouTube video ID
|
||||
- `title` (optional) - Video title for accessibility
|
||||
- `className` (optional) - Custom CSS classes
|
||||
- `aspectRatio` (optional) - Default "56.25%" (16:9)
|
||||
- `style` (optional) - 'default' | 'minimal' | 'rounded' | 'flat'
|
||||
|
||||
**Styling Control:**
|
||||
```css
|
||||
/* Override CSS variables */
|
||||
.youtube-embed {
|
||||
--aspect-ratio: 56.25%;
|
||||
--bg-color: #000000;
|
||||
--border-radius: 8px;
|
||||
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
/* Use data attributes for variations */
|
||||
.youtube-embed[data-style="minimal"] { /* ... */ }
|
||||
.youtube-embed[data-aspect="square"] { /* ... */ }
|
||||
```
|
||||
|
||||
### 2. TwitterEmbed
|
||||
**Location:** `src/components/TwitterEmbed.astro`
|
||||
|
||||
**Features:**
|
||||
- Fetches tweet data at build time using Twitter oEmbed API
|
||||
- Fallback to simple link if API fails
|
||||
- Theme support (light/dark)
|
||||
- Alignment options
|
||||
- Runtime widget loading for enhanced display
|
||||
|
||||
**Usage:**
|
||||
```astro
|
||||
---
|
||||
import TwitterEmbed from '../components/TwitterEmbed.astro';
|
||||
---
|
||||
|
||||
<TwitterEmbed
|
||||
tweetId="1234567890123456789"
|
||||
theme="dark"
|
||||
className="my-tweet"
|
||||
align="center"
|
||||
/>
|
||||
```
|
||||
|
||||
**Props:**
|
||||
- `tweetId` (required) - Tweet ID from URL
|
||||
- `theme` (optional) - 'light' | 'dark'
|
||||
- `className` (optional) - Custom CSS classes
|
||||
- `align` (optional) - 'left' | 'center' | 'right'
|
||||
|
||||
**Note:** Requires internet access during build to fetch tweet data.
|
||||
|
||||
### 3. GenericEmbed
|
||||
**Location:** `src/components/GenericEmbed.astro`
|
||||
|
||||
**Features:**
|
||||
- Universal oEmbed support
|
||||
- Auto-detects provider
|
||||
- Fallback for unsupported platforms
|
||||
- Type-specific styling (video, article, rich)
|
||||
|
||||
**Usage:**
|
||||
```astro
|
||||
---
|
||||
import GenericEmbed from '../components/GenericEmbed.astro';
|
||||
---
|
||||
|
||||
<GenericEmbed
|
||||
url="https://vimeo.com/123456789"
|
||||
type="video"
|
||||
className="my-embed"
|
||||
maxWidth="800px"
|
||||
/>
|
||||
```
|
||||
|
||||
**Props:**
|
||||
- `url` (required) - Full URL to embed
|
||||
- `type` (optional) - 'video' | 'article' | 'rich'
|
||||
- `className` (optional) - Custom CSS classes
|
||||
- `maxWidth` (optional) - Container max width
|
||||
|
||||
**Supported Providers:**
|
||||
- YouTube (via oEmbed)
|
||||
- Vimeo (via oEmbed)
|
||||
- Twitter/X (via oEmbed)
|
||||
- CodePen (via oEmbed)
|
||||
- GitHub Gists (via oEmbed)
|
||||
|
||||
## Integration Examples
|
||||
|
||||
### In Blog Posts (src/pages/blog/[slug].astro)
|
||||
```astro
|
||||
---
|
||||
import YouTubeEmbed from '../components/YouTubeEmbed.astro';
|
||||
import TwitterEmbed from '../components/TwitterEmbed.astro';
|
||||
import GenericEmbed from '../components/GenericEmbed.astro';
|
||||
---
|
||||
|
||||
{post.slug === 'my-post' && (
|
||||
<>
|
||||
<h2>YouTube Example</h2>
|
||||
<YouTubeEmbed videoId="dQw4w9WgXcQ" style="minimal" />
|
||||
|
||||
<h2>Twitter Example</h2>
|
||||
<TwitterEmbed tweetId="1234567890123456789" theme="dark" />
|
||||
|
||||
<h2>Vimeo Example</h2>
|
||||
<GenericEmbed url="https://vimeo.com/123456789" type="video" />
|
||||
</>
|
||||
)}
|
||||
```
|
||||
|
||||
### In MDX Content
|
||||
```mdx
|
||||
import { YouTubeEmbed, TwitterEmbed } from '../components/Embeds';
|
||||
|
||||
# My Blog Post
|
||||
|
||||
Here's a YouTube video:
|
||||
|
||||
<YouTubeEmbed videoId="dQw4w9WgXcQ" style="rounded" />
|
||||
|
||||
And a tweet:
|
||||
|
||||
<TwitterEmbed tweetId="1234567890123456789" />
|
||||
```
|
||||
|
||||
## Custom Styling Examples
|
||||
|
||||
### Minimal Blog Style
|
||||
```css
|
||||
/* In your global.css */
|
||||
.youtube-embed[data-style="minimal"] {
|
||||
--border-radius: 4px;
|
||||
--shadow: none;
|
||||
--bg-color: #1a1a1a;
|
||||
}
|
||||
|
||||
.twitter-embed {
|
||||
--border-radius: 6px;
|
||||
--bg-color: #f8fafc;
|
||||
}
|
||||
|
||||
/* Hover effects */
|
||||
.embed-wrapper:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
```
|
||||
|
||||
### Dark Mode Support
|
||||
```css
|
||||
/* Dark mode adjustments */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.youtube-embed {
|
||||
--bg-color: #000000;
|
||||
--border-color: #334155;
|
||||
}
|
||||
|
||||
.twitter-embed[data-theme="dark"] {
|
||||
--bg-color: #1e293b;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Width & Alignment
|
||||
```astro
|
||||
<YouTubeEmbed
|
||||
videoId="dQw4w9WgXcQ"
|
||||
className="max-w-2xl mx-auto"
|
||||
/>
|
||||
```
|
||||
|
||||
## Performance Benefits
|
||||
|
||||
1. **Build-Time Generation** - All embeds are fetched at build time
|
||||
2. **Zero Client JS** - No external API calls on page load
|
||||
3. **Lazy Loading** - Iframes load only when visible
|
||||
4. **CDN Compatible** - Works with any static hosting
|
||||
5. **SEO Friendly** - Static HTML content
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Twitter Embed Not Loading
|
||||
- Check internet connection during build
|
||||
- Verify tweet ID is correct
|
||||
- Check Twitter API status
|
||||
- Fallback link will be shown
|
||||
|
||||
### YouTube Embed Issues
|
||||
- Verify video ID is correct
|
||||
- Video must be public/unlisted
|
||||
- Check iframe restrictions
|
||||
|
||||
### Generic Embed Failures
|
||||
- Provider must support oEmbed
|
||||
- Some providers require authentication
|
||||
- Fallback to simple link provided
|
||||
|
||||
## Migration from Paid Services
|
||||
|
||||
If you were using services like Embedly or Iframely:
|
||||
|
||||
1. **Replace imports:**
|
||||
```diff
|
||||
- import Embedly from 'embedly-react';
|
||||
+ import YouTubeEmbed from '../components/YouTubeEmbed.astro';
|
||||
```
|
||||
|
||||
2. **Update props:**
|
||||
```diff
|
||||
- <Embedly url={videoUrl} />
|
||||
+ <YouTubeEmbed videoId="abc123" />
|
||||
```
|
||||
|
||||
3. **Customize styling** using CSS variables
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Test components in your development environment
|
||||
2. Add embeds to existing blog posts
|
||||
3. Customize styling to match your theme
|
||||
4. Consider adding more providers (Instagram, TikTok, etc.)
|
||||
|
||||
All components are **free**, **self-hosted**, and give you **complete control** over styling and behavior.
|
||||
210
apps/web/ENV_SETUP.md
Normal file
210
apps/web/ENV_SETUP.md
Normal file
@@ -0,0 +1,210 @@
|
||||
# Environment Setup Guide
|
||||
|
||||
This guide explains how to configure environment variables for your Hetzner deployment.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Copy the example file:**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. **Edit the .env file:**
|
||||
```bash
|
||||
nano .env
|
||||
```
|
||||
|
||||
3. **Fill in your values** (see below)
|
||||
|
||||
## Required Variables
|
||||
|
||||
### `DOMAIN`
|
||||
Your website domain name.
|
||||
```
|
||||
DOMAIN=mintel.me
|
||||
```
|
||||
|
||||
### `ADMIN_EMAIL`
|
||||
Email for SSL certificate notifications.
|
||||
```
|
||||
ADMIN_EMAIL=admin@mintel.me
|
||||
```
|
||||
|
||||
## Optional Variables
|
||||
|
||||
### `REDIS_URL`
|
||||
Connection string for Redis cache.
|
||||
- **Default**: `redis://redis:6379`
|
||||
- **Format**: `redis://host:port`
|
||||
- **Example**: `redis://redis:6379`
|
||||
|
||||
### `PLAUSIBLE_DOMAIN`
|
||||
Domain for Plausible analytics tracking.
|
||||
- **Default**: Same as `DOMAIN`
|
||||
- **Example**: `mintel.me`
|
||||
|
||||
### `PLAUSIBLE_SCRIPT_URL`
|
||||
URL to your Plausible analytics script.
|
||||
- **Default**: `https://plausible.yourdomain.com/js/script.js`
|
||||
- **Example**: `https://analytics.mintel.me/js/script.js`
|
||||
|
||||
## Woodpecker CI/CD Variables
|
||||
|
||||
These are only needed if using Woodpecker for automated deployment:
|
||||
|
||||
### `DEPLOY_HOST`
|
||||
Hetzner server IP address or hostname.
|
||||
```
|
||||
DEPLOY_HOST=123.45.67.89
|
||||
```
|
||||
|
||||
### `DEPLOY_USER`
|
||||
User for SSH access (usually root).
|
||||
```
|
||||
DEPLOY_USER=root
|
||||
```
|
||||
|
||||
### `SSH_PRIVATE_KEY`
|
||||
Private key for SSH authentication.
|
||||
```
|
||||
SSH_PRIVATE_KEY=-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
...
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
```
|
||||
|
||||
**Generate SSH key if needed:**
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -C "woodpecker@mintel.me"
|
||||
ssh-copy-id root@YOUR_HETZNER_IP
|
||||
```
|
||||
|
||||
### `SLACK_WEBHOOK` (Optional)
|
||||
Slack webhook for deployment notifications.
|
||||
```
|
||||
SLACK_WEBHOOK=https://hooks.slack.com/services/YOUR/WEBHOOK/URL
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Local Development
|
||||
```bash
|
||||
# Create .env file
|
||||
cp .env.example .env
|
||||
|
||||
# Edit with your values
|
||||
nano .env
|
||||
|
||||
# Start services
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
### Production Deployment
|
||||
```bash
|
||||
# On Hetzner server
|
||||
cd /opt/mintel
|
||||
|
||||
# Create .env file
|
||||
cat > .env << EOF
|
||||
DOMAIN=mintel.me
|
||||
ADMIN_EMAIL=admin@mintel.me
|
||||
REDIS_URL=redis://redis:6379
|
||||
PLAUSIBLE_DOMAIN=mintel.me
|
||||
PLAUSIBLE_SCRIPT_URL=https://analytics.mintel.me/js/script.js
|
||||
EOF
|
||||
|
||||
# Deploy
|
||||
./deploy.sh
|
||||
```
|
||||
|
||||
### Woodpecker CI/CD
|
||||
1. Go to your Gitea repository
|
||||
2. Navigate to Settings → Secrets
|
||||
3. Add these secrets:
|
||||
- `DEPLOY_HOST` - Your Hetzner IP
|
||||
- `DEPLOY_USER` - Usually `root`
|
||||
- `SSH_PRIVATE_KEY` - Private key content
|
||||
- `DOMAIN` - Your domain
|
||||
- `ADMIN_EMAIL` - Your email
|
||||
- (Optional) `SLACK_WEBHOOK`
|
||||
|
||||
## Security Notes
|
||||
|
||||
- **Never commit `.env` file** to git (it's in `.gitignore`)
|
||||
- **Keep SSH keys secure** and never share
|
||||
- **Use strong passwords** for all services
|
||||
- **Enable firewall** on Hetzner server
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Variables not loading
|
||||
```bash
|
||||
# Check if .env file exists
|
||||
ls -la .env
|
||||
|
||||
# Check file permissions
|
||||
chmod 600 .env
|
||||
|
||||
# Verify variables
|
||||
cat .env
|
||||
```
|
||||
|
||||
### Docker Compose not using .env
|
||||
```bash
|
||||
# Explicitly specify env file
|
||||
docker-compose --env-file .env up
|
||||
|
||||
# Or check if it's being loaded
|
||||
docker-compose config
|
||||
```
|
||||
|
||||
### Woodpecker secrets not working
|
||||
1. Verify secret names match exactly
|
||||
2. Check repository settings
|
||||
3. Restart Woodpecker agent
|
||||
4. Check Woodpecker logs
|
||||
|
||||
## Environment-Specific Configurations
|
||||
|
||||
### Development
|
||||
```bash
|
||||
# .env
|
||||
DOMAIN=localhost:3000
|
||||
ADMIN_EMAIL=dev@localhost
|
||||
REDIS_URL=redis://localhost:6379
|
||||
```
|
||||
|
||||
### Staging
|
||||
```bash
|
||||
# .env
|
||||
DOMAIN=staging.mintel.me
|
||||
ADMIN_EMAIL=staging@mintel.me
|
||||
REDIS_URL=redis://redis:6379
|
||||
```
|
||||
|
||||
### Production
|
||||
```bash
|
||||
# .env
|
||||
DOMAIN=mintel.me
|
||||
ADMIN_EMAIL=admin@mintel.me
|
||||
REDIS_URL=redis://redis:6379
|
||||
PLAUSIBLE_DOMAIN=mintel.me
|
||||
PLAUSIBLE_SCRIPT_URL=https://analytics.mintel.me/js/script.js
|
||||
```
|
||||
|
||||
## Available Variables Reference
|
||||
|
||||
| Variable | Required | Default | Description |
|
||||
|----------|----------|---------|-------------|
|
||||
| `DOMAIN` | ✅ Yes | - | Website domain |
|
||||
| `ADMIN_EMAIL` | ✅ Yes | - | SSL contact email |
|
||||
| `REDIS_URL` | ❌ No | `redis://redis:6379` | Redis connection |
|
||||
| `PLAUSIBLE_DOMAIN` | ❌ No | Same as `DOMAIN` | Analytics domain |
|
||||
| `PLAUSIBLE_SCRIPT_URL` | ❌ No | Plausible default | Analytics script URL |
|
||||
| `DEPLOY_HOST` | CI Only | - | Hetzner server IP |
|
||||
| `DEPLOY_USER` | CI Only | `root` | SSH user |
|
||||
| `SSH_PRIVATE_KEY` | CI Only | - | SSH private key |
|
||||
| `SLACK_WEBHOOK` | ❌ No | - | Slack notifications |
|
||||
|
||||
---
|
||||
|
||||
**Next**: Run `./deploy.sh` or push to trigger CI/CD deployment!
|
||||
104
apps/web/README.md
Normal file
104
apps/web/README.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# Technical Problem Solver's Blog
|
||||
|
||||
A clean, readable blog built with Astro and React. Focus on practical insights and learning notes.
|
||||
|
||||
## Features
|
||||
|
||||
- **Clean Architecture**: Modular React components for article elements
|
||||
- **TypeScript**: Full type safety throughout
|
||||
- **MDX Support**: Write content with JSX components
|
||||
- **Fast**: Static site generation with Astro
|
||||
- **Readable**: Carefully designed typography and spacing
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/ # Modular React components
|
||||
│ ├── ArticleHeading.tsx
|
||||
│ ├── ArticleParagraph.tsx
|
||||
│ ├── ArticleList.tsx
|
||||
│ ├── ArticleBlockquote.tsx
|
||||
│ ├── BlogPostCard.tsx
|
||||
│ └── Container.tsx
|
||||
├── data/
|
||||
│ └── blogPosts.ts # Blog post metadata
|
||||
├── layouts/ # Astro layouts
|
||||
│ ├── BaseLayout.astro
|
||||
│ └── BlogLayout.astro
|
||||
└── pages/ # Routes
|
||||
├── index.astro # Home page
|
||||
└── blog/ # Blog posts
|
||||
├── first-note.astro
|
||||
└── debugging-tips.astro
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Start dev server
|
||||
npm run dev
|
||||
|
||||
# Build for production
|
||||
npm run build
|
||||
|
||||
# Preview production build
|
||||
npm run preview
|
||||
```
|
||||
|
||||
## Writing Posts
|
||||
|
||||
1. Add post metadata to `src/data/blogPosts.ts`
|
||||
2. Create a new `.astro` file in `src/pages/blog/`
|
||||
3. Import and use the modular components
|
||||
4. The post will automatically appear on the home page
|
||||
|
||||
## Components
|
||||
|
||||
The blog uses modular React components for article elements:
|
||||
|
||||
- `H1`, `H2`, `H3` - Headings with consistent styling
|
||||
- `Paragraph`, `LeadParagraph` - Text with proper spacing
|
||||
- `UL`, `OL`, `LI` - Lists with proper indentation
|
||||
- `Blockquote`, `CodeBlock`, `InlineCode` - Code and quotes
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
- **Readability First**: Clear typography, generous spacing
|
||||
- **No Distractions**: Clean layout, focus on content
|
||||
- **Practical**: Tools and techniques that work
|
||||
- **Calm**: No hype, just facts
|
||||
|
||||
## 🎥 UI Showcase Toolkit
|
||||
A Remotion-based toolkit for creating high-quality, deterministic showcase videos of our React components.
|
||||
|
||||
- **Source of Truth**: Renders real project components.
|
||||
- **Automated**: CLI-driven rendering for LinkedIn-ready videos.
|
||||
- **Pixel-Perfect**: 60fps, crisp text, brand-aligned style.
|
||||
|
||||
### Quick Start
|
||||
```bash
|
||||
# Preview compositions
|
||||
npm run video:preview
|
||||
|
||||
# Render the showcase video
|
||||
npm run video:render
|
||||
```
|
||||
See `.agent/workflows/video-toolkit.md` for more details.
|
||||
|
||||
## 🤖 Industrial AI Estimation System
|
||||
A multi-pass AI consultation engine for generating technical project estimations and professional PDFs.
|
||||
|
||||
- **Context-Aware**: Crawls existing customer websites to understand company DNA.
|
||||
- **Industrial Standards**: Strictly follows "Industrial Engineering" aesthetics and technical wording.
|
||||
- **Transparent**: Maps scope directly to a modular pricing model.
|
||||
|
||||
### Quick Start
|
||||
```bash
|
||||
# Generate estimation with website crawl
|
||||
npm run ai-estimate -- "Project Briefing" --url https://example.com
|
||||
```
|
||||
See [ESTIMATION_GUIDE.md](docs/ESTIMATION_GUIDE.md) for full documentation.
|
||||
305
apps/web/app/about/page.tsx
Normal file
305
apps/web/app/about/page.tsx
Normal file
@@ -0,0 +1,305 @@
|
||||
import * as React from 'react';
|
||||
import Image from 'next/image';
|
||||
import { PageHeader } from '../../src/components/PageHeader';
|
||||
import { Section } from '../../src/components/Section';
|
||||
import { Reveal } from '../../src/components/Reveal';
|
||||
import {
|
||||
ExperienceIllustration,
|
||||
ResponsibilityIllustration,
|
||||
ResultIllustration,
|
||||
ConceptSystem,
|
||||
ConceptTarget,
|
||||
ContactIllustration,
|
||||
HeroLines,
|
||||
ParticleNetwork,
|
||||
GridLines
|
||||
} from '../../src/components/Landing';
|
||||
import { Check } from 'lucide-react';
|
||||
import { H3, H4, LeadText, BodyText, Label, MonoLabel } from '../../src/components/Typography';
|
||||
import { BackgroundGrid, Card, Container } from '../../src/components/Layout';
|
||||
import { Button } from '../../src/components/Button';
|
||||
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
<div className="flex flex-col bg-white overflow-hidden relative">
|
||||
{/* Background Elements */}
|
||||
<ParticleNetwork className="opacity-20" />
|
||||
<BackgroundGrid />
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative pt-32 pb-24 overflow-hidden border-b border-slate-50">
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-full h-full opacity-10 pointer-events-none">
|
||||
<HeroLines className="w-full h-full" />
|
||||
</div>
|
||||
<div className="absolute right-0 top-0 w-96 h-96 opacity-5 pointer-events-none">
|
||||
<GridLines />
|
||||
</div>
|
||||
|
||||
<Container variant="narrow" className="relative z-10">
|
||||
<div className="flex flex-col items-center text-center space-y-12">
|
||||
<Reveal>
|
||||
<div className="relative">
|
||||
{/* Structural rings around avatar */}
|
||||
<div className="absolute inset-0 -m-8 border border-slate-100 rounded-full animate-[spin_30s_linear_infinite] opacity-50" />
|
||||
<div className="absolute inset-0 -m-4 border border-slate-200 rounded-full animate-[spin_20s_linear_infinite_reverse] opacity-30" />
|
||||
|
||||
<div className="relative w-32 h-32 md:w-40 md:h-40 rounded-full overflow-hidden border border-slate-200 shadow-xl bg-white p-1 group">
|
||||
<div className="w-full h-full rounded-full overflow-hidden">
|
||||
<img
|
||||
src="/header.webp"
|
||||
alt="Marc Mintel"
|
||||
className="w-full h-full object-cover grayscale transition-all duration-1000 ease-in-out scale-110 group-hover:scale-100 group-hover:grayscale-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<div className="space-y-6 max-w-3xl">
|
||||
<Reveal delay={0.1}>
|
||||
<div className="flex items-center justify-center gap-4 mb-4">
|
||||
<div className="h-px w-8 bg-slate-900"></div>
|
||||
<MonoLabel className="text-slate-900">Digital Architect</MonoLabel>
|
||||
<div className="h-px w-8 bg-slate-900"></div>
|
||||
</div>
|
||||
</Reveal>
|
||||
<PageHeader
|
||||
title={<>Über <span className="text-slate-200">mich.</span></>}
|
||||
description="Warum ich tue, was ich tue – und wie Sie davon profitieren."
|
||||
className="pt-0 md:pt-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
{/* Connector to first section */}
|
||||
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-px h-16 bg-gradient-to-b from-transparent to-slate-200" />
|
||||
</section>
|
||||
|
||||
{/* Section 01: Experience */}
|
||||
<Section
|
||||
number="01"
|
||||
title="Erfahrung"
|
||||
borderTop
|
||||
illustration={<ExperienceIllustration className="w-24 h-24" />}
|
||||
>
|
||||
<div className="space-y-12">
|
||||
<Reveal>
|
||||
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
|
||||
15 Jahre Web-Entwicklung. <br />
|
||||
<span className="text-slate-200">Vom Designer zum Architekten.</span>
|
||||
</H3>
|
||||
</Reveal>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
|
||||
<Reveal delay={0.1}>
|
||||
<div className="space-y-8">
|
||||
<LeadText className="text-xl md:text-2xl text-slate-400">
|
||||
Ich habe Agenturen, Konzerne und Startups von innen gesehen. Dabei habe ich gelernt, what really counts: <span className="text-slate-900">Ergebnisse, nicht Prozesse.</span>
|
||||
</LeadText>
|
||||
<ul className="space-y-4">
|
||||
{[
|
||||
'Komplexe Systeme vereinfacht',
|
||||
'Performance-Probleme gelöst',
|
||||
'Nachhaltige Software-Architekturen gebaut'
|
||||
].map((item, i) => (
|
||||
<li key={i} className="flex items-center gap-4 group">
|
||||
<div className="w-1.5 h-1.5 bg-slate-900 rounded-full group-hover:scale-150 transition-transform" />
|
||||
<BodyText className="text-lg">{item}</BodyText>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</Reveal>
|
||||
<Reveal delay={0.2}>
|
||||
<Card variant="gray" hover={false} padding="normal" className="group">
|
||||
<H4 className="text-2xl mb-6">Mein Fokus heute: Direkte Zusammenarbeit ohne Reibungsverluste.</H4>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{['Effizient', 'Pragmatisch', 'Verlässlich'].map((tag, i) => (
|
||||
<span key={i} className="px-4 py-2 bg-white border border-slate-200 rounded-full shadow-sm">
|
||||
<Label className="text-slate-900">{tag}</Label>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Section 02: Responsibility */}
|
||||
<Section
|
||||
number="02"
|
||||
title="Verantwortung"
|
||||
variant="gray"
|
||||
borderTop
|
||||
illustration={<ResponsibilityIllustration className="w-24 h-24" />}
|
||||
>
|
||||
<div className="space-y-12">
|
||||
<Reveal>
|
||||
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
|
||||
Ich stehe für meine <br />
|
||||
<span className="text-slate-200">Arbeit gerade.</span>
|
||||
</H3>
|
||||
</Reveal>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-12 items-center">
|
||||
<div className="md:col-span-8 space-y-8">
|
||||
<Reveal delay={0.1}>
|
||||
<LeadText className="text-xl md:text-2xl text-slate-400">
|
||||
In der klassischen Agenturwelt verschwindet Verantwortung oft hinter Hierarchien. Bei mir gibt es nur <span className="text-slate-900">einen Ansprechpartner:</span> Mich.
|
||||
</LeadText>
|
||||
</Reveal>
|
||||
<Reveal delay={0.2}>
|
||||
<Card variant="white" padding="normal" className="flex flex-row items-start gap-6 group">
|
||||
<div className="w-12 h-12 bg-slate-900 text-white rounded-xl flex items-center justify-center shrink-0 font-bold text-xl group-hover:rotate-12 transition-transform duration-500">!</div>
|
||||
<BodyText className="text-slate-900 font-medium text-lg md:text-xl leading-relaxed">
|
||||
Ich übernehme die volle Verantwortung für die technische Umsetzung und Qualität Ihres Projekts. Ohne Ausreden.
|
||||
</BodyText>
|
||||
</Card>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Section 03: Systems */}
|
||||
<Section
|
||||
number="03"
|
||||
title="Philosophie"
|
||||
borderTop
|
||||
illustration={<ConceptSystem className="w-24 h-24" />}
|
||||
>
|
||||
<div className="space-y-16">
|
||||
<Reveal>
|
||||
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
|
||||
Nachhaltigkeit durch <br />
|
||||
<span className="text-slate-200">sauberen Code.</span>
|
||||
</H3>
|
||||
</Reveal>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
|
||||
<div className="space-y-8">
|
||||
<Reveal delay={0.1}>
|
||||
<LeadText className="text-xl text-slate-400">
|
||||
Ich baue keine Wegwerf-Produkte. Meine Systeme sind so konzipiert, dass sie mit Ihrem Unternehmen <span className="text-slate-900">wachsen können.</span>
|
||||
</LeadText>
|
||||
</Reveal>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{['Skalierbar', 'Wartbar', 'Performant', 'Sicher', 'Unabhängig', 'Zukunftssicher'].map((item, i) => (
|
||||
<Reveal key={i} delay={0.2 + i * 0.05}>
|
||||
<div className="flex items-center gap-3 group">
|
||||
<div className="w-5 h-5 rounded-full bg-slate-50 flex items-center justify-center group-hover:bg-slate-900 transition-colors duration-500">
|
||||
<Check className="w-2.5 h-2.5 text-slate-400 group-hover:text-white" />
|
||||
</div>
|
||||
<Label className="text-slate-900">{item}</Label>
|
||||
</div>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Reveal delay={0.3}>
|
||||
<Card variant="dark" padding="normal" className="relative rounded-2xl overflow-hidden group">
|
||||
<div className="absolute top-0 right-0 w-48 h-48 bg-white/5 -translate-y-24 translate-x-24 rounded-full blur-3xl group-hover:bg-white/10 transition-colors duration-1000" />
|
||||
<H4 className="text-white text-2xl mb-6 relative z-10">Kein Vendor Lock-in.</H4>
|
||||
<LeadText className="text-slate-400 text-lg relative z-10 leading-relaxed">
|
||||
Sie behalten die volle Kontrolle über Ihren Code und Ihre Daten. Keine Abhängigkeit von proprietären Systemen.
|
||||
</LeadText>
|
||||
</Card>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Section 04: Result */}
|
||||
<Section
|
||||
number="04"
|
||||
title="Ergebnis"
|
||||
borderTop
|
||||
illustration={<ResultIllustration className="w-24 h-24" />}
|
||||
>
|
||||
<div className="space-y-16">
|
||||
<Reveal>
|
||||
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
|
||||
Was Sie von mir <br />
|
||||
<span className="text-slate-200">erwarten können.</span>
|
||||
</H3>
|
||||
</Reveal>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-16">
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-6">
|
||||
<Label>Kein:</Label>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{['Agentur-Zirkus', 'Meeting-Marathon', 'Ticket-Wahnsinn', 'CMS-Frust'].map((item, i) => (
|
||||
<span key={i} className="px-4 py-2 border border-slate-100 rounded-full bg-slate-50/50">
|
||||
<BodyText className="text-slate-400 line-through text-base mb-0">{item}</BodyText>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-6">
|
||||
<Label className="text-slate-900">Sondern:</Label>
|
||||
<div className="space-y-8">
|
||||
{[
|
||||
{ label: 'Direkte Kommunikation', desc: 'Kurze Wege, schnelle Entscheidungen.' },
|
||||
{ label: 'Echte Expertise', desc: 'Fundiertes Wissen aus 15 Jahren Praxis.' },
|
||||
{ label: 'Messbare Qualität', desc: 'Code, der hält, was er verspricht.' }
|
||||
].map((item, i) => (
|
||||
<Reveal key={i} delay={0.2 + i * 0.1}>
|
||||
<div className="flex gap-6 items-start group">
|
||||
<div className="w-8 h-8 rounded-full bg-slate-900 flex items-center justify-center shrink-0 mt-1 group-hover:scale-110 transition-transform">
|
||||
<Check className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<H4 className="text-xl">{item.label}</H4>
|
||||
<BodyText className="text-base text-slate-400">{item.desc}</BodyText>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Section 05: Today */}
|
||||
<Section
|
||||
number="05"
|
||||
title="Kontakt"
|
||||
variant="gray"
|
||||
borderTop
|
||||
illustration={<ContactIllustration className="w-24 h-24" />}
|
||||
>
|
||||
<div className="space-y-12">
|
||||
<Reveal>
|
||||
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
|
||||
Bereit für eine <br />
|
||||
<span className="text-slate-200">Zusammenarbeit?</span>
|
||||
</H3>
|
||||
</Reveal>
|
||||
|
||||
<Card variant="white" hover={false} padding="large" className="rounded-3xl shadow-xl relative overflow-hidden group">
|
||||
<div className="absolute top-0 right-0 w-96 h-96 bg-slate-50 -translate-y-1/2 translate-x-1/2 rounded-full blur-[80px] group-hover:bg-slate-100 transition-colors duration-1000" />
|
||||
|
||||
<div className="relative z-10 space-y-8">
|
||||
<LeadText className="text-2xl md:text-4xl leading-tight max-w-2xl text-slate-400">
|
||||
Lassen Sie uns gemeinsam etwas bauen, das <span className="text-slate-900">wirklich funktioniert.</span>
|
||||
</LeadText>
|
||||
|
||||
<div className="pt-4">
|
||||
<Button href="/contact">
|
||||
Projekt anfragen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
268
apps/web/app/api/download-zip/route.ts
Normal file
268
apps/web/app/api/download-zip/route.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { FileExampleManager } from '../../../src/data/fileExamples';
|
||||
|
||||
// Simple ZIP creation without external dependencies
|
||||
class SimpleZipCreator {
|
||||
private files: Array<{ filename: string; content: string }> = [];
|
||||
|
||||
addFile(filename: string, content: string) {
|
||||
this.files.push({ filename, content });
|
||||
}
|
||||
|
||||
// Create a basic ZIP file structure
|
||||
create(): number[] {
|
||||
const encoder = new TextEncoder();
|
||||
const chunks: number[][] = [];
|
||||
|
||||
let offset = 0;
|
||||
const centralDirectory: Array<{
|
||||
name: string;
|
||||
offset: number;
|
||||
size: number;
|
||||
compressedSize: number;
|
||||
}> = [];
|
||||
|
||||
// Process each file
|
||||
for (const file of this.files) {
|
||||
const contentBytes = Array.from(encoder.encode(file.content));
|
||||
const filenameBytes = Array.from(encoder.encode(file.filename));
|
||||
|
||||
// Local file header
|
||||
const localHeader: number[] = [];
|
||||
|
||||
// Local file header signature (little endian)
|
||||
localHeader.push(0x50, 0x4b, 0x03, 0x04);
|
||||
// Version needed to extract
|
||||
localHeader.push(20, 0);
|
||||
// General purpose bit flag
|
||||
localHeader.push(0, 0);
|
||||
// Compression method (0 = store)
|
||||
localHeader.push(0, 0);
|
||||
// Last modified time/date
|
||||
localHeader.push(0, 0, 0, 0);
|
||||
// CRC32 (0 for simplicity)
|
||||
localHeader.push(0, 0, 0, 0);
|
||||
// Compressed size
|
||||
localHeader.push(...intToLittleEndian(contentBytes.length, 4));
|
||||
// Uncompressed size
|
||||
localHeader.push(...intToLittleEndian(contentBytes.length, 4));
|
||||
// Filename length
|
||||
localHeader.push(...intToLittleEndian(filenameBytes.length, 2));
|
||||
// Extra field length
|
||||
localHeader.push(0, 0);
|
||||
|
||||
// Add filename
|
||||
localHeader.push(...filenameBytes);
|
||||
|
||||
chunks.push(localHeader);
|
||||
chunks.push(contentBytes);
|
||||
|
||||
// Store info for central directory
|
||||
centralDirectory.push({
|
||||
name: file.filename,
|
||||
offset: offset,
|
||||
size: contentBytes.length,
|
||||
compressedSize: contentBytes.length
|
||||
});
|
||||
|
||||
offset += localHeader.length + contentBytes.length;
|
||||
}
|
||||
|
||||
// Central directory
|
||||
const centralDirectoryChunks: number[][] = [];
|
||||
let centralDirectoryOffset = offset;
|
||||
|
||||
for (const entry of centralDirectory) {
|
||||
const filenameBytes = Array.from(encoder.encode(entry.name));
|
||||
const centralHeader: number[] = [];
|
||||
|
||||
// Central directory header signature
|
||||
centralHeader.push(0x50, 0x4b, 0x01, 0x02);
|
||||
// Version made by
|
||||
centralHeader.push(20, 0);
|
||||
// Version needed to extract
|
||||
centralHeader.push(20, 0);
|
||||
// General purpose bit flag
|
||||
centralHeader.push(0, 0);
|
||||
// Compression method
|
||||
centralHeader.push(0, 0);
|
||||
// Last modified time/date
|
||||
centralHeader.push(0, 0, 0, 0);
|
||||
// CRC32
|
||||
centralHeader.push(0, 0, 0, 0);
|
||||
// Compressed size
|
||||
centralHeader.push(...intToLittleEndian(entry.compressedSize, 4));
|
||||
// Uncompressed size
|
||||
centralHeader.push(...intToLittleEndian(entry.size, 4));
|
||||
// Filename length
|
||||
centralHeader.push(...intToLittleEndian(filenameBytes.length, 2));
|
||||
// Extra field length
|
||||
centralHeader.push(0, 0);
|
||||
// File comment length
|
||||
centralHeader.push(0, 0);
|
||||
// Disk number start
|
||||
centralHeader.push(0, 0);
|
||||
// Internal file attributes
|
||||
centralHeader.push(0, 0);
|
||||
// External file attributes
|
||||
centralHeader.push(0, 0, 0, 0);
|
||||
// Relative offset of local header
|
||||
centralHeader.push(...intToLittleEndian(entry.offset, 4));
|
||||
|
||||
// Add filename
|
||||
centralHeader.push(...filenameBytes);
|
||||
|
||||
centralDirectoryChunks.push(centralHeader);
|
||||
}
|
||||
|
||||
const centralDirectorySize = centralDirectoryChunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
||||
|
||||
// End of central directory
|
||||
const endOfCentralDirectory: number[] = [];
|
||||
|
||||
// End of central directory signature
|
||||
endOfCentralDirectory.push(0x50, 0x4b, 0x05, 0x06);
|
||||
// Number of this disk
|
||||
endOfCentralDirectory.push(0, 0);
|
||||
// Number of the disk with the start of the central directory
|
||||
endOfCentralDirectory.push(0, 0);
|
||||
// Total number of entries on this disk
|
||||
endOfCentralDirectory.push(...intToLittleEndian(centralDirectory.length, 2));
|
||||
// Total number of entries
|
||||
endOfCentralDirectory.push(...intToLittleEndian(centralDirectory.length, 2));
|
||||
// Size of the central directory
|
||||
endOfCentralDirectory.push(...intToLittleEndian(centralDirectorySize, 4));
|
||||
// Offset of start of central directory
|
||||
endOfCentralDirectory.push(...intToLittleEndian(centralDirectoryOffset, 4));
|
||||
// ZIP file comment length
|
||||
endOfCentralDirectory.push(0, 0);
|
||||
|
||||
// Combine all chunks
|
||||
const result: number[] = [];
|
||||
chunks.forEach(chunk => result.push(...chunk));
|
||||
centralDirectoryChunks.forEach(chunk => result.push(...chunk));
|
||||
result.push(...endOfCentralDirectory);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to convert integer to little endian bytes
|
||||
function intToLittleEndian(value: number, bytes: number): number[] {
|
||||
const result: number[] = [];
|
||||
for (let i = 0; i < bytes; i++) {
|
||||
result.push((value >> (i * 8)) & 0xff);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { fileIds } = body;
|
||||
|
||||
if (!Array.isArray(fileIds) || fileIds.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'fileIds array is required and must not be empty' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get file contents
|
||||
const files = await Promise.all(
|
||||
fileIds.map(async (id) => {
|
||||
const file = await FileExampleManager.getFileExample(id);
|
||||
if (!file) {
|
||||
throw new Error(`File with id ${id} not found`);
|
||||
}
|
||||
return file;
|
||||
})
|
||||
);
|
||||
|
||||
// Create ZIP
|
||||
const zipCreator = new SimpleZipCreator();
|
||||
files.forEach(file => {
|
||||
zipCreator.addFile(file.filename, file.content);
|
||||
});
|
||||
|
||||
const zipData = zipCreator.create();
|
||||
const buffer = Buffer.from(new Uint8Array(zipData));
|
||||
|
||||
// Return ZIP file
|
||||
return new Response(buffer, {
|
||||
headers: {
|
||||
'Content-Type': 'application/zip',
|
||||
'Content-Disposition': `attachment; filename="code-examples-${Date.now()}.zip"`,
|
||||
'Cache-Control': 'no-cache',
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('ZIP download error:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to create zip file', details: errorMessage },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const fileId = searchParams.get('id');
|
||||
|
||||
if (!fileId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'id parameter is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const file = await FileExampleManager.getFileExample(fileId);
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json(
|
||||
{ error: 'File not found' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const content = encoder.encode(file.content);
|
||||
const buffer = Buffer.from(content);
|
||||
|
||||
return new Response(buffer, {
|
||||
headers: {
|
||||
'Content-Type': getMimeType(file.language),
|
||||
'Content-Disposition': `attachment; filename="${file.filename}"`,
|
||||
'Cache-Control': 'no-cache',
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('File download error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to download file' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get MIME type
|
||||
function getMimeType(language: string): string {
|
||||
const mimeTypes: Record<string, string> = {
|
||||
'python': 'text/x-python',
|
||||
'typescript': 'text/x-typescript',
|
||||
'javascript': 'text/javascript',
|
||||
'dockerfile': 'text/x-dockerfile',
|
||||
'yaml': 'text/yaml',
|
||||
'json': 'application/json',
|
||||
'html': 'text/html',
|
||||
'css': 'text/css',
|
||||
'sql': 'text/x-sql',
|
||||
'bash': 'text/x-shellscript',
|
||||
'text': 'text/plain'
|
||||
};
|
||||
return mimeTypes[language] || 'text/plain';
|
||||
}
|
||||
89
apps/web/app/api/og/[[...slug]]/route.tsx
Normal file
89
apps/web/app/api/og/[[...slug]]/route.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { blogPosts } from '../../../../src/data/blogPosts';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ slug?: string[] }> }
|
||||
) {
|
||||
const { slug: slugArray } = await params;
|
||||
const slug = slugArray?.[0] || 'home';
|
||||
|
||||
let title: string;
|
||||
let description: string;
|
||||
|
||||
if (slug === 'home') {
|
||||
title = 'Marc Mintel';
|
||||
description = 'Technical problem solver\'s blog - practical insights and learning notes';
|
||||
} else {
|
||||
const post = blogPosts.find(p => p.slug === slug);
|
||||
title = post?.title || 'Marc Mintel';
|
||||
description = (post?.description || 'Technical problem solver\'s blog - practical insights and learning notes').slice(0, 100);
|
||||
}
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#fff',
|
||||
padding: '60px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '60px',
|
||||
right: '60px',
|
||||
width: '120px',
|
||||
height: '4px',
|
||||
backgroundColor: '#3b82f6',
|
||||
borderRadius: '2px',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '48px',
|
||||
fontWeight: 700,
|
||||
color: '#1e293b',
|
||||
marginBottom: '20px',
|
||||
fontFamily: 'sans-serif',
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '24px',
|
||||
fontWeight: 400,
|
||||
color: '#64748b',
|
||||
marginBottom: 'auto',
|
||||
fontFamily: 'sans-serif',
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '18px',
|
||||
fontWeight: 500,
|
||||
color: '#94a3b8',
|
||||
fontFamily: 'sans-serif',
|
||||
}}
|
||||
>
|
||||
mintel.me
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
}
|
||||
);
|
||||
}
|
||||
199
apps/web/app/blog/[slug]/page.tsx
Normal file
199
apps/web/app/blog/[slug]/page.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
import * as React from 'react';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { blogPosts } from '../../../src/data/blogPosts';
|
||||
import { Tag } from '../../../src/components/Tag';
|
||||
import { CodeBlock } from '../../../src/components/ArticleBlockquote';
|
||||
import { H2 } from '../../../src/components/ArticleHeading';
|
||||
import { Paragraph, LeadParagraph } from '../../../src/components/ArticleParagraph';
|
||||
import { UL, LI } from '../../../src/components/ArticleList';
|
||||
import { FileExamplesList } from '../../../src/components/FileExamplesList';
|
||||
import { FileExampleManager } from '../../../src/data/fileExamples';
|
||||
import { BlogPostClient } from '../../../src/components/BlogPostClient';
|
||||
import { PageHeader } from '../../../src/components/PageHeader';
|
||||
import { Section } from '../../../src/components/Section';
|
||||
import { Reveal } from '../../../src/components/Reveal';
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return blogPosts.map((post) => ({
|
||||
slug: post.slug,
|
||||
}));
|
||||
}
|
||||
|
||||
export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = await params;
|
||||
const post = blogPosts.find((p) => p.slug === slug);
|
||||
|
||||
if (!post) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const formattedDate = new Date(post.date).toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
});
|
||||
|
||||
const wordCount = post.description.split(/\s+/).length + 100;
|
||||
const readingTime = Math.max(1, Math.ceil(wordCount / 200));
|
||||
|
||||
const showFileExamples = post.tags?.some(tag =>
|
||||
['architecture', 'design-patterns', 'system-design', 'docker', 'deployment'].includes(tag)
|
||||
);
|
||||
|
||||
// Load file examples for the post
|
||||
let groups: any[] = [];
|
||||
if (showFileExamples) {
|
||||
const allGroups = await FileExampleManager.getAllGroups();
|
||||
groups = allGroups
|
||||
.map((group) => ({
|
||||
...group,
|
||||
files: group.files.filter((file) => {
|
||||
if (file.postSlug !== slug) return false;
|
||||
return true;
|
||||
}),
|
||||
}))
|
||||
.filter((group) => group.files.length > 0);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-24 py-12 md:py-24 overflow-hidden">
|
||||
<BlogPostClient readingTime={readingTime} title={post.title} />
|
||||
|
||||
<PageHeader
|
||||
title={post.title}
|
||||
description={post.description}
|
||||
backLink={{ href: '/blog', label: 'Zurück zum Blog' }}
|
||||
backgroundSymbol="B"
|
||||
/>
|
||||
|
||||
<main id="post-content">
|
||||
<Section number="01" title="Inhalt">
|
||||
<div className="prose prose-slate max-w-none">
|
||||
<div className="flex flex-wrap items-center gap-4 text-[10px] font-bold text-slate-400 mb-12 uppercase tracking-[0.2em]">
|
||||
<time dateTime={post.date}>{formattedDate}</time>
|
||||
<span className="text-slate-200">•</span>
|
||||
<span>{readingTime} min read</span>
|
||||
</div>
|
||||
|
||||
{post.tags && post.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mb-12">
|
||||
{post.tags.map((tag, index) => (
|
||||
<Tag key={tag} tag={tag} index={index} className="text-xs" />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{slug === 'first-note' && (
|
||||
<>
|
||||
<LeadParagraph>
|
||||
This blog is a public notebook. It's where I document things I learn, problems I solve, and tools I test.
|
||||
</LeadParagraph>
|
||||
<H2>Why write in public?</H2>
|
||||
<Paragraph>
|
||||
I forget things. Writing them down helps. Making them public helps me think more clearly and might help someone else.
|
||||
</Paragraph>
|
||||
<H2>What to expect</H2>
|
||||
<UL>
|
||||
<LI>Short entries, usually under 500 words</LI>
|
||||
<LI>Practical solutions to specific problems</LI>
|
||||
<LI>Notes on tools and workflows</LI>
|
||||
<LI>Mistakes and what I learned</LI>
|
||||
</UL>
|
||||
</>
|
||||
)}
|
||||
|
||||
{slug === 'debugging-tips' && (
|
||||
<>
|
||||
<LeadParagraph>
|
||||
Sometimes the simplest debugging tool is the best one. Print statements get a bad reputation, but they're often exactly what you need.
|
||||
</LeadParagraph>
|
||||
<H2>Why print statements work</H2>
|
||||
<Paragraph>
|
||||
Debuggers are powerful, but they change how your code runs. Print statements don't.
|
||||
</Paragraph>
|
||||
<CodeBlock language="python" showLineNumbers={true}>
|
||||
{`def process_data(data):
|
||||
print(f"Processing {len(data)} items")
|
||||
result = expensive_operation(data)
|
||||
print(f"Operation result: {result}")
|
||||
return result`}
|
||||
</CodeBlock>
|
||||
|
||||
<H2>Complete examples</H2>
|
||||
<Paragraph>
|
||||
Here are some practical file examples you can copy and download. These include proper error handling and logging.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<FileExamplesList groups={groups} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{slug === 'architecture-patterns' && (
|
||||
<>
|
||||
<LeadParagraph>
|
||||
Good software architecture is about making the right decisions early. Here are some patterns I've found useful in production systems.
|
||||
</LeadParagraph>
|
||||
<H2>Repository Pattern</H2>
|
||||
<Paragraph>
|
||||
The repository pattern provides a clean separation between your business logic and data access layer. It makes your code more testable and maintainable.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Service Layer</H2>
|
||||
<Paragraph>
|
||||
Services orchestrate business logic and coordinate between repositories and domain events. They keep your controllers thin and your business rules organized.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Domain Events</H2>
|
||||
<Paragraph>
|
||||
Domain events help you decouple components and react to changes in your system. They're essential for building scalable, event-driven architectures.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Complete examples</H2>
|
||||
<Paragraph>
|
||||
These TypeScript examples demonstrate modern architecture patterns for scalable applications. You can copy them directly into your project.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<FileExamplesList groups={groups} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{slug === 'docker-deployment' && (
|
||||
<>
|
||||
<LeadParagraph>
|
||||
Docker has become the standard for containerizing applications. Here's how to set up production-ready deployments that are secure, efficient, and maintainable.
|
||||
</LeadParagraph>
|
||||
<H2>Multi-stage builds</H2>
|
||||
<Paragraph>
|
||||
Multi-stage builds keep your production images small and secure by separating build and runtime environments. This reduces attack surface and speeds up deployments.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Health checks and monitoring</H2>
|
||||
<Paragraph>
|
||||
Proper health checks ensure your containers are running correctly. Combined with restart policies, this gives you resilient, self-healing deployments.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Orchestration with Docker Compose</H2>
|
||||
<Paragraph>
|
||||
Docker Compose makes it easy to manage multi-service applications in development and production. Define services, networks, and volumes in a single file.
|
||||
</Paragraph>
|
||||
|
||||
<H2>Complete examples</H2>
|
||||
<Paragraph>
|
||||
These Docker configurations are production-ready. Use them as a starting point for your own deployments.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<FileExamplesList groups={groups} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
242
apps/web/app/blog/embed-demo/page.tsx
Normal file
242
apps/web/app/blog/embed-demo/page.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
import React from 'react';
|
||||
import { Tag } from '../../../src/components/Tag';
|
||||
import { H2 } from '../../../src/components/ArticleHeading';
|
||||
import { Paragraph, LeadParagraph } from '../../../src/components/ArticleParagraph';
|
||||
import { UL, LI } from '../../../src/components/ArticleList';
|
||||
import { CodeBlock } from '../../../src/components/ArticleBlockquote';
|
||||
import { YouTubeEmbed } from '../../../src/components/YouTubeEmbed';
|
||||
import { TwitterEmbed } from '../../../src/components/TwitterEmbed';
|
||||
import { GenericEmbed } from '../../../src/components/GenericEmbed';
|
||||
import { Mermaid } from '../../../src/components/Mermaid';
|
||||
import { BlogPostClient } from '../../../src/components/BlogPostClient';
|
||||
|
||||
export default function EmbedDemoPage() {
|
||||
const post = {
|
||||
title: "Rich Content Embedding Demo",
|
||||
description: "Testing our new free embed components for YouTube, Twitter, Mermaid diagrams, and other platforms",
|
||||
date: "2024-02-15",
|
||||
slug: "embed-demo",
|
||||
tags: ["embeds", "components", "tutorial", "mermaid"]
|
||||
};
|
||||
|
||||
const formattedDate = new Date(post.date).toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
});
|
||||
|
||||
const readingTime = 5;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
<BlogPostClient readingTime={readingTime} title={post.title} />
|
||||
|
||||
<main id="post-content" className="pt-24">
|
||||
<section className="py-12 md:py-16">
|
||||
<div className="max-w-3xl mx-auto px-6">
|
||||
<div className="text-center">
|
||||
<h1 className="text-3xl md:text-5xl font-serif font-bold text-slate-900 mb-6 leading-tight tracking-tight">
|
||||
{post.title}
|
||||
</h1>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-center gap-4 text-sm text-slate-600 mb-6 font-sans">
|
||||
<time dateTime={post.date} className="flex items-center gap-1.5 px-3 py-1 bg-slate-50 rounded-full">
|
||||
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zM4 8h12v8H4V8z"/>
|
||||
</svg>
|
||||
{formattedDate}
|
||||
</time>
|
||||
<span className="text-slate-400">•</span>
|
||||
<span className="flex items-center gap-1.5 px-3 py-1 bg-slate-50 rounded-full">
|
||||
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clipRule="evenodd"/>
|
||||
</svg>
|
||||
{readingTime} min
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-xl md:text-2xl text-slate-600 leading-relaxed font-serif italic mb-8 max-w-2xl mx-auto">
|
||||
{post.description}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-2 mb-8">
|
||||
{post.tags.map((tag, index) => (
|
||||
<Tag key={tag} tag={tag} index={index} className="text-xs" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="max-w-3xl mx-auto px-6 pb-24">
|
||||
<div className="prose prose-slate max-w-none">
|
||||
<LeadParagraph>
|
||||
This post demonstrates our new free embed components that give you full styling control over YouTube videos, Twitter tweets, and other rich content - all generated at build time.
|
||||
</LeadParagraph>
|
||||
|
||||
<H2>YouTube Embed Example</H2>
|
||||
<Paragraph>
|
||||
Here's a YouTube video embedded with full styling control. The component uses build-time generation for optimal performance.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-6">
|
||||
<YouTubeEmbed
|
||||
videoId="dQw4w9WgXcQ"
|
||||
title="Demo Video"
|
||||
style="minimal"
|
||||
className="my-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
You can customize the appearance using CSS variables or data attributes:
|
||||
</Paragraph>
|
||||
|
||||
<CodeBlock
|
||||
language="jsx"
|
||||
showLineNumbers={true}
|
||||
>
|
||||
{`<YouTubeEmbed
|
||||
videoId="dQw4w9WgXcQ"
|
||||
style="minimal" // 'default' | 'minimal' | 'rounded' | 'flat'
|
||||
aspectRatio="56.25%" // Custom aspect ratio
|
||||
className="my-4" // Additional classes
|
||||
/>`}
|
||||
</CodeBlock>
|
||||
|
||||
<H2>Twitter/X Embed Example</H2>
|
||||
<Paragraph>
|
||||
Twitter embeds use the official Twitter iframe embed for reliable display.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-4">
|
||||
<TwitterEmbed
|
||||
tweetId="20"
|
||||
theme="light"
|
||||
align="center"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CodeBlock
|
||||
language="jsx"
|
||||
showLineNumbers={true}
|
||||
>
|
||||
{`<TwitterEmbed
|
||||
tweetId="20"
|
||||
theme="light" // 'light' | 'dark'
|
||||
align="center" // 'left' | 'center' | 'right'
|
||||
/>`}
|
||||
</CodeBlock>
|
||||
|
||||
<H2>Generic Embed Example</H2>
|
||||
<Paragraph>
|
||||
The generic component supports direct embeds for Vimeo, CodePen, GitHub Gists, and other platforms.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-6">
|
||||
<GenericEmbed
|
||||
url="https://vimeo.com/123456789"
|
||||
type="video"
|
||||
maxWidth="800px"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CodeBlock
|
||||
language="jsx"
|
||||
showLineNumbers={true}
|
||||
>
|
||||
{`<GenericEmbed
|
||||
url="https://vimeo.com/123456789"
|
||||
type="video" // 'video' | 'article' | 'rich'
|
||||
maxWidth="800px"
|
||||
/>`}
|
||||
</CodeBlock>
|
||||
|
||||
<H2>Mermaid Diagrams</H2>
|
||||
<Paragraph>
|
||||
We've added support for Mermaid diagrams! You can now create flowcharts, sequence diagrams, and more using a simple text-based syntax.
|
||||
</Paragraph>
|
||||
|
||||
<div className="my-8">
|
||||
<Mermaid
|
||||
graph={`graph LR
|
||||
A[Client] --> B[Load Balancer]
|
||||
B --> C[App Server 1]
|
||||
B --> D[App Server 2]
|
||||
C --> E[(Database)]
|
||||
D --> E`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Paragraph>
|
||||
Usage is straightforward:
|
||||
</Paragraph>
|
||||
|
||||
<CodeBlock
|
||||
language="jsx"
|
||||
showLineNumbers={true}
|
||||
>
|
||||
{`<Mermaid
|
||||
graph={\`graph LR
|
||||
A[Client] --> B[Load Balancer]
|
||||
B --> C[App Server 1]
|
||||
B --> D[App Server 2]
|
||||
C --> E[(Database)]
|
||||
D --> E\`}
|
||||
/>`}
|
||||
</CodeBlock>
|
||||
|
||||
<H2>Styling Control</H2>
|
||||
<Paragraph>
|
||||
All components use CSS variables for easy customization:
|
||||
</Paragraph>
|
||||
|
||||
<CodeBlock
|
||||
language="css"
|
||||
showLineNumbers={true}
|
||||
>
|
||||
{`.youtube-embed {
|
||||
--aspect-ratio: 56.25%;
|
||||
--bg-color: #000000;
|
||||
--border-radius: 12px;
|
||||
--shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
/* Data attribute variations */
|
||||
.youtube-embed[data-style="minimal"] {
|
||||
--border-radius: 4px;
|
||||
--shadow: none;
|
||||
}`}
|
||||
</CodeBlock>
|
||||
|
||||
<H2>Benefits</H2>
|
||||
<UL>
|
||||
<LI><strong>Free:</strong> No paid services required</LI>
|
||||
<LI><strong>Fast:</strong> Build-time generation, no runtime API calls</LI>
|
||||
<LI><strong>Flexible:</strong> Full styling control via CSS variables</LI>
|
||||
<LI><strong>Self-hosted:</strong> Complete ownership and privacy</LI>
|
||||
<LI><strong>SEO-friendly:</strong> Static HTML content</LI>
|
||||
</UL>
|
||||
|
||||
<H2>Integration</H2>
|
||||
<Paragraph>
|
||||
Simply import the components in your blog posts:
|
||||
</Paragraph>
|
||||
|
||||
<CodeBlock
|
||||
language="jsx"
|
||||
showLineNumbers={true}
|
||||
>
|
||||
{`import { YouTubeEmbed } from '../components/YouTubeEmbed';
|
||||
import { TwitterEmbed } from '../components/TwitterEmbed';
|
||||
import { GenericEmbed } from '../components/GenericEmbed';
|
||||
|
||||
<YouTubeEmbed videoId="abc123" style="rounded" />
|
||||
<TwitterEmbed tweetId="123456789" theme="dark" />`}
|
||||
</CodeBlock>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
106
apps/web/app/blog/page.tsx
Normal file
106
apps/web/app/blog/page.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { MediumCard } from '../../src/components/MediumCard';
|
||||
import { SearchBar } from '../../src/components/SearchBar';
|
||||
import { Tag } from '../../src/components/Tag';
|
||||
import { blogPosts } from '../../src/data/blogPosts';
|
||||
import { PageHeader } from '../../src/components/PageHeader';
|
||||
import { Reveal } from '../../src/components/Reveal';
|
||||
|
||||
export default function BlogPage() {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [filteredPosts, setFilteredPosts] = useState(blogPosts);
|
||||
|
||||
// Sort posts by date
|
||||
const allPosts = [...blogPosts].sort((a, b) =>
|
||||
new Date(b.date).getTime() - new Date(a.date).getTime()
|
||||
);
|
||||
|
||||
// Get unique tags
|
||||
const allTags = Array.from(new Set(allPosts.flatMap(post => post.tags || [])));
|
||||
|
||||
useEffect(() => {
|
||||
const query = searchQuery.toLowerCase().trim();
|
||||
if (query.startsWith('#')) {
|
||||
const tag = query.slice(1);
|
||||
setFilteredPosts(allPosts.filter(post =>
|
||||
post.tags?.some(t => t.toLowerCase() === tag.toLowerCase())
|
||||
));
|
||||
} else {
|
||||
setFilteredPosts(allPosts.filter(post => {
|
||||
const title = post.title.toLowerCase();
|
||||
const description = post.description.toLowerCase();
|
||||
const tags = (post.tags || []).join(' ').toLowerCase();
|
||||
return title.includes(query) || description.includes(query) || tags.includes(query);
|
||||
}));
|
||||
}
|
||||
}, [searchQuery]);
|
||||
|
||||
const filterByTag = (tag: string) => {
|
||||
setSearchQuery(`#${tag}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-24 py-12 md:py-24 overflow-hidden">
|
||||
<PageHeader
|
||||
title={<>Blog <br /><span className="text-slate-200">& Notes.</span></>}
|
||||
description="A public notebook of things I figured out, mistakes I made, and tools I tested."
|
||||
backgroundSymbol="B"
|
||||
/>
|
||||
|
||||
<section className="narrow-container">
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-16">
|
||||
{/* Sidebar / Filter area */}
|
||||
<div className="md:col-span-4">
|
||||
<div className="sticky top-32 space-y-16">
|
||||
<Reveal>
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-[10px] font-bold uppercase tracking-[0.3em] text-slate-400">Suchen</h3>
|
||||
<SearchBar value={searchQuery} onChange={setSearchQuery} />
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{allTags.length > 0 && (
|
||||
<Reveal delay={0.2}>
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-[10px] font-bold uppercase tracking-[0.3em] text-slate-400">Themen</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{allTags.map((tag, index) => (
|
||||
<button
|
||||
key={tag}
|
||||
onClick={() => filterByTag(tag)}
|
||||
className="text-left"
|
||||
>
|
||||
<Tag tag={tag} index={index} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Posts area */}
|
||||
<div className="md:col-span-8">
|
||||
<div id="posts-container" className="flex flex-col gap-8">
|
||||
{filteredPosts.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<p>No posts found matching your criteria.</p>
|
||||
</div>
|
||||
) : (
|
||||
filteredPosts.map((post, i) => (
|
||||
<Reveal key={post.slug} delay={0.1 * i} width="100%">
|
||||
<MediumCard post={post} />
|
||||
</Reveal>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
491
apps/web/app/case-studies/klz-cables/page.tsx
Normal file
491
apps/web/app/case-studies/klz-cables/page.tsx
Normal file
@@ -0,0 +1,491 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { motion, useScroll, useTransform } from 'framer-motion';
|
||||
import { Section } from '../../../src/components/Section';
|
||||
import { Reveal } from '../../../src/components/Reveal';
|
||||
import { H1, H2, H3, LeadText, Label, MonoLabel, BodyText } from '../../../src/components/Typography';
|
||||
import { BackgroundGrid, Container } from '../../../src/components/Layout';
|
||||
import { MotionButton } from '../../../src/components/Button';
|
||||
import { IframeSection } from '../../../src/components/IframeSection';
|
||||
import {
|
||||
Activity,
|
||||
Database,
|
||||
Layout,
|
||||
Users,
|
||||
ArrowRight,
|
||||
Zap,
|
||||
ShieldCheck,
|
||||
Globe2,
|
||||
Settings,
|
||||
Search,
|
||||
Monitor,
|
||||
Cpu,
|
||||
Server,
|
||||
Layers
|
||||
} from 'lucide-react';
|
||||
|
||||
/**
|
||||
* TECHNICAL MARKER COMPONENT
|
||||
* Implements the "hand-drawn marker" effect from STYLEGUIDE.md
|
||||
* Updated: Only yellow marker as requested.
|
||||
*/
|
||||
const Marker: React.FC<{ children: React.ReactNode; delay?: number }> = ({
|
||||
children,
|
||||
delay = 0
|
||||
}) => {
|
||||
return (
|
||||
<span className="relative inline-block px-1">
|
||||
<motion.span
|
||||
initial={{ scaleX: 0, opacity: 0 }}
|
||||
whileInView={{ scaleX: 1, opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 1.2, delay: delay + 0.1, ease: [0.23, 1, 0.32, 1] }}
|
||||
className="absolute inset-0 z-[-1] -skew-x-6 rotate-[-1deg] translate-y-1 transform-gpu bg-[rgba(255,235,59,0.95)] origin-left"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default function KLZCablesCaseStudy() {
|
||||
const { scrollYProgress } = useScroll();
|
||||
const heroY = useTransform(scrollYProgress, [0, 0.2], [0, -20]);
|
||||
const heroOpacity = useTransform(scrollYProgress, [0, 0.15], [1, 0]);
|
||||
const gridRotate = useTransform(scrollYProgress, [0, 1], [0, 2]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col bg-white relative min-h-screen selection:bg-slate-900 selection:text-white overflow-hidden">
|
||||
<motion.div style={{ opacity: heroOpacity }} className="fixed inset-0 z-0 pointer-events-none">
|
||||
<BackgroundGrid />
|
||||
</motion.div>
|
||||
|
||||
{/* --- HERO: INDUSTRIAL INFRASTRUCTURE --- */}
|
||||
<section className="relative min-h-[40vh] py-20 overflow-hidden border-b border-slate-100 bg-white">
|
||||
<motion.div
|
||||
style={{ y: heroY, rotate: gridRotate }}
|
||||
className="absolute inset-0 bg-[linear-gradient(to_right,#f1f5f9_1px,transparent_1px),linear-gradient(to_bottom,#f1f5f9_1px,transparent_1px)] bg-[size:6rem_6rem] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_50%,#000_20%,transparent_100%)] pointer-events-none opacity-40"
|
||||
/>
|
||||
|
||||
<Container variant="narrow" className="relative z-10">
|
||||
<div className="space-y-12">
|
||||
<Reveal direction="down" blur>
|
||||
<div className="inline-flex items-center gap-6">
|
||||
<motion.div
|
||||
initial={{ width: 0 }}
|
||||
whileInView={{ width: 48 }}
|
||||
transition={{ duration: 1, ease: "circOut" }}
|
||||
className="h-px bg-slate-900"
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<MonoLabel className="text-slate-900 tracking-[0.4em]">SYSTEM-ARCHITEKTUR // 2025</MonoLabel>
|
||||
<Label className="text-[10px] text-slate-400 font-mono">HARDENED WORDPRESS // VARNISH STACK</Label>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<div className="space-y-12">
|
||||
<Reveal delay={0.1} direction="up" scale={0.98} blur>
|
||||
<H1 className="text-6xl md:text-8xl tracking-tighter leading-[0.9] font-bold text-slate-900">
|
||||
KLZ Cables<br />
|
||||
<span className="text-slate-100">Case Study.</span>
|
||||
</H1>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={0.2} direction="right" blur>
|
||||
<div className="max-w-3xl border-l-[3px] border-slate-900 pl-8 md:pl-12">
|
||||
<LeadText className="text-2xl md:text-4xl leading-tight text-slate-900 font-medium">
|
||||
Engineering eines <br />
|
||||
<Marker delay={0.2}>B2B Commerce Systems.</Marker>
|
||||
</LeadText>
|
||||
<BodyText className="mt-6 text-lg md:text-xl text-slate-500 max-w-xl leading-relaxed font-serif italic">
|
||||
Vom statischen Altsystem zum industriellen Standard. Ich habe das KLZ-System auf das Wesentliche reduziert: Hardened Infrastructure, parametrische Datenpflege und zero maintenance.
|
||||
</BodyText>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
<Reveal delay={0.4} direction="up" scale={0.98} blur>
|
||||
<div className="flex flex-wrap gap-12 md:gap-24 pt-12 border-t border-slate-100">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-slate-400">Data Integrity</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-2.5 h-2.5 bg-[rgba(129,199,132,1)] rounded-full animate-pulse" />
|
||||
<span className="text-2xl font-bold font-mono text-slate-900 tracking-tight">Relational Data</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-slate-400">Security Layer</Label>
|
||||
<div className="flex items-center gap-3 text-2xl font-bold font-mono text-slate-900">
|
||||
<ShieldCheck className="w-6 h-6 text-[rgba(129,199,132,1)]" />
|
||||
<span>WP + Varnish</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
|
||||
{/* --- SECTION 01: ARCHITECTURE --- */}
|
||||
<Section
|
||||
number="01"
|
||||
title="System-Hardening & Logic"
|
||||
borderBottom
|
||||
containerVariant="normal"
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-16 lg:gap-24 items-start">
|
||||
<div className="md:col-span-12 mb-12">
|
||||
<Reveal direction="left" blur>
|
||||
<H2 className="text-5xl md:text-8xl tracking-tighter mb-12">
|
||||
Architektur- <br />Refactor.
|
||||
</H2>
|
||||
</Reveal>
|
||||
</div>
|
||||
<div className="md:col-span-7 space-y-12">
|
||||
<Reveal delay={0.1} direction="up" blur>
|
||||
<div className="space-y-10">
|
||||
<BodyText className="text-2xl leading-relaxed font-serif italic text-slate-500">
|
||||
Vom statischen HTML zur zentralen Daten-Instanz.
|
||||
</BodyText>
|
||||
<BodyText className="text-xl text-slate-600 leading-relaxed">
|
||||
Ich habe die KLZ-Architektur radikal auf einen entkoppelten High-Performance-Stack umgestellt. WordPress fungiert hier nicht als CMS-Baukasten, sondern als <Marker delay={0.3}>Headless JSON-Provider</Marker>. Durch die Implementierung nativer PHP-Microservices und den Verzicht auf volatile Drittanbieter-Plugins wurde ein System geschaffen, das keine technologischen Überraschungen zulässt. <Marker delay={0.5}>Stability by Design.</Marker>
|
||||
</BodyText>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
<div className="md:col-span-5 relative">
|
||||
<Reveal delay={0.3} direction="right" scale={0.98} blur>
|
||||
<motion.div
|
||||
whileHover={{ y: -5, scale: 1.01 }}
|
||||
transition={{ type: "spring", stiffness: 200, damping: 25 }}
|
||||
className="p-12 bg-slate-50 rounded-[3rem] border border-slate-100 space-y-12 relative overflow-hidden group shadow-sm text-left"
|
||||
>
|
||||
<div className="space-y-8 relative z-10">
|
||||
<Label className="text-slate-900">System Metriken</Label>
|
||||
<div className="space-y-8">
|
||||
{[
|
||||
{ label: 'Edge Caching', desc: 'Varnish + W3TC Object Cache', icon: <Server className="w-5 h-5 text-slate-400" /> },
|
||||
{ label: 'Analytics', desc: 'Independent (Global Data Compliance)', icon: <Activity className="w-5 h-5 text-slate-400" /> },
|
||||
{ label: 'Custom Core', desc: 'REST via Native Services', icon: <Cpu className="w-5 h-5 text-slate-400" /> }
|
||||
].map((item, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ x: -20, opacity: 0 }}
|
||||
whileInView={{ x: 0, opacity: 1 }}
|
||||
transition={{ delay: 0.5 + (i * 0.1), duration: 0.5 }}
|
||||
className="flex gap-6 border-b border-slate-200/50 pb-6 last:border-0 last:pb-0"
|
||||
>
|
||||
<div className="shrink-0 mt-1">{item.icon}</div>
|
||||
<div className="space-y-1">
|
||||
<MonoLabel className="text-[10px] text-slate-400">{item.label}</MonoLabel>
|
||||
<BodyText className="text-base font-bold text-slate-900">{item.desc}</BodyText>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* --- SHOWCASE: LANDING --- */}
|
||||
<section className="py-32 bg-slate-50 border-y border-slate-100 overflow-hidden relative">
|
||||
<div className="w-full max-w-[1920px] mx-auto px-4 md:px-12 relative z-10">
|
||||
<Reveal direction="none" blur>
|
||||
<div className="relative mb-16 flex justify-between items-end">
|
||||
<div className="space-y-6">
|
||||
<Label className="text-slate-500">Infrastructure Validation</Label>
|
||||
<H3 className="text-5xl md:text-8xl tracking-tighter">Global Hub.</H3>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
<Reveal delay={0.2} width="100%" direction="up" scale={0.98} blur>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.01 }}
|
||||
transition={{ type: "spring", stiffness: 200, damping: 20 }}
|
||||
>
|
||||
<IframeSection
|
||||
src="/showcase/klz-cables.com/index.html"
|
||||
height="850px"
|
||||
desktopWidth={1920}
|
||||
allowScroll
|
||||
browserFrame
|
||||
className="w-full h-full transition-all duration-1000 ease-in-out no-scrollbar"
|
||||
/>
|
||||
</motion.div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* --- SECTION 02: TECHNICAL DETAIL --- */}
|
||||
<Section
|
||||
number="02"
|
||||
title="Asset Management"
|
||||
variant="white"
|
||||
borderBottom
|
||||
containerVariant="wide"
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-16">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-end">
|
||||
<Reveal direction="left" blur>
|
||||
<div className="space-y-6">
|
||||
<Label className="text-slate-400">Asset Pipelines</Label>
|
||||
<H3 className="text-4xl md:text-6xl tracking-tighter">Automated Documentation.</H3>
|
||||
</div>
|
||||
</Reveal>
|
||||
<Reveal delay={0.1} direction="right" blur>
|
||||
<BodyText className="text-xl text-slate-500 pb-2 font-serif italic">
|
||||
Für Hochspannungs-N2XS(F)2Y Kabel ist Datentreue eine Sicherheitsanforderung. Ich habe eine automatisierte Asset-Pipeline entwickelt, die technische Datenblätter serverseitig generiert und validiert.
|
||||
</BodyText>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
<Reveal delay={0.2} width="100%" direction="up" scale={0.98} blur>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.01 }}
|
||||
transition={{ type: "spring", stiffness: 200, damping: 20 }}
|
||||
className="relative w-full group"
|
||||
>
|
||||
<div className="relative w-full overflow-visible">
|
||||
<IframeSection
|
||||
src="/showcase/klz-cables.com/power-cables-medium-voltage-cables.html"
|
||||
height="1000px"
|
||||
desktopWidth={1920}
|
||||
allowScroll
|
||||
offsetY={100}
|
||||
browserFrame
|
||||
className="w-full transition-all duration-1000 no-scrollbar"
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* --- SECTION 03: COMMERCE --- */}
|
||||
<Section
|
||||
number="03"
|
||||
title="Katalog-Architektur"
|
||||
borderBottom
|
||||
containerVariant="wide"
|
||||
>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 items-center">
|
||||
<div className="lg:col-span-12 mb-12 text-center lg:text-left relative z-10">
|
||||
<Reveal direction="down" blur>
|
||||
<H3 className="text-4xl md:text-6xl max-w-4xl tracking-tighter">
|
||||
Fokus auf <br /><Marker delay={0.2}>Spezifikationen.</Marker>
|
||||
</H3>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-8 relative group">
|
||||
<Reveal width="100%" direction="left" scale={0.98} blur>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.01 }}
|
||||
transition={{ type: "spring", stiffness: 200, damping: 20 }}
|
||||
className="relative h-[650px] w-full overflow-visible group"
|
||||
>
|
||||
<IframeSection
|
||||
src="/showcase/klz-cables.com/which-cables-for-wind-power-differences-from-low-to-extra-high-voltage-explained-2.html"
|
||||
height="100%"
|
||||
desktopWidth={1920}
|
||||
allowScroll
|
||||
browserFrame
|
||||
className="h-full w-full transition-all duration-700 no-scrollbar"
|
||||
/>
|
||||
</motion.div>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-4 space-y-10 relative z-10">
|
||||
<Reveal delay={0.2} direction="right" blur>
|
||||
<div className="space-y-6">
|
||||
<Label className="text-slate-400">Katalog-Struktur</Label>
|
||||
<LeadText className="text-lg">
|
||||
Der Produktbereich wurde konsequent auf die Bedürfnisse technischer Planer optimiert. Klare Hierarchien und der Verzicht auf E-Commerce-Rauschen ermöglichen einen direkten Zugriff auf Kabel-Parameter und Datenblätter.
|
||||
</LeadText>
|
||||
<motion.div
|
||||
whileHover={{ x: 10 }}
|
||||
className="p-8 bg-white border border-slate-200 rounded-3xl shadow-sm"
|
||||
>
|
||||
<Layers className="w-6 h-6 text-slate-400 mb-4" />
|
||||
<BodyText className="text-sm font-medium">Strukturierte Aufbereitung technischer Produktdaten.</BodyText>
|
||||
</motion.div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* --- SECTION 04: CONTENT ENGINE --- */}
|
||||
<Section
|
||||
number="04"
|
||||
title="Content Strategy"
|
||||
variant="white"
|
||||
borderBottom
|
||||
containerVariant="wide"
|
||||
>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 items-center">
|
||||
<div className="lg:col-span-4 space-y-10 order-2 lg:order-1">
|
||||
<Reveal direction="left" blur>
|
||||
<div className="space-y-6">
|
||||
<Label className="text-slate-400">Knowledge Transfer</Label>
|
||||
<H3 className="text-4xl md:text-6xl tracking-tighter">Insights & News.</H3>
|
||||
<BodyText className="text-xl text-slate-500 font-serif italic">
|
||||
Die News-Engine dient als technischer Hub für Industrie-Standards. Durch die Implementierung eines performanten Blog-Systems wird Fachwissen direkt an die Zielgruppe kommuniziert.
|
||||
</BodyText>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
<div className="lg:col-span-8 order-1 lg:order-2">
|
||||
<Reveal width="100%" direction="right" scale={0.98} blur>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.01 }}
|
||||
transition={{ type: "spring", stiffness: 200, damping: 20 }}
|
||||
className="relative h-[700px] w-full"
|
||||
>
|
||||
<IframeSection
|
||||
src="/showcase/klz-cables.com/blog.html"
|
||||
height="100%"
|
||||
desktopWidth={1600}
|
||||
allowScroll
|
||||
browserFrame
|
||||
className="h-full w-full no-scrollbar"
|
||||
/>
|
||||
</motion.div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* --- SECTION 05: TEAM & TRUST --- */}
|
||||
<Section
|
||||
number="05"
|
||||
title="Ergebnis"
|
||||
borderBottom
|
||||
containerVariant="wide"
|
||||
>
|
||||
<div className="space-y-16 text-center">
|
||||
<Reveal direction="up" blur>
|
||||
<H3 className="text-5xl md:text-8xl tracking-tighter">System-Lifecycle.</H3>
|
||||
<LeadText className="mx-auto max-w-2xl pt-6 text-xl">
|
||||
Die Migration von einer statischen Datei-Struktur zu einer zentralisierten Daten-Instanz eliminiert technische Schulden und manuelle Fehlerquellen. Das Ergebnis ist eine wartungsfreie Architektur, die technische Datentreue über den gesamten Produkt-Lifecycle sicherstellt.
|
||||
</LeadText>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={0.2} width="100%" direction="up" scale={0.98} blur>
|
||||
<div className="relative group w-full text-left">
|
||||
<div className="relative block w-full overflow-visible">
|
||||
<IframeSection
|
||||
src="/showcase/klz-cables.com/team.html"
|
||||
height="1100px"
|
||||
desktopWidth={1440}
|
||||
allowScroll
|
||||
browserFrame
|
||||
className="w-full h-full no-scrollbar"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* --- SECTION 06: CONVERSION --- */}
|
||||
<Section
|
||||
number="06"
|
||||
title="Lead Engineering"
|
||||
variant="white"
|
||||
containerVariant="wide"
|
||||
>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 items-center">
|
||||
<div className="lg:col-span-7">
|
||||
<Reveal direction="left" scale={0.98} blur>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.01 }}
|
||||
transition={{ type: "spring", stiffness: 200, damping: 20 }}
|
||||
className="relative h-[800px] w-full"
|
||||
>
|
||||
<IframeSection
|
||||
src="/showcase/klz-cables.com/contact.html"
|
||||
height="100%"
|
||||
desktopWidth={1200}
|
||||
allowScroll
|
||||
browserFrame
|
||||
className="h-full w-full no-scrollbar"
|
||||
/>
|
||||
</motion.div>
|
||||
</Reveal>
|
||||
</div>
|
||||
<div className="lg:col-span-5 space-y-10">
|
||||
<Reveal direction="right" blur>
|
||||
<div className="space-y-6">
|
||||
<Label className="text-slate-400">Conversion Layer</Label>
|
||||
<H3 className="text-4xl md:text-6xl tracking-tighter">Direkter Draht.</H3>
|
||||
<BodyText className="text-xl text-slate-500 font-serif italic">
|
||||
Das Kontakt-System wurde auf maximale Reduktion getrimmt. Keine unnötigen Hürden, sondern ein direkter Kommunikations-Kanal zwischen technischem Bedarf und individueller Beratung.
|
||||
</BodyText>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* --- FINAL CTA: ARCHITECTURE & VALUE --- */}
|
||||
<section className="py-40 md:py-64 bg-white relative overflow-hidden border-t border-slate-100">
|
||||
<BackgroundGrid />
|
||||
<Container variant="normal" className="relative z-10">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-24 items-center text-left">
|
||||
<div className="space-y-12">
|
||||
<Reveal direction="left" blur>
|
||||
<div className="space-y-4">
|
||||
<MonoLabel className="text-slate-400 tracking-[0.4em]">CONSULTING // ENGINEERING</MonoLabel>
|
||||
<H2 className="text-6xl md:text-8xl tracking-tighter leading-none font-bold">
|
||||
Architektur <br />
|
||||
<span className="text-slate-100">ohne Altlasten.</span>
|
||||
</H2>
|
||||
</div>
|
||||
</Reveal>
|
||||
<Reveal delay={0.2} direction="left" blur>
|
||||
<BodyText className="text-2xl text-slate-500 max-w-xl font-serif italic leading-relaxed">
|
||||
Vom Prototyp zum industriellen Standard. Ich entwickle digitale Infrastrukturen, die technische Freiheit und operative Stabilität garantieren – wartungsfrei und skalierbar.
|
||||
</BodyText>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-50/80 backdrop-blur-sm border border-slate-100 p-10 md:p-14 rounded-[3.5rem] space-y-12 relative group shadow-sm">
|
||||
<Reveal direction="right" blur>
|
||||
<div className="inline-flex items-center gap-3 px-4 py-2 bg-white rounded-full border border-slate-200 mb-4 font-mono text-[10px] tracking-widest text-slate-500 uppercase">
|
||||
<div className="w-1.5 h-1.5 bg-green-400 rounded-full animate-pulse" />
|
||||
Operational Excellence
|
||||
</div>
|
||||
<div className="space-y-10">
|
||||
{[
|
||||
{ title: "Hardened Infrastructure", desc: "Zentralisierte Datenpflege und entkoppelte WordPress-Instanzen." },
|
||||
{ title: "Automated Data Pipelines", desc: "Validierung technischer Spezifikationen ohne manuelle Eingriffe." },
|
||||
{ title: "Maintenance-Free Core", desc: "Plugin-freie Logik für deterministische System-Sicherheit." }
|
||||
].map((item, i) => (
|
||||
<div key={i} className="space-y-3 group/item">
|
||||
<MonoLabel className="text-[10px] text-slate-400 group-hover/item:text-slate-900 transition-colors duration-500">{item.title}</MonoLabel>
|
||||
<BodyText className="text-lg font-bold text-slate-900 leading-tight">{item.desc}</BodyText>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Reveal>
|
||||
<Reveal delay={0.5} direction="up" blur className="pt-6">
|
||||
<MotionButton href="/contact" variant="outline" className="w-full py-8 text-lg group border-2 border-slate-900 rounded-full bg-white hover:bg-slate-900 hover:text-white transition-all duration-700">
|
||||
System-Analyse anfragen
|
||||
<ArrowRight className="inline-block ml-4 w-6 h-6 group-hover:translate-x-4 transition-transform duration-700" />
|
||||
</MotionButton>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
88
apps/web/app/case-studies/page.tsx
Normal file
88
apps/web/app/case-studies/page.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { PageHeader } from '../../src/components/PageHeader';
|
||||
import { Section } from '../../src/components/Section';
|
||||
import { Reveal } from '../../src/components/Reveal';
|
||||
import { H3, LeadText, Label } from '../../src/components/Typography';
|
||||
import { BackgroundGrid, Card, Container } from '../../src/components/Layout';
|
||||
import { MotionButton } from '../../src/components/Button';
|
||||
import Image from 'next/image';
|
||||
|
||||
export default function CaseStudiesPage() {
|
||||
return (
|
||||
<div className="flex flex-col bg-white overflow-hidden relative min-h-screen">
|
||||
<BackgroundGrid />
|
||||
|
||||
<PageHeader
|
||||
title={<>Case Studies: <br /><span className="text-slate-200">Qualität in jedem Detail.</span></>}
|
||||
description="Ein Blick hinter die Kulissen ausgewählter Projekte. Von der ersten Idee bis zum fertigen Hochleistungssystem."
|
||||
backLink={{ href: '/', label: 'Zurück' }}
|
||||
backgroundSymbol="C"
|
||||
/>
|
||||
|
||||
<Section number="01" title="Projekte" borderTop>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
|
||||
<Reveal>
|
||||
<Card variant="white" className="group overflow-hidden">
|
||||
<div className="aspect-video relative overflow-hidden rounded-xl mb-8 bg-slate-100 border border-slate-100">
|
||||
{/* We'll use a placeholder or a screenshot if available.
|
||||
Since we have the cloned site, we could technically iframe a preview here too,
|
||||
but a static image or a styled div is more standard for a card. */}
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-[#0117bf] transition-transform duration-700 group-hover:scale-105 p-12">
|
||||
<Image
|
||||
src="/showcase/klz-cables/assets/img/white_logo_transparent_background.svg"
|
||||
alt="KLZ Cables Logo"
|
||||
width={200}
|
||||
height={200}
|
||||
className="w-full h-auto max-w-[240px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Label>Infrastructure & Energy</Label>
|
||||
<H3 className="group-hover:text-slate-900 transition-colors">KLZ Cables – Digitaler Netzbau</H3>
|
||||
<LeadText className="text-base line-clamp-3">
|
||||
Wie wir eine komplexe WordPress-Struktur in ein performantes, sauberes und langlebiges Web-System verwandelt haben. Fokus auf Performance, SEO und Benutzerführung.
|
||||
</LeadText>
|
||||
|
||||
<div className="pt-4">
|
||||
<MotionButton href="/case-studies/klz-cables">
|
||||
Case Study lesen
|
||||
</MotionButton>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={0.2}>
|
||||
<div className="h-full flex flex-col justify-center border-2 border-dashed border-slate-100 rounded-3xl p-12 text-center space-y-4">
|
||||
<Label>Demnächst</Label>
|
||||
<H3 className="text-slate-200">Weitere Projekte sind in Arbeit.</H3>
|
||||
<LeadText className="text-base italic">
|
||||
Ich dokumentiere gerade weitere spannende Projekte aus den Bereichen SaaS, E-Commerce und Systemarchitektur.
|
||||
</LeadText>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section number="02" title="Philosophie" borderTop variant="gray">
|
||||
<div className="max-w-3xl space-y-8">
|
||||
<Reveal>
|
||||
<H3 className="text-4xl leading-tight">
|
||||
Warum ich Case Studies zeige? <br />
|
||||
<span className="text-slate-200">Weil Code mehr als Text ist.</span>
|
||||
</H3>
|
||||
</Reveal>
|
||||
<Reveal delay={0.2}>
|
||||
<LeadText className="text-xl">
|
||||
In diesen Case Studies geht es nicht nur um bunte Bilder. Es geht um die technischen Entscheidungen, die ein Projekt erfolgreich machen. Schnelle Ladezeiten, SEO-Exzellenz und wartbarer Code sind keine Zufälle, sondern das Ergebnis von präziser Planung.
|
||||
</LeadText>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
40
apps/web/app/contact/page.tsx
Normal file
40
apps/web/app/contact/page.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import * as React from 'react';
|
||||
import { Reveal } from '../../src/components/Reveal';
|
||||
import { PageHeader } from '../../src/components/PageHeader';
|
||||
import { Section } from '../../src/components/Section';
|
||||
import { ContactForm } from '../../src/components/ContactForm';
|
||||
|
||||
export default function ContactPage() {
|
||||
return (
|
||||
<div className="flex flex-col gap-12 py-12 md:py-24">
|
||||
<PageHeader
|
||||
title={<>Projekt <br /><span className="text-slate-200">konfigurieren.</span></>}
|
||||
description="Nutzen Sie den Konfigurator für eine erste Einschätzung oder schreiben Sie mir direkt eine Email."
|
||||
backLink={{ href: '/', label: 'Zurück' }}
|
||||
backgroundSymbol="?"
|
||||
/>
|
||||
|
||||
<Section number="01" title="Konfigurator" containerVariant="wide" className="!py-12">
|
||||
<ContactForm />
|
||||
</Section>
|
||||
|
||||
<Section number="02" title="Direkt" className="!py-12">
|
||||
<div className="grid grid-cols-1 gap-24">
|
||||
<Reveal delay={0.4}>
|
||||
<div className="space-y-8">
|
||||
<a
|
||||
href="mailto:marc@mintel.me"
|
||||
className="group block space-y-2"
|
||||
>
|
||||
<span className="text-xs font-bold uppercase tracking-widest text-slate-300 group-hover:text-slate-900 transition-colors">Email</span>
|
||||
<p className="text-3xl md:text-6xl font-bold text-slate-900 border-b border-slate-100 group-hover:border-slate-900 transition-all duration-500 pb-6 tracking-tighter">
|
||||
marc@mintel.me
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
692
apps/web/app/globals.css
Normal file
692
apps/web/app/globals.css
Normal file
@@ -0,0 +1,692 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Base styles - Tailwind only */
|
||||
@layer base {
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-white text-slate-800 font-serif antialiased selection:bg-slate-900 selection:text-white;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
@apply font-sans font-bold text-slate-900 tracking-tighter;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply text-6xl md:text-8xl leading-[0.95] mb-12;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-4xl md:text-6xl leading-tight mb-8 mt-16;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-3xl md:text-5xl leading-tight mb-6 mt-12;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply text-2xl md:text-3xl leading-tight mb-4 mt-8;
|
||||
}
|
||||
|
||||
p {
|
||||
@apply mb-4 text-base leading-relaxed text-slate-700;
|
||||
}
|
||||
|
||||
.lead {
|
||||
@apply text-xl md:text-2xl text-slate-600 mb-6 leading-relaxed;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
a {
|
||||
@apply text-slate-900 hover:text-slate-700 transition-colors no-underline;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
@apply ml-5 mb-4;
|
||||
}
|
||||
|
||||
li {
|
||||
@apply mb-1;
|
||||
}
|
||||
|
||||
code:not([class*='language-']) {
|
||||
@apply bg-slate-50 px-1.5 py-0.5 rounded-md font-mono text-[0.9em] text-slate-800 border border-slate-100;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
@apply border-l-4 border-slate-900 pl-6 italic text-slate-700 my-8 text-xl md:text-2xl font-serif;
|
||||
}
|
||||
|
||||
/* Focus states */
|
||||
a:focus,
|
||||
button:focus,
|
||||
input:focus,
|
||||
textarea:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Remove default tap highlight on mobile */
|
||||
* {
|
||||
-webkit-tap-highlight-color: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Components - Tailwind utility classes */
|
||||
@layer components {
|
||||
|
||||
/* Legacy hooks required by tests */
|
||||
.file-example {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
.container {
|
||||
@apply max-w-6xl mx-auto px-6 py-12;
|
||||
}
|
||||
|
||||
.wide-container {
|
||||
@apply max-w-7xl mx-auto px-6 py-16;
|
||||
}
|
||||
|
||||
.narrow-container {
|
||||
@apply max-w-4xl mx-auto px-6 py-10;
|
||||
}
|
||||
|
||||
.highlighter-tag {
|
||||
@apply inline-block text-[10px] uppercase tracking-wider font-bold px-3 py-1 rounded-full cursor-pointer transition-all duration-300;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
@apply w-full px-6 py-4 border border-slate-200 rounded-2xl focus:outline-none focus:border-slate-400 transition-all duration-300;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.search-box::placeholder {
|
||||
@apply text-slate-400;
|
||||
}
|
||||
|
||||
/* Blog post card */
|
||||
.post-card {
|
||||
@apply mb-8 last:mb-0;
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
@apply text-xs text-slate-500 font-sans mb-2;
|
||||
}
|
||||
|
||||
.post-excerpt {
|
||||
@apply text-slate-700 mb-2 leading-relaxed;
|
||||
}
|
||||
|
||||
.post-tags {
|
||||
@apply flex flex-wrap gap-1;
|
||||
}
|
||||
|
||||
/* Article page */
|
||||
.article-header {
|
||||
@apply mb-12;
|
||||
}
|
||||
|
||||
.article-title {
|
||||
@apply text-4xl md:text-5xl font-bold mb-3;
|
||||
}
|
||||
|
||||
.article-meta {
|
||||
@apply text-sm text-slate-500 font-sans mb-5;
|
||||
}
|
||||
|
||||
.article-content {
|
||||
@apply text-lg leading-relaxed;
|
||||
}
|
||||
|
||||
.article-content p {
|
||||
@apply mb-5;
|
||||
}
|
||||
|
||||
.article-content h2 {
|
||||
@apply text-2xl font-bold mt-8 mb-3;
|
||||
}
|
||||
|
||||
.article-content h3 {
|
||||
@apply text-xl font-bold mt-6 mb-2;
|
||||
}
|
||||
|
||||
.article-content ul,
|
||||
.article-content ol {
|
||||
@apply ml-6 mb-5;
|
||||
}
|
||||
|
||||
.article-content li {
|
||||
@apply mb-1;
|
||||
}
|
||||
|
||||
.article-content blockquote {
|
||||
@apply border-l-2 border-slate-400 pl-4 italic text-slate-600 my-5 text-lg;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
@apply inline-flex items-center justify-center px-6 py-3 border border-slate-200 bg-white text-slate-600 font-sans font-bold text-sm uppercase tracking-widest rounded-full transition-all duration-500 ease-[cubic-bezier(0.23,1,0.32,1)] hover:border-slate-400 hover:text-slate-900 hover:bg-slate-50 hover:-translate-y-0.5 hover:shadow-xl hover:shadow-slate-100 active:translate-y-0 active:shadow-sm;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply border-slate-900 text-slate-900 hover:bg-slate-900 hover:text-white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply border-slate-200 text-slate-500 hover:border-slate-400 hover:text-slate-900;
|
||||
}
|
||||
|
||||
/* Hide scrollbars but keep functionality */
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
/* IE and Edge */
|
||||
scrollbar-width: none;
|
||||
/* Firefox */
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
@apply text-center py-8 text-slate-500;
|
||||
}
|
||||
|
||||
.empty-state svg {
|
||||
@apply mx-auto mb-2 text-slate-300;
|
||||
}
|
||||
|
||||
/* Line clamp utility */
|
||||
.line-clamp-3 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Reading progress indicator */
|
||||
.reading-progress-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: #0f172a;
|
||||
transform-origin: left;
|
||||
transform: scaleX(0);
|
||||
z-index: 100;
|
||||
transition: transform 0.1s ease-out;
|
||||
}
|
||||
|
||||
/* Floating back to top button */
|
||||
.floating-back-to-top {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
background: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 9999px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #64748b;
|
||||
transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.floating-back-to-top.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.floating-back-to-top:hover {
|
||||
background: #f8fafc;
|
||||
color: #1e293b;
|
||||
border-color: #cbd5e1;
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
/* Reduced motion support */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
|
||||
.floating-back-to-top,
|
||||
.reading-progress-bar {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Additional global styles from BaseLayout */
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
/* Selection color */
|
||||
::selection {
|
||||
background: #0f172a;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Tag Styles */
|
||||
.highlighter-tag {
|
||||
animation: tagPopIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) both;
|
||||
animation-delay: calc(var(--tag-index, 0) * 0.05s);
|
||||
}
|
||||
|
||||
.highlighter-tag:hover {
|
||||
@apply -translate-y-0.5 scale-105 shadow-lg shadow-slate-200;
|
||||
}
|
||||
|
||||
@keyframes tagPopIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.8) translateY(10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.highlighter-yellow {
|
||||
background: linear-gradient(135deg, rgba(255, 235, 59, 0.95) 0%, rgba(255, 213, 79, 0.95) 100%);
|
||||
color: #3f2f00;
|
||||
}
|
||||
|
||||
.highlighter-pink {
|
||||
background: linear-gradient(135deg, rgba(255, 167, 209, 0.95) 0%, rgba(255, 122, 175, 0.95) 100%);
|
||||
color: #3f0018;
|
||||
}
|
||||
|
||||
.highlighter-green {
|
||||
background: linear-gradient(135deg, rgba(129, 199, 132, 0.95) 0%, rgba(102, 187, 106, 0.95) 100%);
|
||||
color: #002f0a;
|
||||
}
|
||||
|
||||
.highlighter-blue {
|
||||
background: linear-gradient(135deg, rgba(226, 232, 240, 0.95) 0%, rgba(203, 213, 225, 0.95) 100%);
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.highlighter-tag:hover::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -2px;
|
||||
background: inherit;
|
||||
filter: blur(8px);
|
||||
opacity: 0.4;
|
||||
z-index: -1;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.highlighter-tag:active {
|
||||
transform: rotate(-1deg) translateY(0) scale(0.98);
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
.highlighter-tag:focus {
|
||||
@apply -translate-y-0.5 scale-105;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
/* Marker Title Styles */
|
||||
.marker-title::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -0.15em;
|
||||
right: -0.15em;
|
||||
bottom: 0.05em;
|
||||
height: 0.62em;
|
||||
border-radius: 0.18em;
|
||||
z-index: -1;
|
||||
|
||||
background:
|
||||
linear-gradient(180deg,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(255, 255, 255, 0) 20%,
|
||||
rgba(253, 230, 138, 0.70) 20%,
|
||||
rgba(253, 230, 138, 0.70) 100%);
|
||||
|
||||
transform-origin: left center;
|
||||
transform:
|
||||
rotate(calc((var(--marker-seed, 0) - 3) * 0.45deg)) skewX(calc((var(--marker-seed, 0) - 3) * -0.25deg));
|
||||
|
||||
filter: saturate(1.05);
|
||||
}
|
||||
|
||||
.marker-title::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -0.18em;
|
||||
right: -0.05em;
|
||||
bottom: 0.05em;
|
||||
height: 0.62em;
|
||||
border-radius: 0.18em;
|
||||
z-index: -1;
|
||||
|
||||
background:
|
||||
linear-gradient(90deg,
|
||||
rgba(253, 230, 138, 0.00) 0%,
|
||||
rgba(253, 230, 138, 0.60) 8%,
|
||||
rgba(253, 230, 138, 0.55) 60%,
|
||||
rgba(253, 230, 138, 0.35) 100%);
|
||||
|
||||
opacity: 0.75;
|
||||
mix-blend-mode: multiply;
|
||||
transform:
|
||||
rotate(calc((var(--marker-seed, 0) - 3) * 0.45deg)) translateY(0.02em);
|
||||
|
||||
mask-image:
|
||||
linear-gradient(180deg,
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgba(0, 0, 0, 1) 20%,
|
||||
rgba(0, 0, 0, 1) 100%);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
|
||||
.post-link:hover .marker-title::before,
|
||||
.post-link:hover .marker-title::after {
|
||||
filter: saturate(1.08) contrast(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
/* Mermaid Styles */
|
||||
.mermaid-container svg {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.mermaid-container rect,
|
||||
.mermaid-container circle,
|
||||
.mermaid-container ellipse,
|
||||
.mermaid-container polygon,
|
||||
.mermaid-container path,
|
||||
.mermaid-container .actor,
|
||||
.mermaid-container .node {
|
||||
fill: white !important;
|
||||
stroke: #cbd5e1 !important;
|
||||
stroke-width: 1.5px !important;
|
||||
}
|
||||
|
||||
.mermaid-container .edgePath .path,
|
||||
.mermaid-container .messageLine0,
|
||||
.mermaid-container .messageLine1,
|
||||
.mermaid-container .flowchart-link {
|
||||
stroke: #cbd5e1 !important;
|
||||
stroke-width: 1.5px !important;
|
||||
}
|
||||
|
||||
.mermaid-container text,
|
||||
.mermaid-container .label,
|
||||
.mermaid-container .labelText,
|
||||
.mermaid-container .edgeLabel,
|
||||
.mermaid-container .node text,
|
||||
.mermaid-container tspan {
|
||||
font-family: 'Inter', sans-serif !important;
|
||||
fill: #334155 !important;
|
||||
color: #334155 !important;
|
||||
stroke: none !important;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.mermaid-container .marker,
|
||||
.mermaid-container marker path {
|
||||
fill: #cbd5e1 !important;
|
||||
stroke: #cbd5e1 !important;
|
||||
}
|
||||
|
||||
/* Generic Embed Styles */
|
||||
.generic-embed {
|
||||
--max-width: 100%;
|
||||
--border-radius: 24px;
|
||||
--bg-color: #ffffff;
|
||||
--border-color: #e2e8f0;
|
||||
--shadow: none;
|
||||
|
||||
margin: 1.5rem 0;
|
||||
width: 100%;
|
||||
max-width: var(--max-width);
|
||||
}
|
||||
|
||||
.embed-wrapper {
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-color);
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.generic-embed[data-type="video"] .embed-wrapper {
|
||||
aspect-ratio: 16/9;
|
||||
height: 0;
|
||||
padding-bottom: 56.25%;
|
||||
/* 16:9 */
|
||||
}
|
||||
|
||||
.generic-embed[data-type="video"] .embed-wrapper iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.embed-wrapper:hover {
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
.generic-embed[data-provider="youtube.com"] {
|
||||
--bg-color: #000000;
|
||||
}
|
||||
|
||||
.generic-embed[data-provider="vimeo.com"] {
|
||||
--bg-color: #1a1a1a;
|
||||
}
|
||||
|
||||
.generic-embed[data-provider="codepen.io"] {
|
||||
--bg-color: #1e1e1e;
|
||||
--border-color: #333;
|
||||
}
|
||||
|
||||
.embed-fallback {
|
||||
padding: 1.5rem;
|
||||
background: #f8fafc;
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: var(--border-radius);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fallback-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.fallback-link {
|
||||
@apply text-slate-900 underline underline-offset-4;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
margin-top: 0.25rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.fallback-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.generic-embed {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.embed-fallback {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.embed-wrapper:hover {
|
||||
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
/* File Example Styles */
|
||||
[data-file-example] {
|
||||
box-shadow: 0 1px 0 rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.copy-btn,
|
||||
.download-btn {
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.copy-btn[data-copied='true'] {
|
||||
color: #065f46;
|
||||
background: rgba(16, 185, 129, 0.10);
|
||||
border-color: rgba(16, 185, 129, 0.35);
|
||||
}
|
||||
|
||||
/* Prism.js syntax highlighting - light, low-noise */
|
||||
code[class*='language-'],
|
||||
pre[class*='language-'],
|
||||
pre:has(code[class*='language-']) {
|
||||
color: #0f172a;
|
||||
background: transparent;
|
||||
text-shadow: none;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.65;
|
||||
direction: ltr;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
tab-size: 2;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: #64748b;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.token.operator {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag,
|
||||
.token.constant,
|
||||
.token.symbol,
|
||||
.token.deleted {
|
||||
color: #c2410c;
|
||||
}
|
||||
|
||||
.token.boolean,
|
||||
.token.number {
|
||||
color: #a16207;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.builtin,
|
||||
.token.inserted {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.language-css .token.string,
|
||||
.style .token.string {
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.keyword {
|
||||
color: #7c3aed;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.token.function,
|
||||
.token.class-name {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important,
|
||||
.token.variable {
|
||||
color: #db2777;
|
||||
}
|
||||
51
apps/web/app/layout.tsx
Normal file
51
apps/web/app/layout.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Inter, Newsreader } from 'next/font/google';
|
||||
import { Analytics } from '../src/components/Analytics';
|
||||
import { Footer } from '../src/components/Footer';
|
||||
import { Header } from '../src/components/Header';
|
||||
import { InteractiveElements } from '../src/components/InteractiveElements';
|
||||
import './globals.css';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' });
|
||||
const newsreader = Newsreader({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-newsreader',
|
||||
style: 'italic',
|
||||
display: 'swap',
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: 'Marc Mintel',
|
||||
template: '%s | Marc Mintel',
|
||||
},
|
||||
description: "Technical problem solver's blog - practical insights and learning notes",
|
||||
metadataBase: new URL('https://mintel.me'),
|
||||
icons: {
|
||||
icon: '/favicon.svg',
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" className={`${inter.variable} ${newsreader.variable}`}>
|
||||
<head>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||
</head>
|
||||
<body className="min-h-screen bg-white">
|
||||
<Header />
|
||||
<main>
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
<InteractiveElements />
|
||||
<Analytics />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
315
apps/web/app/page.tsx
Normal file
315
apps/web/app/page.tsx
Normal file
@@ -0,0 +1,315 @@
|
||||
import {
|
||||
ComparisonRow,
|
||||
ConceptAutomation,
|
||||
ConceptCode,
|
||||
ConceptCommunication,
|
||||
ConceptMessy,
|
||||
ConceptPrice,
|
||||
ConceptPrototyping,
|
||||
ConceptSystem,
|
||||
ConceptWebsite,
|
||||
DifferenceIllustration,
|
||||
HeroArchitecture,
|
||||
HeroMainIllustration
|
||||
} from '../src/components/Landing';
|
||||
import { Reveal } from '../src/components/Reveal';
|
||||
import { Section } from '../src/components/Section';
|
||||
import { H1, H3, LeadText, BodyText, MonoLabel, Label } from '../src/components/Typography';
|
||||
import { BackgroundGrid, Card, Container } from '../src/components/Layout';
|
||||
import { Button } from '../src/components/Button';
|
||||
|
||||
export default function LandingPage() {
|
||||
return (
|
||||
<div className="flex flex-col bg-white overflow-hidden relative">
|
||||
|
||||
<BackgroundGrid />
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative min-h-[80vh] flex items-center pt-24 md:pt-0">
|
||||
<Container variant="narrow" className="relative">
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-12 md:gap-24 items-center">
|
||||
{/* Left Column */}
|
||||
<div className="md:col-span-6 relative z-10">
|
||||
<Reveal>
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-8 h-px bg-slate-900"></div>
|
||||
<MonoLabel className="text-slate-900">Digital Architect</MonoLabel>
|
||||
</div>
|
||||
<H1 className="text-6xl md:text-8xl">
|
||||
Websites <br />
|
||||
<span className="text-slate-200">ohne Overhead.</span>
|
||||
</H1>
|
||||
<div className="pt-4">
|
||||
<Button href="#contact" variant="outline">
|
||||
Projekt anfragen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
{/* Right Column */}
|
||||
<div className="md:col-span-6 relative h-[400px] md:h-[600px] flex items-center justify-center">
|
||||
<div className="absolute inset-0 -z-10 opacity-[0.03] pointer-events-none flex items-center justify-center scale-150">
|
||||
<HeroArchitecture className="w-full h-full" />
|
||||
</div>
|
||||
|
||||
<Reveal delay={0.2} className="w-full h-full flex items-center justify-center">
|
||||
<div className="relative w-full h-full flex items-center justify-center pointer-events-none">
|
||||
<HeroMainIllustration className="w-full h-full scale-110 md:scale-125 origin-center" />
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
|
||||
{/* Section 02: The Promise */}
|
||||
<Section
|
||||
number="02"
|
||||
title="Das Versprechen"
|
||||
borderTop
|
||||
>
|
||||
<div className="space-y-16 relative">
|
||||
<Reveal>
|
||||
<H3 className="max-w-3xl">
|
||||
Schluss mit aufgeblähten Prozessen. <br />
|
||||
<span className="text-slate-200">Ich reduziere auf das Wesentliche.</span>
|
||||
</H3>
|
||||
</Reveal>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-16 relative z-10">
|
||||
<Reveal delay={0.1}>
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<Label className="text-slate-900">Was ich biete</Label>
|
||||
</div>
|
||||
<ul className="space-y-6">
|
||||
{[
|
||||
{ text: 'Direkte Kommunikation ohne Umwege', icon: <ConceptCommunication className="w-12 h-12" /> },
|
||||
{ text: 'Schnelle Prototypen statt langer Konzepte', icon: <ConceptPrototyping className="w-12 h-12" /> },
|
||||
{ text: 'Sauberer Code, der auch morgen noch läuft', icon: <ConceptCode className="w-12 h-12" /> },
|
||||
{ text: 'Fixpreise für volle Budgetsicherheit', icon: <ConceptPrice className="w-12 h-12" /> }
|
||||
].map((item, i) => (
|
||||
<li key={i} className="flex items-center gap-6 group">
|
||||
<div className="shrink-0 transition-transform duration-500 group-hover:scale-110">
|
||||
{item.icon}
|
||||
</div>
|
||||
<LeadText className="text-xl">{item.text}</LeadText>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={0.2}>
|
||||
<div className="space-y-8 opacity-40 hover:opacity-100 transition-opacity duration-700">
|
||||
<div className="flex items-center gap-4">
|
||||
<Label>Was ich nicht mache</Label>
|
||||
</div>
|
||||
<ul className="space-y-4">
|
||||
{[
|
||||
'Endlose Workshops ohne Ergebnis',
|
||||
'PowerPoint-Schlachten',
|
||||
'Outsourcing an Billig-Anbieter',
|
||||
'Wartungsverträge mit versteckten Kosten'
|
||||
].map((item, i) => (
|
||||
<li key={i} className="flex items-start gap-3 decoration-slate-200 line-through">
|
||||
<span className="w-1.5 h-1.5 bg-slate-200 rounded-full mt-2.5 shrink-0"></span>
|
||||
<LeadText className="text-slate-400 text-lg">{item}</LeadText>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Section 03: The Difference */}
|
||||
<Section
|
||||
number="03"
|
||||
title="Der Unterschied"
|
||||
variant="white"
|
||||
borderTop
|
||||
>
|
||||
<div className="space-y-16 relative">
|
||||
<div className="flex flex-col md:flex-row gap-12 items-center">
|
||||
<Reveal className="flex-1">
|
||||
<LeadText className="text-2xl md:text-3xl leading-tight max-w-2xl relative z-10 text-slate-400">
|
||||
Ich arbeite nicht gegen die Zeit, sondern <span className="text-slate-900">für das Ergebnis.</span> Mein Fokus liegt auf der Umsetzung, nicht auf der Verwaltung von Prozessen.
|
||||
</LeadText>
|
||||
</Reveal>
|
||||
<Reveal delay={0.2} className="w-full md:w-72 shrink-0">
|
||||
<div className="p-6 bg-slate-50 rounded-2xl border border-slate-100">
|
||||
<DifferenceIllustration className="w-full h-auto grayscale opacity-50" />
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-8 relative z-20">
|
||||
<ComparisonRow
|
||||
negativeLabel="Klassisch"
|
||||
negativeText="Lange Planungsphasen und abstrakte Konzepte."
|
||||
positiveLabel="Mein Weg"
|
||||
positiveText="Schnelle Prototypen. Sie sehen Fortschritt in Tagen."
|
||||
delay={0.1}
|
||||
/>
|
||||
<ComparisonRow
|
||||
negativeLabel="Klassisch"
|
||||
negativeText="Komplexe Preisstrukturen und versteckte Kosten."
|
||||
positiveLabel="Mein Weg"
|
||||
positiveText="Klare Fixpreise. Volle Kostentransparenz."
|
||||
reverse
|
||||
delay={0.2}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Section 04: Target Group */}
|
||||
<Section
|
||||
number="04"
|
||||
title="Zielgruppe"
|
||||
borderTop
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 relative z-10">
|
||||
<Reveal>
|
||||
<Card variant="dark" padding="normal" className="group">
|
||||
<div className="space-y-6 relative overflow-hidden">
|
||||
<div className="w-16 h-16 bg-white/5 rounded-xl flex items-center justify-center border border-white/10">
|
||||
<ConceptPrice className="w-8 h-8" />
|
||||
</div>
|
||||
<H3 className="text-white text-3xl">Unternehmer & <br/>Geschäftsführer</H3>
|
||||
<LeadText className="text-slate-400 text-lg">
|
||||
"Ich brauche eine Lösung, die funktioniert. Ich habe keine Zeit für technische Details."
|
||||
</LeadText>
|
||||
</div>
|
||||
<div className="pt-8 border-t border-white/5 mt-8">
|
||||
<Label className="group-hover:text-white transition-colors">Perfekt für Sie</Label>
|
||||
</div>
|
||||
</Card>
|
||||
</Reveal>
|
||||
<Reveal delay={0.2}>
|
||||
<Card variant="white" padding="normal" className="group">
|
||||
<div className="space-y-6 relative overflow-hidden">
|
||||
<div className="w-16 h-16 bg-slate-50 border border-slate-100 rounded-xl flex items-center justify-center">
|
||||
<ConceptWebsite className="w-8 h-8" />
|
||||
</div>
|
||||
<H3 className="text-3xl">Marketing & <br/>Vertrieb</H3>
|
||||
<LeadText className="text-slate-400 text-lg">
|
||||
"Wir brauchen Landingpages und Tools, um unsere Ziele zu erreichen. Schnell und zuverlässig."
|
||||
</LeadText>
|
||||
</div>
|
||||
<div className="pt-8 border-t border-slate-50 mt-8">
|
||||
<Label className="group-hover:text-slate-900 transition-colors">Perfekt für Sie</Label>
|
||||
</div>
|
||||
</Card>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Section 05: Services */}
|
||||
<Section
|
||||
number="05"
|
||||
title="Leistungen"
|
||||
variant="gray"
|
||||
borderTop
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 relative z-20">
|
||||
<Reveal delay={0.1}>
|
||||
<Card variant="white" padding="small" className="group">
|
||||
<div className="w-16 h-16 bg-slate-50 rounded-xl flex items-center justify-center mb-8 group-hover:scale-110 transition-transform duration-500">
|
||||
<ConceptWebsite className="w-8 h-8" />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<H3 className="text-2xl">Websites</H3>
|
||||
<BodyText>
|
||||
High-Performance Websites. Maßgeschneiderte Architektur statt Baukasten.
|
||||
</BodyText>
|
||||
<div className="pt-4">
|
||||
<a href="/websites" className="text-[10px] font-bold uppercase tracking-[0.4em] text-slate-900 border-b border-slate-100 pb-1 hover:border-slate-900 transition-all">
|
||||
Details
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={0.3}>
|
||||
<Card variant="white" padding="small" className="group mt-8 md:mt-0">
|
||||
<div className="w-16 h-16 bg-slate-50 rounded-xl flex items-center justify-center mb-8 group-hover:scale-110 transition-transform duration-500">
|
||||
<ConceptSystem className="w-8 h-8" />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<H3 className="text-2xl">Systeme</H3>
|
||||
<BodyText>
|
||||
Web-Applikationen, Portale, interne Tools. Wenn Standard an Grenzen stößt.
|
||||
</BodyText>
|
||||
</div>
|
||||
</Card>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={0.5}>
|
||||
<Card variant="white" padding="small" className="group">
|
||||
<div className="w-16 h-16 bg-slate-50 rounded-xl flex items-center justify-center mb-8 group-hover:scale-110 transition-transform duration-500">
|
||||
<ConceptAutomation className="w-8 h-8" />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<H3 className="text-2xl">Automatisierung</H3>
|
||||
<BodyText>
|
||||
Verbindung von Tools, automatische Prozesse, Daten-Synchronisation.
|
||||
</BodyText>
|
||||
</div>
|
||||
</Card>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Section 06: Contact */}
|
||||
<Section
|
||||
number="06"
|
||||
title="Kontakt"
|
||||
borderTop
|
||||
>
|
||||
<div className="relative py-12" id="contact">
|
||||
<Reveal>
|
||||
<div className="space-y-16">
|
||||
<H1 className="text-6xl md:text-8xl">
|
||||
Lassen Sie uns <br />
|
||||
<span className="text-slate-200">starten.</span>
|
||||
</H1>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-16 items-start relative z-10">
|
||||
<div className="space-y-8 flex-1">
|
||||
<LeadText className="text-2xl md:text-3xl text-slate-400">
|
||||
Schreiben Sie mir kurz, worum es geht. Ich melde mich innerhalb von <span className="text-slate-900">24 Stunden</span>.
|
||||
</LeadText>
|
||||
<div className="pt-4">
|
||||
<a
|
||||
href="/contact"
|
||||
className="inline-block text-3xl md:text-5xl font-bold text-slate-900 hover:text-slate-400 transition-all duration-700 border-b-2 border-slate-900 hover:border-slate-200 pb-2"
|
||||
>
|
||||
Projekt anfragen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full md:w-72 space-y-6 p-6 bg-slate-50 rounded-2xl border border-slate-100">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||
<Label className="text-slate-900">Verfügbarkeit</Label>
|
||||
</div>
|
||||
<BodyText className="text-base leading-snug">
|
||||
Aktuell nehme ich Projekte für <span className="font-bold text-slate-900">Q2 2026</span> an.
|
||||
</BodyText>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
41
apps/web/app/tags/[tag]/page.tsx
Normal file
41
apps/web/app/tags/[tag]/page.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import * as React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { blogPosts } from '../../../src/data/blogPosts';
|
||||
import { MediumCard } from '../../../src/components/MediumCard';
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const allTags = Array.from(new Set(blogPosts.flatMap(post => post.tags || [])));
|
||||
return allTags.map(tag => ({
|
||||
tag,
|
||||
}));
|
||||
}
|
||||
|
||||
export default async function TagPage({ params }: { params: Promise<{ tag: string }> }) {
|
||||
const { tag } = await params;
|
||||
const posts = blogPosts.filter(post => post.tags?.includes(tag));
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-4 py-8">
|
||||
<header className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-slate-900 mb-2">
|
||||
Posts tagged <span className="highlighter-yellow px-2 rounded">{tag}</span>
|
||||
</h1>
|
||||
<p className="text-slate-600">
|
||||
{posts.length} post{posts.length === 1 ? '' : 's'}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="space-y-4">
|
||||
{posts.map(post => (
|
||||
<MediumCard key={post.slug} post={post} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 pt-6 border-t border-slate-200">
|
||||
<Link href="/blog" className="text-slate-600 hover:text-slate-900 inline-flex items-center">
|
||||
← Back to blog
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
99
apps/web/app/technologies/[slug]/content.tsx
Normal file
99
apps/web/app/technologies/[slug]/content.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Container } from '../../../src/components/Layout';
|
||||
import { Label } from '../../../src/components/Typography';
|
||||
import { Check, ArrowLeft, Zap, ExternalLink } from 'lucide-react';
|
||||
import { technologies } from './data';
|
||||
|
||||
export default function TechnologyContent({ slug }: { slug: string }) {
|
||||
const tech = technologies[slug];
|
||||
|
||||
if (!tech) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold mb-4">Technology Not Found</h1>
|
||||
<Link href="/" className="text-blue-600 hover:underline">Return Home</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Icon = tech.icon;
|
||||
|
||||
return (
|
||||
<div className="bg-white min-h-screen text-slate-900 pb-24">
|
||||
<div className="bg-slate-50 border-b border-slate-200">
|
||||
<Container className="py-24">
|
||||
<Link href="/case-studies/klz-cables" className="inline-flex items-center text-sm font-bold text-slate-500 hover:text-slate-900 mb-8 transition-colors">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" /> Back to Case Study
|
||||
</Link>
|
||||
|
||||
<div className="flex flex-col md:flex-row items-start gap-8">
|
||||
<div className={`p-6 rounded-2xl shadow-lg ${tech.color}`}>
|
||||
<Icon className="w-12 h-12" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Label className="text-slate-400 mb-2">TECHNOLOGY DEEP DIVE</Label>
|
||||
<h1 className="text-4xl md:text-5xl font-bold tracking-tight mb-4">{tech.title}</h1>
|
||||
<p className="text-xl text-slate-500 font-medium">{tech.subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
|
||||
<Container className="py-16">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16">
|
||||
<div className="lg:col-span-8 space-y-12">
|
||||
<section>
|
||||
<h2 className="text-2xl font-bold mb-4">What is it?</h2>
|
||||
<p className="text-xl leading-relaxed text-slate-700">{tech.description}</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-bold mb-6 flex items-center gap-3">
|
||||
<Zap className="w-6 h-6 text-amber-500" /> Why I use it
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{tech.benefits.map((benefit, i) => (
|
||||
<div key={i} className="flex gap-3 p-4 bg-slate-50 rounded-xl border border-slate-100">
|
||||
<Check className="w-5 h-5 text-green-600 shrink-0" />
|
||||
<span className="font-medium text-slate-700">{benefit}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="bg-blue-50 border border-blue-100 rounded-2xl p-8">
|
||||
<Label className="text-blue-600 mb-2">CUSTOMER IMPACT</Label>
|
||||
<h3 className="text-2xl font-bold text-slate-900 mb-4">What does this mean for you?</h3>
|
||||
<p className="text-lg text-slate-700 leading-relaxed">
|
||||
{tech.customerValue}
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-4 space-y-8">
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-2xl p-6 sticky top-24">
|
||||
<h3 className="font-bold text-slate-900 mb-4">Related Technologies</h3>
|
||||
<div className="space-y-2">
|
||||
{tech.related.map((item) => (
|
||||
<Link
|
||||
key={item.slug}
|
||||
href={`/technologies/${item.slug}`}
|
||||
className="flex items-center justify-between p-3 bg-white border border-slate-200 rounded-lg hover:border-slate-400 hover:shadow-sm transition-all group"
|
||||
>
|
||||
<span className="font-medium text-slate-700 group-hover:text-slate-900">{item.name}</span>
|
||||
<ExternalLink className="w-4 h-4 text-slate-400 group-hover:text-slate-600" />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
105
apps/web/app/technologies/[slug]/data.tsx
Normal file
105
apps/web/app/technologies/[slug]/data.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { Layers, Code, Database, Palette, Terminal } from 'lucide-react';
|
||||
|
||||
export interface TechInfo {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
description: string;
|
||||
icon: any; // React.ElementType
|
||||
benefits: string[];
|
||||
customerValue: string;
|
||||
color: string;
|
||||
related: { name: string; slug: string }[];
|
||||
}
|
||||
|
||||
export const technologies: Record<string, TechInfo> = {
|
||||
'next-js-14': {
|
||||
title: 'Next.js 14',
|
||||
subtitle: 'The React Framework for the Web',
|
||||
description: 'Next.js 14 is the latest version of the industry-leading framework for building high-performance web applications. It allows me to create fast, scalable, and search-engine-friendly websites by rendering content on the server before sending it to your users.',
|
||||
icon: Layers,
|
||||
benefits: [
|
||||
'Lightning-fast page loads with Server Components',
|
||||
'Automatic image optimization',
|
||||
'Instant navigation between pages',
|
||||
'Top-tier SEO (Search Engine Optimization) out of the box'
|
||||
],
|
||||
customerValue: 'For my clients, Next.js means a website that ranks higher on Google, keeps visitors engaged with instant interactions, and scales effortlessly as your traffic grows.',
|
||||
color: 'bg-black text-white',
|
||||
related: [
|
||||
{ name: 'TypeScript', slug: 'typescript' },
|
||||
{ name: 'React', slug: 'react' }
|
||||
]
|
||||
},
|
||||
'typescript': {
|
||||
title: 'TypeScript',
|
||||
subtitle: 'JavaScript with Syntax for Types',
|
||||
description: 'TypeScript adds a powerful type system to JavaScript, catching errors before they ever reach production. It acts as a safety net for your code, ensuring that data flows exactly as expected through your entire application.',
|
||||
icon: Code,
|
||||
benefits: [
|
||||
'Eliminates whole categories of common bugs',
|
||||
'Makes large codebases easier to maintain',
|
||||
'Improves developer productivity and code confidence',
|
||||
'Ensures critical data integrity'
|
||||
],
|
||||
customerValue: 'Using TypeScript means your application is robust and reliable from day one. It dramatically reduces the risk of "runtime errors" that could crash your site, saving time and money on bug fixes down the line.',
|
||||
color: 'bg-blue-600 text-white',
|
||||
related: [
|
||||
{ name: 'Directus CMS', slug: 'directus-cms' },
|
||||
{ name: 'Next.js 14', slug: 'next-js-14' }
|
||||
]
|
||||
},
|
||||
'directus-cms': {
|
||||
title: 'Directus CMS',
|
||||
subtitle: 'The Open Data Platform',
|
||||
description: 'Directus is a modern, headless Content Management System (CMS) that instantly turns any database into a beautiful, easy-to-use application for managing your content. Unlike traditional CMSs, it doesn\'t dictate how your website looks.',
|
||||
icon: Database,
|
||||
benefits: [
|
||||
'Intuitive interface for non-technical editors',
|
||||
'Complete freedom regarding front-end design',
|
||||
'Real-time updates and live previews',
|
||||
'Highly secure and role-based access control'
|
||||
],
|
||||
customerValue: 'Directus gives you full control over your content without needing a developer for every text change. It separates your data from the design, ensuring your website can evolve visually without rebuilding your entire content library.',
|
||||
color: 'bg-purple-600 text-white',
|
||||
related: [
|
||||
{ name: 'Next.js 14', slug: 'next-js-14' },
|
||||
{ name: 'Tailwind CSS', slug: 'tailwind-css' }
|
||||
]
|
||||
},
|
||||
'tailwind-css': {
|
||||
title: 'Tailwind CSS',
|
||||
subtitle: 'Utility-First CSS Framework',
|
||||
description: 'Tailwind CSS is a utility-first framework that allows me to build custom designs directly in markup. It eliminates the need for bulky, overriding stylesheets and ensures a consistent design system across every page.',
|
||||
icon: Palette,
|
||||
benefits: [
|
||||
'Rapid UI development and prototyping',
|
||||
'Consistent spacing, colors, and typography',
|
||||
'Highly optimized final bundle size (only includes used styles)',
|
||||
'Responsive design made simple'
|
||||
],
|
||||
customerValue: 'Tailwind ensures your brand looks pixel-perfect on every device. It also results in incredibly small CSS files, meaning your site loads faster for users on mobile networks.',
|
||||
color: 'bg-cyan-500 text-white',
|
||||
related: [
|
||||
{ name: 'React', slug: 'react' },
|
||||
{ name: 'Next.js 14', slug: 'next-js-14' }
|
||||
]
|
||||
},
|
||||
'react': {
|
||||
title: 'React',
|
||||
subtitle: 'The Library for Web and Native User Interfaces',
|
||||
description: 'React is the core library powering Next.js. It lets me build encapsulated components that manage their own state, then compose them to make complex UIs.',
|
||||
icon: Terminal,
|
||||
benefits: [
|
||||
'Component-based architecture for reuse',
|
||||
'Efficient updates and rendering',
|
||||
'Rich ecosystem of libraries and tools',
|
||||
'Strong community support'
|
||||
],
|
||||
customerValue: 'React enables rich, app-like interactions on your website. Whether it\'s a complex dashboard or a smooth animation, React makes it possible to build dynamic experiences that feel instantaneous.',
|
||||
color: 'bg-blue-400 text-white',
|
||||
related: [
|
||||
{ name: 'Next.js 14', slug: 'next-js-14' },
|
||||
{ name: 'Tailwind CSS', slug: 'tailwind-css' }
|
||||
]
|
||||
}
|
||||
};
|
||||
14
apps/web/app/technologies/[slug]/page.tsx
Normal file
14
apps/web/app/technologies/[slug]/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import { technologies } from './data';
|
||||
import TechnologyContent from './content';
|
||||
|
||||
export default function TechnologyPage({ params }: { params: { slug: string } }) {
|
||||
return <TechnologyContent slug={params.slug} />;
|
||||
}
|
||||
|
||||
// Generate static params for these dynamic routes
|
||||
export async function generateStaticParams() {
|
||||
return Object.keys(technologies).map((slug) => ({
|
||||
slug,
|
||||
}));
|
||||
}
|
||||
309
apps/web/app/websites/page.tsx
Normal file
309
apps/web/app/websites/page.tsx
Normal file
@@ -0,0 +1,309 @@
|
||||
'use client';
|
||||
|
||||
import { PageHeader } from '../../src/components/PageHeader';
|
||||
import { Reveal } from '../../src/components/Reveal';
|
||||
import { Section } from '../../src/components/Section';
|
||||
import {
|
||||
SystemArchitecture,
|
||||
SpeedPerformance,
|
||||
SolidFoundation,
|
||||
LayerSeparation,
|
||||
DirectService,
|
||||
TaskDone,
|
||||
ConceptAutomation,
|
||||
ConceptCode,
|
||||
ConceptCommunication,
|
||||
ConceptPrototyping,
|
||||
ConceptSystem,
|
||||
ConceptTarget
|
||||
} from '../../src/components/Landing';
|
||||
import { Check } from 'lucide-react';
|
||||
import { H2, H3, H4, LeadText, BodyText, Label } from '../../src/components/Typography';
|
||||
import { BackgroundGrid, Card } from '../../src/components/Layout';
|
||||
import { MotionButton } from '../../src/components/Button';
|
||||
|
||||
export default function WebsitesPage() {
|
||||
return (
|
||||
<div className="flex flex-col bg-white overflow-hidden relative">
|
||||
|
||||
<BackgroundGrid />
|
||||
|
||||
<PageHeader
|
||||
title={<>Websites, die <br /><span className="text-slate-200">einfach funktionieren.</span></>}
|
||||
description="Keine Baukästen, keine Plugins, kein Overhead. Nur sauberer Code und maximale Performance."
|
||||
backLink={{ href: '/', label: 'Zurück' }}
|
||||
backgroundSymbol="W"
|
||||
/>
|
||||
|
||||
{/* Intro / Problem */}
|
||||
<Section
|
||||
number="01"
|
||||
title="Der Ansatz"
|
||||
borderTop
|
||||
illustration={<SystemArchitecture className="w-24 h-24" />}
|
||||
>
|
||||
<div className="space-y-12">
|
||||
<Reveal>
|
||||
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
|
||||
Ich baue Websites wie Systeme – <br />
|
||||
<span className="text-slate-200">nicht wie Broschüren.</span>
|
||||
</H3>
|
||||
</Reveal>
|
||||
<Reveal delay={0.2}>
|
||||
<LeadText className="text-xl md:text-2xl max-w-2xl text-slate-400">
|
||||
Eine Website ist kein Flyer. Sie ist ein <span className="text-slate-900">Werkzeug</span>, das jeden Tag arbeitet.
|
||||
Deshalb baue ich sie stabil, schnell und wartungsfrei.
|
||||
</LeadText>
|
||||
</Reveal>
|
||||
<Reveal delay={0.4}>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 pt-8">
|
||||
{[
|
||||
{ label: 'Stabil', icon: ConceptSystem },
|
||||
{ label: 'Schnell', icon: ConceptAutomation },
|
||||
{ label: 'Wartungsfrei', icon: ConceptCode },
|
||||
{ label: 'Sicher', icon: ConceptTarget },
|
||||
].map((item, i) => (
|
||||
<div key={i} className="flex flex-col gap-3 group">
|
||||
<div className="w-12 h-12 rounded-xl bg-slate-50 flex items-center justify-center border border-slate-100 group-hover:scale-110 transition-transform duration-500">
|
||||
<item.icon className="w-6 h-6" />
|
||||
</div>
|
||||
<Label className="text-slate-900">{item.label}</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Speed */}
|
||||
<Section
|
||||
number="02"
|
||||
title="Performance"
|
||||
borderTop
|
||||
variant="gray"
|
||||
illustration={<SpeedPerformance className="w-24 h-24" />}
|
||||
>
|
||||
<div className="space-y-12">
|
||||
<Reveal>
|
||||
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
|
||||
Geschwindigkeit ist <br />
|
||||
<span className="text-slate-200">kein Extra. Sie ist Standard.</span>
|
||||
</H3>
|
||||
</Reveal>
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-12 items-center">
|
||||
<div className="md:col-span-7 space-y-8">
|
||||
<Reveal delay={0.2}>
|
||||
<LeadText className="text-xl text-slate-400">
|
||||
Viele Websites sind langsam, weil sie zusammengeklickt sind. Meine sind schnell, weil sie <span className="text-slate-900">von Grund auf</span> entwickelt wurden.
|
||||
</LeadText>
|
||||
</Reveal>
|
||||
<Reveal delay={0.4}>
|
||||
<ul className="space-y-4">
|
||||
{[
|
||||
'Seiten laden ohne Verzögerung',
|
||||
'Optimiert für Suchmaschinen (SEO)',
|
||||
'Bessere Nutzererfahrung',
|
||||
'Höhere Conversion-Rates',
|
||||
].map((item, i) => (
|
||||
<li key={i} className="flex items-center gap-4 group">
|
||||
<div className="w-1.5 h-1.5 bg-slate-900 rounded-full group-hover:scale-150 transition-transform" />
|
||||
<LeadText className="text-lg md:text-xl">{item}</LeadText>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Reveal>
|
||||
</div>
|
||||
<div className="md:col-span-5">
|
||||
<Reveal delay={0.6}>
|
||||
<Card variant="white" padding="normal" className="text-center group">
|
||||
<div className="text-7xl md:text-8xl font-bold text-slate-900 tracking-tighter group-hover:scale-110 transition-transform duration-700">90+</div>
|
||||
<Label className="mt-4">Pagespeed Score</Label>
|
||||
</Card>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* No Maintenance */}
|
||||
<Section
|
||||
number="03"
|
||||
title="Technik"
|
||||
borderTop
|
||||
illustration={<SolidFoundation className="w-24 h-24" />}
|
||||
>
|
||||
<div className="space-y-12">
|
||||
<Reveal>
|
||||
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
|
||||
Keine Plugins. <br />
|
||||
<span className="text-slate-200">Keine Abhängigkeiten.</span>
|
||||
</H3>
|
||||
</Reveal>
|
||||
<Reveal delay={0.2}>
|
||||
<LeadText className="text-xl md:text-2xl max-w-2xl text-slate-400">
|
||||
Ich nutze keine Baukästen, die sich selbst zerstören.
|
||||
Ihre Website besteht aus <span className="text-slate-900">sauberem Code</span>, der Ihnen gehört.
|
||||
</LeadText>
|
||||
</Reveal>
|
||||
<Reveal delay={0.4}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 pt-8">
|
||||
<Card variant="white" padding="normal" className="group">
|
||||
<div className="space-y-4">
|
||||
<Label className="text-slate-900 mb-2">Code Qualität</Label>
|
||||
<H4 className="text-2xl">Langlebigkeit</H4>
|
||||
<BodyText>Modernste Web-Technologien für maximale Performance und Wartbarkeit.</BodyText>
|
||||
</div>
|
||||
</Card>
|
||||
<Card variant="white" padding="normal" className="group">
|
||||
<div className="space-y-4">
|
||||
<Label className="text-slate-900 mb-2">Sicherheit</Label>
|
||||
<H4 className="text-2xl">Resilienz</H4>
|
||||
<BodyText>Minimale Angriffsfläche durch Verzicht auf unnötige Drittanbieter-Software.</BodyText>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Content/Tech Separation */}
|
||||
<Section
|
||||
number="04"
|
||||
title="Inhalte"
|
||||
borderTop
|
||||
variant="gray"
|
||||
illustration={<LayerSeparation className="w-24 h-24" />}
|
||||
>
|
||||
<div className="space-y-12">
|
||||
<Reveal>
|
||||
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
|
||||
Inhalte pflegen <br />
|
||||
<span className="text-slate-200">ohne Angst.</span>
|
||||
</H3>
|
||||
</Reveal>
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-12 items-start">
|
||||
<div className="md:col-span-7">
|
||||
<Reveal delay={0.2}>
|
||||
<LeadText className="text-xl md:text-2xl text-slate-400">
|
||||
Sie können Texte und Bilder selbst anpassen, ohne das Design oder die Technik zu gefährden.
|
||||
Ein <span className="text-slate-900">intuitives System</span> sorgt dafür, dass alles an seinem Platz bleibt.
|
||||
</LeadText>
|
||||
</Reveal>
|
||||
</div>
|
||||
<div className="md:col-span-5">
|
||||
<Reveal delay={0.4}>
|
||||
<Card variant="white" padding="normal" className="space-y-8">
|
||||
<div className="space-y-4">
|
||||
<Label>Ihre Freiheit</Label>
|
||||
<div className="flex items-center gap-3 text-xl font-bold text-slate-900">
|
||||
<div className="w-8 h-8 rounded-full bg-slate-900 flex items-center justify-center">
|
||||
<Check className="w-3 h-3 text-white" />
|
||||
</div>
|
||||
Inhalte flexibel verwalten
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4 opacity-30">
|
||||
<Label>Mein Schutz</Label>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-3 text-xl font-bold text-slate-900 line-through">Design-Chaos</div>
|
||||
<div className="flex items-center gap-3 text-xl font-bold text-slate-900 line-through">Technische Fehler</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Simple Changes */}
|
||||
<Section
|
||||
number="05"
|
||||
title="Service"
|
||||
borderTop
|
||||
illustration={<DirectService className="w-24 h-24" />}
|
||||
>
|
||||
<div className="space-y-12">
|
||||
<Reveal>
|
||||
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
|
||||
Änderungen sind <br />
|
||||
<span className="text-slate-200">Teil des Konzepts.</span>
|
||||
</H3>
|
||||
</Reveal>
|
||||
<Reveal delay={0.2}>
|
||||
<LeadText className="text-xl md:text-2xl max-w-2xl text-slate-400">
|
||||
Ihr Business entwickelt sich weiter, Ihre Website auch. <br />
|
||||
Keine komplizierten Prozesse, sondern <span className="text-slate-900">direkte Umsetzung</span> Ihrer Ideen.
|
||||
</LeadText>
|
||||
</Reveal>
|
||||
<Reveal delay={0.4}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 pt-8">
|
||||
<Card variant="white" padding="normal" className="group hover:border-slate-900">
|
||||
<ConceptCommunication className="w-12 h-12 mb-8 group-hover:scale-110 transition-all duration-700" />
|
||||
<div className="space-y-2">
|
||||
<H4 className="text-2xl">Direkter Draht</H4>
|
||||
<BodyText>Sie sprechen direkt mit dem Entwickler. Keine Stille Post.</BodyText>
|
||||
</div>
|
||||
</Card>
|
||||
<Card variant="white" padding="normal" className="group hover:border-slate-900">
|
||||
<ConceptPrototyping className="w-12 h-12 mb-8 group-hover:scale-110 transition-all duration-700" />
|
||||
<div className="space-y-2">
|
||||
<H4 className="text-2xl">Agile Anpassung</H4>
|
||||
<BodyText>Schnelle Iterationen statt langer Wartezeiten.</BodyText>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Result */}
|
||||
<Section
|
||||
number="06"
|
||||
title="Ergebnis"
|
||||
borderTop
|
||||
variant="gray"
|
||||
illustration={<TaskDone className="w-24 h-24" />}
|
||||
>
|
||||
<div className="space-y-16">
|
||||
<Reveal>
|
||||
<H3 className="text-4xl md:text-6xl tracking-tighter">
|
||||
Eine Website, die <br />
|
||||
<span className="text-slate-200">einfach läuft.</span>
|
||||
</H3>
|
||||
</Reveal>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-12">
|
||||
{[
|
||||
{ title: 'Kein Overhead', desc: 'Fokus auf das, was Ihre Kunden wirklich brauchen.' },
|
||||
{ title: 'Volle Kontrolle', desc: 'Der Code gehört Ihnen, ohne Vendor Lock-in.' },
|
||||
{ title: 'Echte Performance', desc: 'Messbare Geschwindigkeit für bessere Ergebnisse.' },
|
||||
].map((item, i) => (
|
||||
<Reveal key={i} delay={i * 0.1}>
|
||||
<div className="space-y-4 group">
|
||||
<div className="w-8 h-px bg-slate-200 group-hover:w-full transition-all duration-1000" />
|
||||
<H4 className="text-2xl">{item.title}</H4>
|
||||
<LeadText className="text-lg text-slate-400">{item.desc}</LeadText>
|
||||
</div>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Reveal delay={0.4}>
|
||||
<div className="pt-16 border-t border-slate-200 flex flex-col md:flex-row justify-between items-start md:items-center gap-8">
|
||||
<div className="space-y-2">
|
||||
<Label>Bereit für mehr?</Label>
|
||||
<LeadText className="text-2xl">
|
||||
Lassen Sie uns über Ihr nächstes Projekt sprechen.
|
||||
</LeadText>
|
||||
</div>
|
||||
<MotionButton href="/contact">
|
||||
Projekt anfragen
|
||||
</MotionButton>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
64
apps/web/context/about.md
Normal file
64
apps/web/context/about.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Marc — digital problem solver
|
||||
|
||||
## Identity
|
||||
- Name: Marc Mintel
|
||||
- Mail: marc@mintel.me
|
||||
- Location: Vulkaneifel, Germany
|
||||
- Role: Independent digital problem solver
|
||||
- Mode: Solo
|
||||
- Focus: Understanding problems and building practical solutions
|
||||
|
||||
## What I do
|
||||
I work on digital problems and build tools, scripts, and systems to solve them.
|
||||
Sometimes that means code, sometimes automation, sometimes AI, sometimes something else.
|
||||
The tool is secondary. The problem comes first.
|
||||
|
||||
## How I work
|
||||
- I try things
|
||||
- I break things
|
||||
- I fix things
|
||||
- I write down what I learned
|
||||
|
||||
## What this blog is
|
||||
A public notebook of:
|
||||
- things I figured out
|
||||
- mistakes I made
|
||||
- tools I tested
|
||||
- small insights that might be useful later
|
||||
|
||||
Mostly short entries.
|
||||
Mostly practical.
|
||||
|
||||
## Why no portfolio
|
||||
Finished projects get outdated.
|
||||
Understanding doesn’t.
|
||||
|
||||
This blog shows how I approach problems, not how pretty something looked last year.
|
||||
|
||||
## Topics
|
||||
- Vibe coding with AI
|
||||
- Debugging and problem solving
|
||||
- Mac tools and workflows
|
||||
- Automation
|
||||
- Small scripts and systems
|
||||
- Learning notes
|
||||
- FOSS
|
||||
|
||||
## Audience
|
||||
People who:
|
||||
- build things
|
||||
- work with computers
|
||||
- solve problems
|
||||
- and don’t need marketing talk
|
||||
|
||||
## Tone
|
||||
- calm
|
||||
- factual
|
||||
- direct
|
||||
- no hype
|
||||
- no self-promotion
|
||||
|
||||
## Core idea
|
||||
Write things down.
|
||||
So I don’t forget.
|
||||
And so others might find them useful.
|
||||
246
apps/web/data/briefings/etib.txt
Normal file
246
apps/web/data/briefings/etib.txt
Normal file
@@ -0,0 +1,246 @@
|
||||
Hallo Marc,
|
||||
|
||||
eine harte Deadline gibt es nicht – Was denkst du ist realistisch? Ich habe als Ziel so
|
||||
April / Mai im Kopf -> dann aber schon zu 95 % fertig. Viele Grüße
|
||||
|
||||
Mit freundlichen Grüßen
|
||||
|
||||
Danny Joseph
|
||||
Geschäftsführer
|
||||
|
||||
E-TIB GmbH
|
||||
Gewerbestraße 22
|
||||
D-03172 Guben
|
||||
|
||||
Mobil +49 15207230518
|
||||
E-Mail d.joseph@e-tib.com
|
||||
Web www.e-tib.com
|
||||
|
||||
--------------------------------------------------------------------------------------------------
|
||||
|
||||
Hey,
|
||||
|
||||
ich würde wie bei https://www.schleicher-gruppe.de/ ein Video auf der Startseite
|
||||
haben wollen. Da ginge sicherlich was vom bisherigen Messevideo. Liebe Grüße.
|
||||
|
||||
Mit freundlichen Grüßen
|
||||
|
||||
Danny Joseph
|
||||
Geschäftsführer
|
||||
|
||||
E-TIB GmbH
|
||||
Gewerbestraße 22
|
||||
D-03172 Guben
|
||||
|
||||
Mobil +49 15207230518
|
||||
E-Mail d.joseph@e-tib.com
|
||||
Web www.e-tib.com
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------------------------
|
||||
Geschäftsführung: Danny Joseph
|
||||
Handelsregister: Amtsgericht Cottbus
|
||||
HRB: 12403 CB
|
||||
USt. ID-Nr.: DE304799919
|
||||
--------------------------------------------------------------------------------------------------
|
||||
|
||||
Von: Frieder Helmich <f.helmich@etib-ing.com>
|
||||
Gesendet: Donnerstag, 29. Januar 2026 08:49
|
||||
An: Marc Mintel <marc@cablecreations.de>; Danny Joseph <d.joseph@e-tib.com>
|
||||
Betreff: AW: Homepage E-TIB
|
||||
|
||||
Hi Marc,
|
||||
|
||||
brauchst du nur Fotos oder bindest du auch videos ein? Wir haben sehr viel Videomaterial. Wir haben auch einen kleinen Film den wir auf der Messe laufen lassen haben.
|
||||
|
||||
Mit freundlichen Grüßen
|
||||
|
||||
i.A. Frieder Helmich
|
||||
|
||||
E-TIB Ingenieurgesellschaft mbH
|
||||
Kampstraße 3
|
||||
D-27412 Bülstedt
|
||||
|
||||
Tel +49 4283 6979923
|
||||
Mobil +49 173 6560514
|
||||
Fax +49 4283 6084091
|
||||
|
||||
E-Mail f.helmich@etib-ing.com
|
||||
Web www.etib-ing.com
|
||||
|
||||
ETIB_Ing_logo_mk
|
||||
Datenschutzhinweise: www.etib-ing.com/datenschutz
|
||||
-----------------------------------------------------------------------------------------------
|
||||
Geschäftsführung: Julian Helmich
|
||||
Handelsregister: Amtsgericht Tostedt
|
||||
HRB: 207158
|
||||
-----------------------------------------------------------------------------------------------
|
||||
|
||||
Von: Marc Mintel <marc@cablecreations.de>
|
||||
Gesendet: Mittwoch, 28. Januar 2026 18:10
|
||||
An: Danny Joseph <d.joseph@e-tib.com>
|
||||
Cc: Frieder Helmich <f.helmich@etib-ing.com>
|
||||
Betreff: Re: Homepage E-TIB
|
||||
|
||||
Hallo Danny,
|
||||
|
||||
Vielen Dank für die schnelle Rückmeldung.
|
||||
Wie gesprochen werde ich mir die Unterlagen und Webseiten im Detail anschauen und mich dann noch einmal bei dir melden.
|
||||
|
||||
Gibt es eigentlich eine Deadline oder einen zeitlichen Rahmen, wo ihr mit der neuen Webseite rechnen möchtet?
|
||||
Je nach dem könnte man auch Features priorisieren, so dass der Kern der Seite schnellstmöglich modernisiert online geht und der Rest im Nachgang.
|
||||
|
||||
Das Foto-Material würde ich auch gerne sichten, dann kann man schon sehen, wie viel sich damit arbeiten lässt.
|
||||
|
||||
Viele Grüße
|
||||
|
||||
|
||||
From: Danny Joseph <d.joseph@e-tib.com>
|
||||
Organization: E-TIB GmbH
|
||||
Date: Wednesday, 28. January 2026 at 16:16
|
||||
To: Marc Mintel <marc@cablecreations.de>
|
||||
Cc: 'Frieder Helmich' <f.helmich@etib-ing.com>
|
||||
Subject: Homepage E-TIB
|
||||
|
||||
Hallo Marc,
|
||||
|
||||
wie telefonisch besprochen erste wirre Gedanken:
|
||||
|
||||
Wir möchten eine minimalistische, hochwertige Homepage die sowohl am PV, als auch
|
||||
Auf Smartphone / Tablet etc. vernünftig ausschaut.
|
||||
|
||||
Bisher war unser Aufhänger:
|
||||
DIE EXPERTEN FÜR KABELTIEFBAU …
|
||||
|
||||
Alles nur Ideen: …
|
||||
|
||||
# Schaltflächen ähnlich: https://www.schleicher-gruppe.de/
|
||||
|
||||
E-TIB GmbH
|
||||
E-TIB Verwaltung GmbH
|
||||
E-TIB Ingenieurgesellschaft mbH
|
||||
E-TIB Bohrtechnik GmbH
|
||||
|
||||
# Schaltflächen ähnlich: https://www.schleicher-gruppe.de/
|
||||
(ehemals Kompetenzen www.e-tib.com)
|
||||
|
||||
Kabelbau
|
||||
Kabelpflugarbeiten
|
||||
Horizontalspülbohrungen
|
||||
Elektromontagen bis 110 kV
|
||||
Glasfaser-Kabelmontagen
|
||||
Wartung & Störungsdienst
|
||||
Genehmigungs- und Ausführungsplanung
|
||||
Komplexe Querung (Bahn, Autobahn, Gewässer)
|
||||
Elektro- und Netzanschlussplanung
|
||||
Vermessung & Dokumentation
|
||||
|
||||
Input für Über uns: Grid … Timeline?
|
||||
Gründung E-TIB GmbH: 16.12.2015
|
||||
Kabelbau
|
||||
Kabelpflugarbeiten
|
||||
Horizontalspülbohrungen
|
||||
Elektromontagen bis 110 kV
|
||||
Glasfaser-Kabelmontagen
|
||||
Wartung & Störungsdienst
|
||||
Elektro- und Netzanschlussplanung
|
||||
Vermessung & Dokumentation
|
||||
|
||||
Gründung E-TIB Verwaltung GmbH: 14.11.2019
|
||||
Der Erwerb, die Vermietung, Verpachtung und Verwaltung
|
||||
von Immobilien, Grundstücken, Maschinen und Geräten.
|
||||
|
||||
Gründung E-TIB Ingenieurgesellschaft mbH: 04.02.2019
|
||||
Genehmigungs- und Ausführungsplanung
|
||||
Komplexe Querung (Bahn, Autobahn, Gewässer)
|
||||
Elektro- und Netzanschlussplanung
|
||||
|
||||
Gründung E-TIB Bohrtechnik GmbH: 21.10.2025
|
||||
Horizontalspülbohrungen in allen Bodenklassen
|
||||
|
||||
Gruppen‑Kacheln (Beispieltexte) ...
|
||||
|
||||
E‑TIB GmbH – Ausführung elektrischer Infrastrukturprojekte
|
||||
E‑TIB Bohrtechnik GmbH – Präzise Horizontalbohrungen in allen Bodenklassen
|
||||
E‑TIB Verwaltung GmbH – Zentrale Dienste, Einkauf, Finanzen
|
||||
E‑TIB Ingenieurgesellschaft mbH – Planung, Projektierung, Dokumentation
|
||||
|
||||
Kontaktseite siehe: www.e-tib.com
|
||||
|
||||
Karriere: ...
|
||||
|
||||
Messen: wo wir dieses Jahr einen Stand haben: Intersolar München, Windenergietage Linstow, Kabelwerkstatt Wiesbaden
|
||||
|
||||
Referenzen: … müsste ich dir zur Verfügung stellen
|
||||
|
||||
Pflichtseiten
|
||||
Impressum (vollständig, Verantwortliche, Registernummer, USt‑ID).
|
||||
Datenschutz (Verarbeitungen, Rechtsgrundlagen, AVV, Cookie‑Gruppen, Löschfristen, Rechte).
|
||||
Cookie‑Einstellungen (Consent Manager: ...)
|
||||
|
||||
www.e-tib.com
|
||||
www.etib-ing.com
|
||||
|
||||
Hier mein instagram account:
|
||||
me.and.eloise
|
||||
Verstehst du mich vielleicht ein kleines Stück mehr…
|
||||
|
||||
Unser Frieder Helmich kann erstes Foto-/Videomaterial zur Verfügung stellen:
|
||||
f.helmich@etib-ing.com
|
||||
|
||||
Lass mir mal eine Idee vom Stundenaufwand / Kosten pro Stunde für Erstellung zukommen,
|
||||
damit wir eine Vertragsgrundlage haben. Danach lass uns loslegen.
|
||||
|
||||
Besten Dank dir.
|
||||
|
||||
Mit freundlichen Grüßen
|
||||
|
||||
Danny Joseph
|
||||
Geschäftsführer
|
||||
|
||||
E-TIB GmbH
|
||||
Gewerbestraße 22
|
||||
D-03172 Guben
|
||||
|
||||
Mobil +49 15207230518
|
||||
E-Mail d.joseph@e-tib.com
|
||||
Web www.e-tib.com
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------------------------
|
||||
Geschäftsführung: Danny Joseph
|
||||
Handelsregister: Amtsgericht Cottbus
|
||||
HRB: 12403 CB
|
||||
USt. ID-Nr.: DE304799919
|
||||
--------------------------------------------------------------------------------------------------
|
||||
|
||||
Von: Marc Mintel <marc@cablecreations.de>
|
||||
Gesendet: Donnerstag, 13. November 2025 16:30
|
||||
An: d.joseph@e-tib.com
|
||||
Betreff: Homepage
|
||||
|
||||
Hi Danny,
|
||||
|
||||
mein Vater meinte, ich könnte mich mal bei dir melden, weil ihr jemanden für eure Website sucht.
|
||||
|
||||
Kurz zu mir: Ich habe über 10 Jahre in der Webentwicklung gearbeitet. Inzwischen liegt mein Schwerpunkt zwar im 3D-Bereich (u. a. cablecreations.de), aber ich betreue weiterhin Websites für Firmen, die das Ganze unkompliziert abgegeben haben möchten. Unter anderem betreue ich auch die Seite von KLZ (klz-cables.com). Der Ablauf ist bei mir recht einfach: Wenn ihr etwas braucht, reicht in der Regel eine kurze Mail – Anpassungen, Inhalte oder technische Themen erledige ich dann im Hintergrund. Dadurch spart ihr euch Schulungen, Zugänge oder lange Meetings, wie man sie oft mit Agenturen hat.
|
||||
|
||||
Wichtig ist: Eine Website braucht auch nach dem Aufbau regelmäßige Pflege, damit Technik und Sicherheit sauber laufen – das übernehme ich dann ebenfalls, damit ihr im Alltag keinen Aufwand damit habt.
|
||||
|
||||
Um einschätzen zu können, ob und wie ich euch unterstützen kann, wäre es gut zu wissen, was ihr mit der Website vorhabt und was an der aktuellen Seite nicht mehr passt. Wenn du magst, können wir dazu auch kurz telefonieren.
|
||||
|
||||
Viele Grüße
|
||||
Marc
|
||||
|
||||
Marc Mintel
|
||||
Founder & 3D Artist
|
||||
marc@cablecreations.de
|
||||
|
||||
Cable Creations
|
||||
www.cablecreations.de
|
||||
info@cablecreations.de
|
||||
VAT: DE367588065
|
||||
|
||||
Georg-Meistermann-Straße 7
|
||||
54586 Schüller
|
||||
Germany
|
||||
80
apps/web/docker-compose.yml
Normal file
80
apps/web/docker-compose.yml
Normal file
@@ -0,0 +1,80 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# Main website - Next.js standalone
|
||||
website:
|
||||
image: registry.infra.mintel.me/mintel/mintel.me:latest
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile
|
||||
args:
|
||||
- NEXT_PUBLIC_ANALYTICS_PROVIDER=${NEXT_PUBLIC_ANALYTICS_PROVIDER}
|
||||
- NEXT_PUBLIC_UMAMI_WEBSITE_ID=${NEXT_PUBLIC_UMAMI_WEBSITE_ID}
|
||||
- NEXT_PUBLIC_UMAMI_HOST_URL=${NEXT_PUBLIC_UMAMI_HOST_URL}
|
||||
- NEXT_PUBLIC_PLAUSIBLE_DOMAIN=${NEXT_PUBLIC_PLAUSIBLE_DOMAIN}
|
||||
- NEXT_PUBLIC_PLAUSIBLE_SCRIPT_URL=${NEXT_PUBLIC_PLAUSIBLE_SCRIPT_URL}
|
||||
- NEXT_PUBLIC_GLITCHTIP_DSN=${NEXT_PUBLIC_GLITCHTIP_DSN}
|
||||
container_name: mintel-website
|
||||
restart: unless-stopped
|
||||
# Port 3000 is internal to the docker network, Caddy will proxy to it.
|
||||
# We can expose it for debugging if needed, but it's safer to keep it internal.
|
||||
expose:
|
||||
- "3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- NEXT_PUBLIC_ANALYTICS_PROVIDER=${NEXT_PUBLIC_ANALYTICS_PROVIDER}
|
||||
- NEXT_PUBLIC_UMAMI_WEBSITE_ID=${NEXT_PUBLIC_UMAMI_WEBSITE_ID}
|
||||
- NEXT_PUBLIC_UMAMI_HOST_URL=${NEXT_PUBLIC_UMAMI_HOST_URL}
|
||||
- NEXT_PUBLIC_PLAUSIBLE_DOMAIN=${NEXT_PUBLIC_PLAUSIBLE_DOMAIN}
|
||||
- NEXT_PUBLIC_PLAUSIBLE_SCRIPT_URL=${NEXT_PUBLIC_PLAUSIBLE_SCRIPT_URL}
|
||||
- NEXT_PUBLIC_GLITCHTIP_DSN=${NEXT_PUBLIC_GLITCHTIP_DSN}
|
||||
depends_on:
|
||||
- redis
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
# Redis cache for performance
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: mintel-redis
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
|
||||
networks:
|
||||
- app-network
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
|
||||
# Caddy reverse proxy with automatic SSL
|
||||
caddy:
|
||||
image: caddy:2-alpine
|
||||
container_name: mintel-caddy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./docker/Caddyfile:/etc/caddy/Caddyfile
|
||||
- caddy-data:/data
|
||||
- caddy-config:/config
|
||||
environment:
|
||||
- DOMAIN=${DOMAIN:-localhost}
|
||||
- EMAIL=${ADMIN_EMAIL:-admin@example.com}
|
||||
depends_on:
|
||||
- website
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
volumes:
|
||||
redis-data:
|
||||
caddy-data:
|
||||
caddy-config:
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
||||
47
apps/web/docker/Caddyfile
Normal file
47
apps/web/docker/Caddyfile
Normal file
@@ -0,0 +1,47 @@
|
||||
# Caddyfile for reverse proxy with automatic SSL
|
||||
{
|
||||
# Email for Let's Encrypt notifications
|
||||
email {$EMAIL:-admin@example.com}
|
||||
}
|
||||
|
||||
# Main website
|
||||
{$DOMAIN:-localhost} {
|
||||
# Reverse proxy to website container
|
||||
reverse_proxy website:3000
|
||||
|
||||
# Security headers
|
||||
header {
|
||||
# Enable HSTS
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
# Prevent clickjacking
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
# Prevent MIME sniffing
|
||||
X-Content-Type-Options "nosniff"
|
||||
# XSS protection
|
||||
X-XSS-Protection "1; mode=block"
|
||||
# Remove server info
|
||||
Server "mintel"
|
||||
}
|
||||
|
||||
# Logging
|
||||
log {
|
||||
output file /var/log/caddy/access.log
|
||||
format json
|
||||
}
|
||||
|
||||
# Compression
|
||||
encode zstd gzip
|
||||
}
|
||||
|
||||
# Analytics subdomain (if using your existing Plausible)
|
||||
analytics.{$DOMAIN:-localhost} {
|
||||
# Point to your existing Plausible instance
|
||||
# Replace with your Plausible server IP/domain
|
||||
reverse_proxy http://YOUR_PLAUSIBLE_SERVER:8000
|
||||
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
X-Content-Type-Options "nosniff"
|
||||
}
|
||||
}
|
||||
63
apps/web/docker/Dockerfile
Normal file
63
apps/web/docker/Dockerfile
Normal file
@@ -0,0 +1,63 @@
|
||||
# Multi-stage build for Next.js
|
||||
FROM node:22-alpine AS base
|
||||
|
||||
# 1. Install dependencies only when needed
|
||||
FROM base AS deps
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies based on the preferred package manager
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm ci
|
||||
|
||||
# 2. Rebuild the source code only when needed
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
# Next.js collects completely anonymous telemetry data about general usage.
|
||||
# Learn more here: https://nextjs.org/telemetry
|
||||
# Uncomment the following line in case you want to disable telemetry during the build.
|
||||
# ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
# Build arguments for environment variables needed at build time
|
||||
ARG NEXT_PUBLIC_ANALYTICS_PROVIDER
|
||||
ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID
|
||||
ARG NEXT_PUBLIC_UMAMI_HOST_URL
|
||||
ARG NEXT_PUBLIC_PLAUSIBLE_DOMAIN
|
||||
ARG NEXT_PUBLIC_PLAUSIBLE_SCRIPT_URL
|
||||
ARG NEXT_PUBLIC_GLITCHTIP_DSN
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# 3. Production image, copy all the files and run next
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
# Uncomment the following line in case you want to disable telemetry during runtime.
|
||||
# ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
# Set the correct permission for prune cache
|
||||
RUN mkdir .next
|
||||
RUN chown nextjs:nodejs .next
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT 3000
|
||||
ENV HOSTNAME "0.0.0.0"
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
46
apps/web/docker/nginx.conf
Normal file
46
apps/web/docker/nginx.conf
Normal file
@@ -0,0 +1,46 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Enable gzip compression
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types
|
||||
text/plain
|
||||
text/css
|
||||
text/xml
|
||||
text/javascript
|
||||
application/javascript
|
||||
application/xml+rss
|
||||
application/json
|
||||
font/woff2
|
||||
image/svg+xml;
|
||||
|
||||
# Cache static assets
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
add_header X-Content-Type-Options "nosniff";
|
||||
}
|
||||
|
||||
# Security headers
|
||||
location / {
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
|
||||
# Try files, fallback to index.html for SPA routing
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Health check endpoint
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "healthy\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
123
apps/web/docs/ABOUT.md
Normal file
123
apps/web/docs/ABOUT.md
Normal file
@@ -0,0 +1,123 @@
|
||||
Über mich
|
||||
|
||||
Ich baue Websites und Systeme seit über 15 Jahren.
|
||||
Nicht weil ich Websites so liebe – sondern weil ich es hasse, wenn Dinge nicht funktionieren.
|
||||
|
||||
In diesen 15 Jahren habe ich:
|
||||
• Agenturen von innen gesehen
|
||||
• Konzerne erlebt
|
||||
• Startups aufgebaut
|
||||
• Marketingversprechen zerlegt
|
||||
• Systeme repariert, die „fertig“ waren
|
||||
• und gelernt, wie man Dinge baut, die einfach laufen
|
||||
|
||||
Heute mache ich das ohne Agentur-Zwischenschichten.
|
||||
Direkt. Sauber. Verantwortlich.
|
||||
|
||||
⸻
|
||||
|
||||
Ich habe fast alle Fehler schon für Sie gemacht
|
||||
|
||||
(damit Sie sie nicht machen müssen)
|
||||
|
||||
Ich habe als Designer angefangen,
|
||||
bin dann Entwickler geworden,
|
||||
und habe irgendwann gemerkt:
|
||||
|
||||
Das Problem ist selten Technik.
|
||||
Es ist immer Zuständigkeit.
|
||||
|
||||
Wenn keiner verantwortlich ist, passiert nichts.
|
||||
Also habe ich mir angewöhnt, Verantwortung zu übernehmen.
|
||||
|
||||
⸻
|
||||
|
||||
Warum ich Websites wie Systeme baue
|
||||
|
||||
Ich war viele Jahre Senior Developer in Firmen, in denen:
|
||||
• Millionenumsätze dranhingen
|
||||
• Fehler teuer waren
|
||||
• Performance nicht optional war
|
||||
• Sicherheit kein Nice-to-Have war
|
||||
• „kurz mal ändern“ trotzdem passieren musste
|
||||
|
||||
Das prägt.
|
||||
|
||||
Deshalb sind meine Websites:
|
||||
• schnell
|
||||
• stabil
|
||||
• boring (im besten Sinne)
|
||||
• erweiterbar
|
||||
• wartungsarm
|
||||
• und nicht abhängig von Plugins oder Agenturen
|
||||
|
||||
⸻
|
||||
|
||||
Ich habe beide Seiten gesehen
|
||||
|
||||
Ich war:
|
||||
• Webdesigner
|
||||
• Entwickler
|
||||
• Marketing
|
||||
• Vertrieb
|
||||
• Agentur
|
||||
• Inhouse
|
||||
• Dienstleister
|
||||
• Unternehmer
|
||||
|
||||
Das heißt:
|
||||
|
||||
Ich weiß, was Unternehmen brauchen –
|
||||
und was sie nicht brauchen.
|
||||
|
||||
(Meetings, Tickets, Workshops, PowerPoint.)
|
||||
|
||||
⸻
|
||||
|
||||
Was Kunden davon haben
|
||||
|
||||
Sie bekommen:
|
||||
• keinen Projektmanager
|
||||
• keinen Prozess
|
||||
• kein Team
|
||||
• kein Ticket
|
||||
• kein CMS-Drama
|
||||
|
||||
Sie bekommen:
|
||||
• eine Person
|
||||
• eine Verantwortung
|
||||
• ein Ergebnis
|
||||
|
||||
⸻
|
||||
|
||||
Ein kurzer Überblick (ohne Lebenslauf-Gefühl)
|
||||
|
||||
Ich habe u. a. gearbeitet bei:
|
||||
• Agenturen
|
||||
• E-Commerce-Plattformen
|
||||
• SaaS-Firmen
|
||||
• Marketing-Teams
|
||||
• internationalen Unternehmen
|
||||
• Mittelständlern
|
||||
• und Konzernen
|
||||
|
||||
Als:
|
||||
• Web Designer
|
||||
• Frontend Developer
|
||||
• Software Developer
|
||||
• Senior Developer
|
||||
• und später Gründer
|
||||
|
||||
Das Ergebnis daraus ist nicht ein Titel.
|
||||
Sondern eine Arbeitsweise.
|
||||
|
||||
⸻
|
||||
|
||||
Heute
|
||||
|
||||
Heute baue ich Websites und Systeme für Unternehmen,
|
||||
die keine Lust mehr auf Agenturen haben
|
||||
und keine Zeit für Chaos.
|
||||
|
||||
Ich übernehme das Thema komplett –
|
||||
damit es für Sie kein Thema mehr ist.
|
||||
154
apps/web/docs/AGBS.md
Normal file
154
apps/web/docs/AGBS.md
Normal file
@@ -0,0 +1,154 @@
|
||||
Allgemeine Geschäftsbedingungen (AGB)
|
||||
|
||||
1. Geltungsbereich
|
||||
|
||||
Diese Allgemeinen Geschäftsbedingungen gelten für alle Verträge zwischen
|
||||
Marc Mintel (nachfolgend „Auftragnehmer“)
|
||||
und dem jeweiligen Kunden (nachfolgend „Auftraggeber“).
|
||||
|
||||
Abweichende oder ergänzende Bedingungen des Auftraggebers werden nicht Vertragsbestandteil, auch wenn ihrer Geltung nicht ausdrücklich widersprochen wird.
|
||||
|
||||
⸻
|
||||
|
||||
2. Vertragsgegenstand
|
||||
|
||||
Der Auftragnehmer erbringt Dienstleistungen im Bereich:
|
||||
• Webentwicklung
|
||||
• technische Umsetzung digitaler Systeme
|
||||
• Funktionen, Schnittstellen und Automatisierungen
|
||||
• Hosting, Betrieb und Wartung, sofern ausdrücklich vereinbart
|
||||
|
||||
Der Auftragnehmer schuldet ausschließlich die vereinbarte technische Leistung, nicht jedoch:
|
||||
• einen wirtschaftlichen Erfolg
|
||||
• bestimmte Umsätze, Conversions oder Reichweiten
|
||||
• Suchmaschinen-Rankings
|
||||
• rechtliche oder geschäftliche Ergebnisse
|
||||
|
||||
⸻
|
||||
|
||||
3. Mitwirkungspflichten des Auftraggebers
|
||||
|
||||
Der Auftraggeber verpflichtet sich, alle zur Leistungserbringung erforderlichen Inhalte, Informationen, Zugänge und Entscheidungen rechtzeitig, vollständig und korrekt bereitzustellen.
|
||||
|
||||
Hierzu zählen insbesondere:
|
||||
• Texte, Bilder, Videos, Produktdaten
|
||||
• Freigaben und Feedback
|
||||
• Zugangsdaten
|
||||
• rechtlich erforderliche Inhalte (z. B. Impressum, Datenschutzerklärung)
|
||||
|
||||
Verzögerungen oder Unterlassungen der Mitwirkung führen zu einer entsprechenden Verschiebung aller Termine.
|
||||
Hieraus entstehen keine Schadensersatz- oder Minderungsansprüche.
|
||||
|
||||
⸻
|
||||
|
||||
4. Ausführungs- und Bearbeitungszeiten
|
||||
|
||||
Angegebene Bearbeitungszeiten sind unverbindliche Schätzungen, keine garantierten Fristen.
|
||||
|
||||
Fixe Termine oder Deadlines gelten nur, wenn sie ausdrücklich schriftlich als verbindlich vereinbart wurden.
|
||||
|
||||
⸻
|
||||
|
||||
5. Abnahme
|
||||
|
||||
Die Leistung gilt als abgenommen, wenn:
|
||||
• der Auftraggeber sie produktiv nutzt oder
|
||||
• innerhalb von 7 Tagen nach Bereitstellung keine wesentlichen Mängel angezeigt werden.
|
||||
|
||||
Optische Abweichungen, Geschmacksfragen oder subjektive Einschätzungen stellen keine Mängel dar.
|
||||
|
||||
⸻
|
||||
|
||||
6. Haftung
|
||||
|
||||
Der Auftragnehmer haftet nur für Schäden, die auf vorsätzlicher oder grob fahrlässiger Pflichtverletzung beruhen.
|
||||
|
||||
Eine Haftung für:
|
||||
• entgangenen Gewinn
|
||||
• Umsatzausfälle
|
||||
• Datenverlust
|
||||
• Betriebsunterbrechungen
|
||||
• mittelbare oder Folgeschäden
|
||||
|
||||
ist ausgeschlossen, soweit gesetzlich zulässig.
|
||||
|
||||
⸻
|
||||
|
||||
7. Verfügbarkeit & Betrieb
|
||||
|
||||
Bei vereinbartem Hosting oder Betrieb schuldet der Auftragnehmer keine permanente Verfügbarkeit.
|
||||
|
||||
Wartungsarbeiten, Updates, Sicherheitsmaßnahmen oder externe Störungen (z. B. Hoster, Netze, Drittanbieter) können zu zeitweisen Einschränkungen führen und begründen keine Haftungsansprüche.
|
||||
|
||||
7a. Betriebs- und Pflegeleistung
|
||||
|
||||
Die Betriebs- und Pflegeleistung ist fester Bestandteil der laufenden Leistungen des Auftragnehmers.
|
||||
|
||||
Sie umfasst ausschließlich:
|
||||
• Sicherstellung des technischen Betriebs der Website
|
||||
• Wartung, Updates und Fehlerbehebung der bestehenden Systeme
|
||||
• Austausch, Korrektur oder Aktualisierung bereits vorhandener Inhalte
|
||||
• Pflege bestehender Datensätze ohne Änderung oder Erweiterung der Datenstruktur
|
||||
|
||||
Nicht Bestandteil der Betriebs- und Pflegeleistung sind insbesondere:
|
||||
• regelmäßige oder fortlaufende Erstellung neuer Inhalte
|
||||
(z. B. Blogartikel, News, Produkte, Seiten)
|
||||
• redaktionelle Tätigkeiten oder Content-Produktion
|
||||
• strategische Inhaltsplanung oder Marketingmaßnahmen
|
||||
• Aufbau neuer Seiten, Features, Funktionen oder Datenmodelle
|
||||
• Serien-, Massen- oder Dauerpflege
|
||||
(z. B. tägliche oder wiederkehrende Inhaltserstellung)
|
||||
|
||||
Die Betriebs- und Pflegeleistung dient ausschließlich der Instandhaltung, Sicherheit und Funktionsfähigkeit der bestehenden Website.
|
||||
|
||||
Leistungen, die darüber hinausgehen, gelten als Neuentwicklung oder Inhaltserstellung und sind gesondert zu beauftragen und zu vergüten.
|
||||
|
||||
⸻
|
||||
|
||||
8. Drittanbieter & externe Systeme
|
||||
|
||||
Der Auftragnehmer übernimmt keine Verantwortung für:
|
||||
• Leistungen, Ausfälle oder Änderungen externer Dienste
|
||||
• APIs, Schnittstellen oder Plattformen Dritter
|
||||
• rechtliche oder technische Änderungen fremder Systeme
|
||||
|
||||
Eine Funktionsfähigkeit kann nur im Rahmen der jeweils aktuellen externen Schnittstellen gewährleistet werden.
|
||||
|
||||
⸻
|
||||
|
||||
9. Inhalte & Rechtliches
|
||||
|
||||
Der Auftraggeber ist allein verantwortlich für:
|
||||
• Inhalte der Website
|
||||
• rechtliche Konformität (DSGVO, Urheberrecht, Wettbewerbsrecht etc.)
|
||||
• bereitgestellte Daten und Medien
|
||||
|
||||
Der Auftragnehmer übernimmt keine rechtliche Prüfung.
|
||||
|
||||
⸻
|
||||
|
||||
10. Vergütung & Zahlungsverzug
|
||||
|
||||
Alle Preise verstehen sich netto zuzüglich gesetzlicher Umsatzsteuer.
|
||||
|
||||
Rechnungen sind, sofern nicht anders vereinbart, innerhalb von 7 Tagen fällig.
|
||||
|
||||
Bei Zahlungsverzug ist der Auftragnehmer berechtigt:
|
||||
• Leistungen auszusetzen
|
||||
• Systeme offline zu nehmen
|
||||
• laufende Arbeiten zu stoppen
|
||||
|
||||
⸻
|
||||
|
||||
11. Kündigung laufender Leistungen
|
||||
|
||||
Laufende Leistungen (z. B. Hosting & Betrieb) können mit einer Frist von 4 Wochen zum Monatsende gekündigt werden, sofern nichts anderes vereinbart ist.
|
||||
|
||||
⸻
|
||||
|
||||
12. Schlussbestimmungen
|
||||
|
||||
Es gilt das Recht der Bundesrepublik Deutschland.
|
||||
Gerichtsstand ist – soweit zulässig – der Sitz des Auftragnehmers.
|
||||
|
||||
Sollte eine Bestimmung dieser AGB unwirksam sein, bleibt die Wirksamkeit der übrigen Regelungen unberührt.
|
||||
73
apps/web/docs/AUTOMATION.md
Normal file
73
apps/web/docs/AUTOMATION.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Routine Automation
|
||||
*Kleine Helfer, die den Alltag deutlich entlasten*
|
||||
|
||||
In vielen mittelständischen Unternehmen fressen wiederkehrende Aufgaben Monat für Monat unzählige Stunden:
|
||||
- Daten aus Dokumenten abtippen
|
||||
- Formulare von Hand ausfüllen
|
||||
- Angebote, Berichte oder Bestätigungen manuell anpassen
|
||||
- Eingehende Anfragen immer wieder neu prüfen und bearbeiten
|
||||
|
||||
Das ist keine wertschöpfende Arbeit.
|
||||
Das ist Routine, die teuer ist, Fehler produziert und gute Mitarbeiter davon abhält, sich um das zu kümmern, was wirklich Umsatz bringt.
|
||||
|
||||
Ich baue genau für diese Routine **einfache, maßgeschneiderte Helfer** – meist mit PDF- oder Excel-Ausgabe, Konfiguratoren oder KI-Dokumenten-Einlesen.
|
||||
Einmal eingerichtet, laufen sie leise im Hintergrund.
|
||||
Kein großes Projekt. Kein monatliches Tool-Abo. Kein „lernen Sie das neue System“.
|
||||
|
||||
### Was das konkret bringen kann – Beispiele aus der Praxis
|
||||
|
||||
- **Schnelle Dokumentenerstellung (PDF-Generatoren)**
|
||||
Kurze Eingaben (Formular, Mail, Excel-Zeile) → fertiges PDF raus: Angebote, Berichte, Protokolle, Bestätigungen, Übersichten. Immer Ihr Corporate Design, immer aktuelle Daten/Bausteine.
|
||||
→ Von 30–120 Minuten runter auf 2–10 Minuten.
|
||||
|
||||
- **Excel-Automatisierungen & smarte Tabellen**
|
||||
Verkaufszahlen, Lagerbestände, Kundenlisten → automatische Berechnungen, Zusammenfassungen, Prognosen oder Exporte. Monatsberichte oder Preislisten aktualisieren sich von selbst.
|
||||
→ Kein ständiges Nachrechnen mehr, keine Versionskonflikte.
|
||||
|
||||
- **Konfiguratoren für Anfragen & Schätzungen**
|
||||
Kunde oder Mitarbeiter geht schrittweise durch ein Formular (auf Ihrer Website oder intern): „Welche Leistung? Welcher Umfang? Welcher Termin?“ → sofort realistische Schätzung, Preisspanne oder fertiges Angebot als PDF/Excel.
|
||||
|
||||
- **KI-Einlesen von PDFs oder handschriftlichen Dokumenten**
|
||||
Eingescannte Rechnungen, Lieferscheine, Formulare, Notizen oder handgeschriebene Protokolle → KI liest Text, Zahlen, Felder aus (auch Handschrift, wo lesbar) → Daten landen in übersichtlicher Tabelle/Excel oder vorausgefülltem Formular.
|
||||
Mitarbeiter prüft nur noch kurz → kleine Korrektur → Prozess geht weiter.
|
||||
→ Kein stundenlanges Abtippen mehr, deutlich schnellerer Durchlauf.
|
||||
|
||||
### Der echte Wert für Sie
|
||||
|
||||
- 30–80 % weniger Zeit bei Routineaufgaben → Ihre Teams konzentrieren sich aufs Wesentliche
|
||||
- Weniger Fehler & Rückfragen → einheitlicher, professioneller Output
|
||||
- Schnellere Reaktion auf Kunden → Konfiguratoren & KI-Einlesen liefern sofort Infos
|
||||
- Amortisation oft schon nach wenigen Wochen oder Dutzend Nutzungen
|
||||
- Nutzt, was Sie bereits haben: Website, Excel, Mail, Scanner-App
|
||||
|
||||
### Was ich **nicht** mache
|
||||
|
||||
Ich ersetze **kein** ERP, CRM, Buchhaltungs- oder HR-System.
|
||||
Kein automatisches Buchen, keine Finanzamtschnittstelle, keine GoBD-Archivierungspflichten.
|
||||
Nur smarte Abkürzungen bei Routine – der Rest bleibt in Ihren bewährten Tools.
|
||||
|
||||
### Ich kann Ihnen helfen, wenn Sie mit diesen typischen Problemen kämpfen
|
||||
|
||||
- „Wir tippen immer noch Daten aus gescannten Dokumenten oder handschriftlichen Notizen ab.“
|
||||
- „Angebote, Berichte oder Protokolle dauern ewig, weil alles von Hand angepasst wird.“
|
||||
- „Kunden fragen ständig dasselbe – wir antworten jedes Mal manuell.“
|
||||
- „Excel-Tabellen und Berechnungen werden ständig neu gemacht und gehen kaputt.“
|
||||
- „Bis wir eine realistische Schätzung oder ein Angebot raus haben, vergeht zu viel Zeit.“
|
||||
|
||||
Schreiben Sie mir einfach einen kurzen Satz zu Ihrem größten Zeitfresser in diesem Bereich.
|
||||
Ich antworte meist innerhalb von 1–2 Tagen:
|
||||
- Ist das machbar? Ja/Nein
|
||||
- Ca. wie viel Aufwand (meist 3–15 Stunden) & Preisrahmen
|
||||
- Was Sie realistisch sparen können (Zeit, Nerven, Fehler)
|
||||
|
||||
Passt es → baue ich es.
|
||||
Danach: Routine digitalisiert. Mehr Ruhe im Alltag.
|
||||
|
||||
**Kurz gesagt**
|
||||
Routine Automation:
|
||||
Nicht die große Revolution.
|
||||
Sondern gezielte Entlastung bei den Dingen, die jeden Tag Zeit und Nerven kosten.
|
||||
Mehr Zeit. Weniger Frust. Besserer Output.
|
||||
Und das Gefühl: „Das läuft jetzt einfach.“
|
||||
|
||||
Wenn bei Ihnen gerade etwas „von Hand gemacht wird“ oder „ewig dauert“ – Ich sage Ihnen, ob und wie schnell man das sinnvoll digitalisieren kann.
|
||||
83
apps/web/docs/ESTIMATION_GUIDE.md
Normal file
83
apps/web/docs/ESTIMATION_GUIDE.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Service Estimation & AI Consultation Guide
|
||||
|
||||
This guide explains how to use the automated estimation system to generate professional PDF quotes for clients using AI-driven context analysis.
|
||||
|
||||
## 🛠 Basic Usage
|
||||
|
||||
The primary entry point is the `ai-estimate` script. It orchestrates a 6-pass AI consultation:
|
||||
1. **Fact Extraction**: Identifying company data and project scope.
|
||||
2. **Feature Deep-Dive**: Generating technical justifications for items.
|
||||
3. **Strategic Content**: Creating the Briefing Analysis and Strategic Vision.
|
||||
4. **Information Architecture**: Designing a hierarchical sitemap.
|
||||
5. **Position Synthesis**: Mapping everything to a transparent pricing model.
|
||||
6. **Industrial Critic**: Final quality gate for tone and accuracy.
|
||||
|
||||
### Generating an Estimation from Scratch
|
||||
|
||||
#### 1. With a Website URL (Recommended)
|
||||
Providing a URL allows the system to crawl the existing site to understand the "Company DNA" and services.
|
||||
```bash
|
||||
npm run ai-estimate -- "Relaunch der Website mit Fokus auf B2B Leads" --url https://example.com
|
||||
```
|
||||
|
||||
#### 2. From a Text File
|
||||
If you have a long briefing in a `.txt` file:
|
||||
```bash
|
||||
npm run ai-estimate -- @briefing.txt --url https://example.com
|
||||
```
|
||||
|
||||
#### 3. Text-Only (No URL)
|
||||
If no URL is provided, the system relies entirely on your briefing text.
|
||||
```bash
|
||||
npm run ai-estimate -- "Neuentwicklung eines Portals für XYZ"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📄 Output Modes
|
||||
|
||||
The system can generate two types of documents:
|
||||
|
||||
### 1. Full Quote (Default)
|
||||
Includes everything: Front Page, Briefing Analysis, Vision, Sitemap, Technical Principles, Detailed Pricing, Roadmap, and Legal Terms (AGB).
|
||||
```bash
|
||||
npm run ai-estimate -- "Project Briefing"
|
||||
```
|
||||
|
||||
### 2. Estimation Only
|
||||
A condensed version excluding legal terms and deep technical principles. Focuses purely on the strategic fit and the price.
|
||||
```bash
|
||||
npm run ai-estimate -- "Project Briefing" --estimation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Cache & Cache Management
|
||||
|
||||
To save costs and time, all AI responses and crawl results are cached in the `.cache` directory.
|
||||
|
||||
### Regenerating with Cached Data
|
||||
If you run the same command again (identical briefing and URL), the system will use the cached results and won't call the AI APIs again. This is useful if you want to tweak the PDF layout without spending tokens.
|
||||
|
||||
### Forcing a Refresh
|
||||
To ignore the cache and get a fresh AI consultation:
|
||||
```bash
|
||||
npm run ai-estimate -- "Project Briefing" --clear-cache
|
||||
```
|
||||
|
||||
### Manual Tweaking (JSON State)
|
||||
Every run saves a detailed state to `out/estimations/json/[Company]_[Timestamp].json`.
|
||||
If you want to manually edit the AI's results (e.g., fix a typo in the sitemap or description), you can edit this JSON file and then regenerate the PDF from it:
|
||||
|
||||
```bash
|
||||
npm run ai-estimate -- --json out/estimations/json/Your_Project.json
|
||||
```
|
||||
*(Add `--estimation` if you want the condensed version).*
|
||||
|
||||
---
|
||||
|
||||
## 💡 Advanced Options
|
||||
|
||||
- `--comments "..."`: Add manual notes that the AI should consider (e.g., "Customer prefers a minimalist blue theme").
|
||||
- `--clear-cache`: Purges all cached data for this project before starting.
|
||||
- `--url [URL]`: Explicitly sets the crawl target (auto-discovered from briefing if omitted).
|
||||
238
apps/web/docs/LANDING_PAGE.md
Normal file
238
apps/web/docs/LANDING_PAGE.md
Normal file
@@ -0,0 +1,238 @@
|
||||
Digitale Systeme für Unternehmen, die keinen Overhead wollen
|
||||
|
||||
Agenturen sind zu langsam.
|
||||
CMS will keiner pflegen.
|
||||
Digitale Themen bleiben liegen.
|
||||
|
||||
Ich mache das anders.
|
||||
|
||||
⸻
|
||||
|
||||
Was ich mache
|
||||
|
||||
Ich setze digitale Systeme für Unternehmen um – direkt, sauber und ohne Agentur-Zirkus.
|
||||
|
||||
Websites, Funktionen, Systeme, interne Tools.
|
||||
Keine Workshops. Keine Tickets. Kein Tech-Blabla.
|
||||
|
||||
Sie erklären mir, was Sie brauchen.
|
||||
Ich sorge dafür, dass es funktioniert.
|
||||
|
||||
⸻
|
||||
|
||||
Für wen das ist
|
||||
|
||||
Für Unternehmen, die:
|
||||
• regelmäßig Änderungen an Website oder Systemen brauchen
|
||||
• keine Lust auf Agenturen haben
|
||||
• kein CMS anfassen wollen
|
||||
• keine Tickets schreiben möchten
|
||||
• keinen Entwickler einstellen wollen
|
||||
• und wollen, dass Dinge einfach erledigt werden
|
||||
|
||||
Wenn bei Ihnen öfter der Satz fällt:
|
||||
|
||||
„Das müsste man mal machen …“
|
||||
|
||||
… aber es passiert nie – dann sind Sie hier richtig.
|
||||
|
||||
⸻
|
||||
|
||||
Das eigentliche Problem
|
||||
|
||||
Digitale Arbeit scheitert nicht an Technik.
|
||||
Sie scheitert an Zuständigkeit.
|
||||
|
||||
Agenturen wollen Projekte.
|
||||
Mitarbeiter haben Wichtigeres zu tun.
|
||||
IT ist ausgelastet.
|
||||
Und kleine Aufgaben sind zu klein für große Angebote.
|
||||
|
||||
Also bleibt alles liegen.
|
||||
|
||||
⸻
|
||||
|
||||
Warum keine Agentur
|
||||
|
||||
Ich habe über 15 Jahre in Agenturen gearbeitet.
|
||||
Ich kenne das Spiel. Und ich weiß, warum es nervt.
|
||||
|
||||
⸻
|
||||
|
||||
Agenturen machen einfache Dinge kompliziert
|
||||
|
||||
Ein Button ändern?
|
||||
|
||||
→ Konzeptcall
|
||||
→ Abstimmung
|
||||
→ internes Meeting
|
||||
→ Angebot
|
||||
→ Warten
|
||||
→ Rechnung
|
||||
|
||||
Ich:
|
||||
→ mache es
|
||||
→ fertig
|
||||
|
||||
⸻
|
||||
|
||||
Agenturen verkaufen Prozesse statt Ergebnisse
|
||||
|
||||
Workshops, Slides, Roadmaps, Alignment, Stakeholder.
|
||||
Klingt nach Fortschritt.
|
||||
Ist oft nur Beschäftigungstherapie.
|
||||
|
||||
Bei mir zählt nur Umsetzung.
|
||||
|
||||
⸻
|
||||
|
||||
Agenturen rechnen nach Stunden
|
||||
|
||||
(und liegen erstaunlich oft daneben)
|
||||
|
||||
„Das dauert nur kurz“
|
||||
→ Überraschung auf der Rechnung
|
||||
|
||||
Ich arbeite mit klaren Leistungen und Fixpreisen.
|
||||
Sie wissen vorher, was es kostet. Immer.
|
||||
|
||||
⸻
|
||||
|
||||
Agenturen geben Aufgaben weiter
|
||||
|
||||
(und verlieren sie dann aus den Augen)
|
||||
|
||||
Heute Projektmanager.
|
||||
Morgen Entwickler.
|
||||
Übermorgen niemand.
|
||||
|
||||
Bei mir gilt:
|
||||
Eine Person. Eine Verantwortung.
|
||||
|
||||
⸻
|
||||
|
||||
Agenturen verschwinden nach dem Projekt
|
||||
|
||||
Kleine Änderung? → neues Angebot
|
||||
Dringend? → Warteschleife
|
||||
|
||||
Ich bleibe.
|
||||
Solange Sie Dinge brauchen.
|
||||
|
||||
⸻
|
||||
|
||||
Wie ich arbeite (und warum das entspannter ist)
|
||||
|
||||
Agenturen machen erst ein Konzept.
|
||||
Dann wird umgesetzt.
|
||||
Dann merkt man: „Passt doch nicht ganz.“
|
||||
Dann wird nachberechnet.
|
||||
|
||||
Ich mache es anders.
|
||||
|
||||
⸻
|
||||
|
||||
Ich baue zuerst. Dann reden wir drüber.
|
||||
|
||||
Sie erklären mir Ihre Vorstellung.
|
||||
Ich setze den ersten echten Stand um.
|
||||
|
||||
Keine Slides.
|
||||
Kein Konzept-PDF.
|
||||
Kein Ratespiel.
|
||||
|
||||
Dann arbeiten wir direkt am Ergebnis, bis es passt.
|
||||
Ohne jedes Mal ein neues Angebot.
|
||||
Ohne Scope-Diskussionen.
|
||||
Ohne Theater.
|
||||
|
||||
⸻
|
||||
|
||||
Was ich konkret umsetze
|
||||
|
||||
Websites
|
||||
• neue Websites (klarer Standard, kein Chaos)
|
||||
• bestehende Websites übernehmen
|
||||
• Seiten ändern oder ergänzen
|
||||
• Performance & SEO
|
||||
• Hosting & Betrieb (inklusive)
|
||||
|
||||
⸻
|
||||
|
||||
Funktionen & Systeme
|
||||
• Produktbereiche
|
||||
• Blogs, News, Jobs
|
||||
• Formulare (auch mehrstufig)
|
||||
• Downloads
|
||||
• Suche & Filter
|
||||
• PDF-Generatoren
|
||||
• API-Ausgaben & Daten-Sync
|
||||
• Sonderlogik
|
||||
|
||||
⸻
|
||||
|
||||
Interne Tools
|
||||
• kleine Inhouse-Tools
|
||||
• Excel ersetzen
|
||||
• Importe & Exporte
|
||||
• Automatisierung
|
||||
• Dinge, die nerven → weg
|
||||
|
||||
⸻
|
||||
|
||||
Was ich bewusst nicht mache
|
||||
• keine CMS-Schulungen
|
||||
• keine Agentur-Workshops
|
||||
• keine Ticketsysteme
|
||||
• keine Stundenabrechnung für Websites
|
||||
• kein Overhead
|
||||
• keine Prozessshows
|
||||
|
||||
Das ist kein Mangel.
|
||||
Das ist der Vorteil.
|
||||
|
||||
⸻
|
||||
|
||||
Preise (klar & transparent)
|
||||
|
||||
Ich arbeite mit festen Leistungen und Fixpreisen.
|
||||
Keine Abos. Keine Überraschungen.
|
||||
|
||||
Grundlage
|
||||
• Website-Basis → 6.000 €
|
||||
• Hosting & Betrieb → 120 € / Monat (inkl. 20 GB Medien)
|
||||
|
||||
Entwicklung
|
||||
• Seite → 800 €
|
||||
• Feature (System) → 2.000 €
|
||||
• Funktion (Logik) → 1.000 €
|
||||
|
||||
Inhalte
|
||||
• Neuer Datensatz → 400 €
|
||||
• Datensatz anpassen → 200 €
|
||||
|
||||
Optional
|
||||
• CMS-Einrichtung → 1.500 €
|
||||
• CMS-Anbindung pro Feature → 800 €
|
||||
• Speichererweiterung → +10 € / 10 GB
|
||||
|
||||
Sie wissen vorher, was es kostet. Immer.
|
||||
|
||||
⸻
|
||||
|
||||
Warum Kunden bleiben
|
||||
• Dinge passieren schnell
|
||||
• Aufgaben verschwinden wirklich
|
||||
• kein Erklären
|
||||
• kein Nachfassen
|
||||
• kein Projektstress
|
||||
• kein Agentur-Zirkus
|
||||
|
||||
Kurz: Ruhe.
|
||||
|
||||
⸻
|
||||
|
||||
Interesse?
|
||||
|
||||
Schreiben Sie mir einfach, was Sie brauchen.
|
||||
Ich sage Ihnen ehrlich, ob ich es mache – und was es kostet.
|
||||
234
apps/web/docs/PRICING.md
Normal file
234
apps/web/docs/PRICING.md
Normal file
@@ -0,0 +1,234 @@
|
||||
Preise
|
||||
|
||||
1. Website – Fixpreis
|
||||
|
||||
Basis
|
||||
|
||||
4.000 € einmalig
|
||||
|
||||
Die Grundlage für jede Website:
|
||||
• Projekt-Setup & Infrastruktur
|
||||
• Hosting-Bereitstellung
|
||||
• Grundstruktur & Design-Vorlage
|
||||
• technisches SEO-Basics
|
||||
• Analytics (mit automatischem Mail-Report)
|
||||
• Testing, Staging, Production Umgebung
|
||||
• Livegang
|
||||
|
||||
Enthält keine Seiten, Inhalte oder Funktionen.
|
||||
|
||||
⸻
|
||||
|
||||
2. Entwicklung (Produktion)
|
||||
|
||||
Seite
|
||||
|
||||
600 € / Seite
|
||||
|
||||
Individuell gestaltete Seite –
|
||||
mit Layout, Struktur, Textaufteilung, responsivem Design.
|
||||
|
||||
⸻
|
||||
|
||||
Feature (System)
|
||||
|
||||
1.500 € / Feature
|
||||
|
||||
Ein in sich geschlossenes System mit Datenstruktur, Darstellung und Pflegefähigkeit.
|
||||
|
||||
Typische Beispiele:
|
||||
• Produktbereich
|
||||
• Blog
|
||||
• News
|
||||
• Jobs
|
||||
• Referenzen
|
||||
• Events
|
||||
|
||||
Ein Feature erzeugt ein Datenmodell, Übersichten & Detailseiten.
|
||||
|
||||
⸻
|
||||
|
||||
Funktion (Logik)
|
||||
|
||||
800 € / Funktion
|
||||
|
||||
Funktionen liefern Logik und Interaktion, z. B.:
|
||||
• Kontaktformular
|
||||
• Mailversand
|
||||
• Suche
|
||||
• Filter
|
||||
• Mehrsprachigkeit (System)
|
||||
• PDF-Export von Daten
|
||||
• API-Anbindungen (z. B. Produkt-Sync)
|
||||
• Redirect-Logik
|
||||
• Automatisierte Aufgaben
|
||||
|
||||
Jede Funktion ist ein klar umrissener Logikbaustein.
|
||||
|
||||
⸻
|
||||
|
||||
3. Visuelle Inszenierung & Interaktion
|
||||
|
||||
(Hier geht es um Design/UX-Extras, nicht um „Standard-Design“.)
|
||||
|
||||
Visuelle Inszenierung
|
||||
|
||||
1.500 € / Abschnitt
|
||||
|
||||
Erweiterte Gestaltung:
|
||||
• Hero-Story
|
||||
• visuelle Abläufe
|
||||
• Scroll-Effekte
|
||||
• speziell inszenierte Sektionen
|
||||
|
||||
⸻
|
||||
|
||||
Komplexe Interaktion
|
||||
|
||||
1.500 € / Interaktion
|
||||
|
||||
Dargestellte, interaktive UI-Erlebnisse:
|
||||
• Konfiguratoren
|
||||
• Live-Previews
|
||||
• mehrstufige Auswahlprozesse
|
||||
|
||||
(Nutzt deine bestehenden Bausteine, gehört aber zur Entwicklung.)
|
||||
|
||||
⸻
|
||||
|
||||
4. Inhalte & Medien
|
||||
|
||||
Neuer Datensatz
|
||||
|
||||
200 € / Stück
|
||||
|
||||
Beispiele:
|
||||
• Produkt
|
||||
• Blogpost
|
||||
• News
|
||||
• Case
|
||||
• Job
|
||||
|
||||
Datensätze enthalten Inhalte mit Text, Medien, Metadaten.
|
||||
|
||||
⸻
|
||||
|
||||
Datensatz anpassen
|
||||
|
||||
200 € / Stück
|
||||
• Textupdates
|
||||
• Bildwechsel
|
||||
• Feldänderungen (ohne Schemaänderung)
|
||||
|
||||
⸻
|
||||
|
||||
5. Betrieb & Wartung (Pflicht)
|
||||
|
||||
Hosting & Betrieb
|
||||
|
||||
12 Monate = 3.000 €
|
||||
|
||||
Sichert:
|
||||
• Webhosting & Verfügbarkeit
|
||||
• Sicherheitsupdates
|
||||
• Backups & Monitoring
|
||||
• Analytics inkl. Reports
|
||||
• Medien-Speicher (Standard bis 20 GB)
|
||||
|
||||
⸻
|
||||
|
||||
6. Speicher-Erweiterung (optional)
|
||||
|
||||
Mehr Speicher
|
||||
|
||||
+10 € / Monat → +10 GB (aber nur 20/100/200 GB)
|
||||
|
||||
Wenn das inklusive Volumen überschritten wird, wird automatisch erweitert.
|
||||
|
||||
(Keine Leistungsdiskussion – nur Infrastruktur.)
|
||||
|
||||
⸻
|
||||
|
||||
7. Headless CMS (optional)
|
||||
|
||||
CMS-Einrichtung
|
||||
|
||||
1.500 € einmalig
|
||||
|
||||
Einrichtung eines Headless CMS:
|
||||
• Struktur
|
||||
• Rollen
|
||||
• Rechte
|
||||
• API-Anbindung
|
||||
• Deployment
|
||||
• kurze Einführung
|
||||
|
||||
⸻
|
||||
|
||||
CMS-Anbindung pro Feature
|
||||
|
||||
800 € / Feature
|
||||
|
||||
Erlaubt, dass Datensätze (z. B. Blog, News) im CMS gepflegt werden.
|
||||
Seiten & Layout bleiben bei dir.
|
||||
|
||||
⸻
|
||||
|
||||
8. App / interne Software
|
||||
|
||||
Entwicklung nach Zeit
|
||||
|
||||
120 € / Stunde
|
||||
|
||||
Für:
|
||||
• interne Tools
|
||||
• Prozesslogik
|
||||
• Workflows
|
||||
• Automatisierungen
|
||||
• alles, was Zustände und Abläufe beinhaltet
|
||||
|
||||
(Kein Fixpreis, weil scope offen ist.)
|
||||
|
||||
⸻
|
||||
|
||||
9. Integrationen (optional)
|
||||
|
||||
API-Schnittstelle / Daten-Sync
|
||||
|
||||
800 € / Zielsystem
|
||||
|
||||
Synchronisation zu externem System (Push):
|
||||
• Produkt-Sync
|
||||
• CRM / ERP / Stripe / sonstige API
|
||||
|
||||
Nicht enthalten:
|
||||
• Betrieb fremder Systeme
|
||||
• Echtzeit-Pull-Mechanismen
|
||||
• Zustandsabhängige Syncs
|
||||
|
||||
⸻
|
||||
|
||||
10. Wichtige Regeln
|
||||
|
||||
Seiten = Entwicklung
|
||||
Datensätze = Inhalte & Pflege
|
||||
Features = Daten-Systeme
|
||||
Funktionen = Logik
|
||||
CMS-Anbindung = optionale Datenpflege über Schnittstelle
|
||||
Betrieb = Hosting, Updates, Backups, Analytics
|
||||
Apps = Stunden (Prozesse & Systeme außerhalb der Website)
|
||||
|
||||
⸻
|
||||
|
||||
Leistungsausschlüsse (kurz und klar)
|
||||
• Kein Betrieb von Mail-Servern
|
||||
• Keine Logistik, kein Shop-Checkout
|
||||
• Kein Drittanbieter-Betrieb
|
||||
• Keine permanente Überwachung fremder Systeme
|
||||
|
||||
⸻
|
||||
|
||||
Satz für Kundenkommunikation
|
||||
|
||||
Ich baue digitale Systeme mit klaren Preisen und Ergebnissen –
|
||||
keine Stunden, keine Überraschungen.
|
||||
43
apps/web/docs/PRINCIPLES.md
Normal file
43
apps/web/docs/PRINCIPLES.md
Normal file
@@ -0,0 +1,43 @@
|
||||
Prinzipien
|
||||
|
||||
Ich arbeite nach klaren Grundsätzen, die sicherstellen, dass meine Kunden fair, transparent und langfristig profitieren.
|
||||
|
||||
⸻
|
||||
|
||||
1. Volle Preis-Transparenz
|
||||
Alle Kosten sind offen und nachvollziehbar.
|
||||
Es gibt keine versteckten Gebühren, keine Abos, keine Lock-ins.
|
||||
Jeder Kunde sieht genau, wofür er bezahlt.
|
||||
|
||||
⸻
|
||||
|
||||
2. Quellcode & Projektzugang
|
||||
Auf Wunsch erhalten Kunden jederzeit den vollständigen Source Code und eine nachvollziehbare Struktur.
|
||||
Damit kann jeder andere Entwickler problemlos weiterarbeiten.
|
||||
Niemand kann später behaupten, der Code sei „Messy“ oder unbrauchbar.
|
||||
|
||||
⸻
|
||||
|
||||
3. Best Practices & saubere Technik
|
||||
Ich setze konsequent bewährte Standards und dokumentierte Abläufe ein.
|
||||
Das sorgt dafür, dass Systeme wartbar, verständlich und erweiterbar bleiben – langfristig.
|
||||
|
||||
⸻
|
||||
|
||||
4. Verantwortung & Fairness
|
||||
Ich übernehme die technische Verantwortung für die Website.
|
||||
Ich garantiere keine Umsätze, Rankings oder rechtliche Ergebnisse – nur saubere Umsetzung und stabile Systeme.
|
||||
Wenn etwas nicht sinnvoll ist, sage ich es ehrlich.
|
||||
|
||||
⸻
|
||||
|
||||
5. Langfristiger Wert
|
||||
Eine Website ist ein Investment.
|
||||
Ich baue sie so, dass Anpassungen, Erweiterungen und Übergaben an andere Entwickler problemlos möglich sind.
|
||||
Das schützt Ihre Investition und vermeidet teure Neuaufbauten.
|
||||
|
||||
⸻
|
||||
|
||||
6. Zusammenarbeit ohne Tricks
|
||||
Keine künstlichen Deadlines, kein unnötiger Overhead.
|
||||
Kommunikation ist klar, Entscheidungen nachvollziehbar, Übergaben sauber dokumentiert.
|
||||
74
apps/web/docs/STYLEGUIDE.md
Normal file
74
apps/web/docs/STYLEGUIDE.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Style Guide: Digital Architect
|
||||
|
||||
This document defines the visual language and design rules for the Mintel ecosystem. The goal is to maintain a "Digital Architect" aesthetic: technical, reductionist, and high-fidelity.
|
||||
|
||||
## 1. Core Philosophy: Reduction to Essentials
|
||||
|
||||
The design should feel "Websites ohne Overhead." Every element must serve a purpose. If it doesn't add value, remove it.
|
||||
|
||||
- **Technical Precision**: Use grids, mono-spaced labels, and clear hierarchies to signal technical competence.
|
||||
- **Tactile Digital Objects**: UI elements should feel like physical objects (buttons that depress, tags that pop in, glass that blurs).
|
||||
- **High Contrast**: Bold black/slate on clean white backgrounds, with vibrant highlighter accents.
|
||||
|
||||
## 2. Color Palette
|
||||
|
||||
The project uses a monochrome base with curated highlighter accents.
|
||||
|
||||
- **Primary Base**:
|
||||
- **Backgrounds**: Pure White (`#ffffff`) for clarity.
|
||||
- **Surfaces**: Slate-50 for subtle depth, White with 90% opacity + 10px blur for glassmorphism.
|
||||
- **Grays (Slate)**:
|
||||
- **Text**: Slate-800 for body, Slate-900 for headings.
|
||||
- **UI Borders**: Slate-100 or Slate-200.
|
||||
- **Muted text**: Slate-400 or Slate-500.
|
||||
- **Highlighter Accents**: Used exclusively for tags, markers, and selective emphasis.
|
||||
- **Yellow**: Warm, high-visibility (`rgba(255, 235, 59, 0.95)`).
|
||||
- **Pink**: Vibrant, energetic (`rgba(255, 167, 209, 0.95)`).
|
||||
- **Green**: Success, technical health (`rgba(129, 199, 132, 0.95)`).
|
||||
- **Blue**: Neutral, structural (`rgba(226, 232, 240, 0.95)`).
|
||||
|
||||
## 3. Typography
|
||||
|
||||
A high-contrast mix of fonts that balances modern tech with editorial readability.
|
||||
|
||||
- **Headings (Sans-serif)**: Use **Inter**.
|
||||
- Tracking: `-0.025em` to `-0.05em` (tracking-tighter).
|
||||
- Weight: Bold (`700`).
|
||||
- Color: Slate-900.
|
||||
- **Body (Serif)**: Use **Newsreader** or Georgia.
|
||||
- Style: Defaults to serif for long-form content to provide a "notebook" feel.
|
||||
- Line-height: Relaxed (`1.6` to `1.75`).
|
||||
- **Technical (Monospace)**: Use **JetBrains Mono**.
|
||||
- Usage: Small labels, tags, code snippets, and "Mono-Labels" (e.g., section numbers).
|
||||
- Feature: Uppercase with wide tracking (`0.3em` to `0.4em`).
|
||||
|
||||
## 4. Layout & Rhythm
|
||||
|
||||
Standardized containers ensure consistency across different screen sizes.
|
||||
|
||||
- **Standard Container**: Max-width 6xl (`72rem`). Used for most page sections.
|
||||
- **Wide Container**: Max-width 7xl (`80rem`). Used for galleries or high-impact visuals.
|
||||
- **Narrow Container**: Max-width 4xl (`56rem`). Used for focused reading and blog posts.
|
||||
- **Section Rhythm**: Sections are separated by clear `border-top` lines and numbered (e.g., "01", "02").
|
||||
|
||||
## 5. UI Elements & Interactions
|
||||
|
||||
### 5.1 Buttons
|
||||
- **Shape**: Always pill-shaped (rounded-full).
|
||||
- **Style**: Thin borders (`1px`) with bold, uppercase mono-spaced text.
|
||||
- **Hover**: Should feel "expensive." Smooth translate-up (`-0.5rem`) and deep, soft shadows.
|
||||
|
||||
### 5.2 Cards & Containers
|
||||
- **Glassmorphism**: Use for search boxes and floating elements (`backdrop-filter: blur(10px)`).
|
||||
- **Cards**: Minimalist. Use `Slate-50` or thin `Slate-100` borders. Avoid heavy shadows unless on hover.
|
||||
|
||||
### 5.3 Highlighters & Tags
|
||||
- **Marker Effect**: Use a hand-drawn marker underline (diagonal skew, slightly erratic rotation) for key titles.
|
||||
- **Tags**: Small, bold, uppercase. They should use `tagPopIn` animations when appearing.
|
||||
|
||||
## 6. Motion & Atmosphere
|
||||
|
||||
- **Reveals**: Content should never "just appear." Use slide-up and fade-in transitions (`0.5s` to `0.7s`) to create a sense of discovery.
|
||||
- **Background Grid**: A subtle, low-opacity grid pattern provides a technical "blueprint" feel to the pages.
|
||||
- **Micro-interactions**: Hovering over icons or tags should trigger subtle scales (`105%-110%`) and color shifts.
|
||||
|
||||
98
apps/web/docs/TECH.md
Normal file
98
apps/web/docs/TECH.md
Normal file
@@ -0,0 +1,98 @@
|
||||
Wie ich Websites technisch umsetze
|
||||
|
||||
Ich entwickle Websites als moderne, performante Websysteme – nicht als Baukasten-Seiten und nicht als schwer wartbare CMS-Konstrukte.
|
||||
Der Fokus liegt auf Geschwindigkeit, Stabilität, Datenschutz und langfristiger Wartbarkeit.
|
||||
|
||||
Die Technik dient dabei immer einem Zweck:
|
||||
Ihre Website soll zuverlässig funktionieren, schnell laden und kein laufendes Risiko darstellen.
|
||||
|
||||
⸻
|
||||
|
||||
Geschwindigkeit & Performance
|
||||
|
||||
Meine Websites sind so aufgebaut, dass Inhalte extrem schnell ausgeliefert werden – unabhängig davon, ob ein Besucher am Desktop oder mobil unterwegs ist.
|
||||
|
||||
Das bedeutet für Sie:
|
||||
• kurze Ladezeiten
|
||||
• bessere Nutzererfahrung
|
||||
• messbar bessere Werte bei Google PageSpeed & Core Web Vitals
|
||||
• geringere Absprungraten
|
||||
|
||||
Die Seiten werden nicht „zusammengeklickt“, sondern technisch optimiert ausgeliefert.
|
||||
|
||||
⸻
|
||||
|
||||
Responsives Design (ohne Kompromisse)
|
||||
|
||||
Jede Website ist von Grund auf responsiv.
|
||||
Layout, Inhalte und Funktionen passen sich automatisch an:
|
||||
• Smartphones
|
||||
• Tablets
|
||||
• Laptops
|
||||
• große Bildschirme
|
||||
|
||||
Dabei wird nicht einfach skaliert, sondern gezielt für unterschiedliche Bildschirmgrößen optimiert.
|
||||
Das Ergebnis ist eine saubere Darstellung und gute Bedienbarkeit auf allen Geräten.
|
||||
|
||||
⸻
|
||||
|
||||
Stabilität & Betriebssicherheit
|
||||
|
||||
Im Hintergrund laufen Überwachungs- und Kontrollmechanismen, die technische Probleme automatisch erkennen.
|
||||
|
||||
Für Sie heißt das:
|
||||
• Fehler werden bemerkt, auch wenn niemand sie meldet
|
||||
• ich werde aktiv informiert, statt erst zu reagieren, wenn etwas kaputt ist
|
||||
• Probleme können frühzeitig behoben werden
|
||||
|
||||
Das reduziert Ausfälle und vermeidet unangenehme Überraschungen.
|
||||
|
||||
⸻
|
||||
|
||||
Datenschutz & DSGVO
|
||||
|
||||
Ich setze konsequent auf freie, selbst betriebene Software statt auf große externe Plattformen.
|
||||
|
||||
Ihre Vorteile:
|
||||
• keine Weitergabe von Nutzerdaten an Dritte
|
||||
• keine versteckten Tracker
|
||||
• keine Abhängigkeit von US-Anbietern
|
||||
• datenschutzfreundliche Statistik ohne Cookies
|
||||
|
||||
Die Website bleibt technisch schlank und rechtlich kontrollierbar.
|
||||
|
||||
⸻
|
||||
|
||||
Unabhängigkeit & Kostenkontrolle
|
||||
|
||||
Da ich keine proprietären Systeme oder Lizenzmodelle einsetze:
|
||||
• entstehen keine laufenden Tool-Gebühren
|
||||
• gibt es keine plötzlichen Preiserhöhungen
|
||||
• bleibt die Website langfristig planbar betreibbar
|
||||
|
||||
Sie zahlen für die Leistung – nicht für Lizenzen oder Marken.
|
||||
|
||||
⸻
|
||||
|
||||
Wartbarkeit & Erweiterbarkeit
|
||||
|
||||
Die technische Struktur ist so aufgebaut, dass:
|
||||
• Inhalte erweitert werden können
|
||||
• Funktionen sauber ergänzt werden können
|
||||
• Anpassungen nicht das ganze System gefährden
|
||||
|
||||
Das schützt Ihre Investition und verhindert teure Neuaufbauten nach kurzer Zeit.
|
||||
|
||||
⸻
|
||||
|
||||
Kurz gesagt
|
||||
|
||||
Ich baue Websites, die:
|
||||
• schnell sind
|
||||
• auf allen Geräten sauber funktionieren
|
||||
• datenschutzkonform betrieben werden
|
||||
• technisch überwacht sind
|
||||
• langfristig wartbar bleiben
|
||||
|
||||
Die Technik steht nicht im Vordergrund –
|
||||
aber sie sorgt dafür, dass Ihre Website zuverlässig ihren Zweck erfüllt.
|
||||
42
apps/web/docs/TONE.md
Normal file
42
apps/web/docs/TONE.md
Normal file
@@ -0,0 +1,42 @@
|
||||
Ton & Haltung in der Kommunikation
|
||||
|
||||
Dieses Dokument beschreibt die verbindlichen Prinzipien, nach denen ich mit Kunden kommuniziere – schriftlich wie mündlich, auf der Website wie im Projektalltag.
|
||||
|
||||
1. Klarheit vor Höflichkeit
|
||||
|
||||
Ich kommuniziere klar, direkt und verständlich.
|
||||
Unklare Formulierungen, Marketingfloskeln oder beschwichtigende Aussagen werden vermieden.
|
||||
Lieber eine ehrliche, präzise Aussage als eine „freundliche“ Unverbindlichkeit.
|
||||
|
||||
2. Ehrlichkeit ohne Verkaufsdruck
|
||||
|
||||
Ich verspreche nichts, was ich nicht sicher einhalten kann.
|
||||
Grenzen, Risiken und Unsicherheiten werden offen benannt.
|
||||
Es gibt keine künstliche Dringlichkeit, kein Upselling aus Prinzip und keine verdeckten Interessen.
|
||||
|
||||
3. Sachlich, ruhig, professionell
|
||||
|
||||
Die Kommunikation bleibt sachlich und respektvoll – auch bei Kritik, Verzögerungen oder Meinungsverschiedenheiten.
|
||||
Emotionale Eskalation, Schuldzuweisungen oder Rechtfertigungsschleifen werden vermieden.
|
||||
|
||||
4. Verantwortung statt Ausreden
|
||||
|
||||
Probleme werden benannt, nicht relativiert.
|
||||
Wenn etwas nicht funktioniert, wird erklärt warum – und wie damit umgegangen wird.
|
||||
Ich übernehme Verantwortung für meine Arbeit, nicht für äußere Faktoren außerhalb meines Einflusses.
|
||||
|
||||
5. Transparenz statt Fachchinesisch
|
||||
|
||||
Komplexe Sachverhalte werden verständlich erklärt, ohne künstliche Vereinfachung oder Herablassung.
|
||||
Fachbegriffe werden nur verwendet, wenn sie notwendig sind.
|
||||
Wissen dient der Orientierung des Kunden, nicht der Selbstdarstellung.
|
||||
|
||||
6. Gleichbehandlung aller Kunden
|
||||
|
||||
Alle Kunden werden gleich behandelt – unabhängig von Projektgröße, Budget oder Laufzeit.
|
||||
Es gibt keine versteckten Prioritäten, Sonderregeln oder impliziten Erwartungshaltungen.
|
||||
|
||||
7. Langfristige Perspektive
|
||||
|
||||
Die Kommunikation ist auf nachhaltige Zusammenarbeit ausgelegt, nicht auf kurzfristige Zustimmung.
|
||||
Entscheidungen und Empfehlungen orientieren sich am langfristigen Nutzen des Kunden.
|
||||
136
apps/web/docs/WEBSITES.md
Normal file
136
apps/web/docs/WEBSITES.md
Normal file
@@ -0,0 +1,136 @@
|
||||
Wie ich Websites baue – und warum Sie damit Ruhe haben
|
||||
|
||||
Die meisten Websites funktionieren.
|
||||
Bis jemand sie anfasst.
|
||||
Oder Google etwas ändert.
|
||||
Oder ein Plugin ein Update macht.
|
||||
Oder die Agentur nicht mehr antwortet.
|
||||
|
||||
Ich baue Websites so, dass das alles egal ist.
|
||||
|
||||
⸻
|
||||
|
||||
Ich baue Websites wie Systeme – nicht wie Broschüren
|
||||
|
||||
Eine Website ist kein Flyer.
|
||||
Sie ist ein System, das jeden Tag arbeitet.
|
||||
|
||||
Deshalb baue ich sie auch so:
|
||||
• stabil
|
||||
• schnell
|
||||
• vorhersehbar
|
||||
• ohne Überraschungen
|
||||
|
||||
Sie müssen nichts warten.
|
||||
Sie müssen nichts lernen.
|
||||
Sie müssen nichts pflegen, wenn Sie nicht wollen.
|
||||
|
||||
⸻
|
||||
|
||||
Geschwindigkeit ist kein Extra. Sie ist Standard.
|
||||
|
||||
Viele Websites sind langsam, weil sie zusammengeklickt sind.
|
||||
|
||||
Meine sind schnell, weil sie gebaut sind.
|
||||
|
||||
Das bedeutet für Sie:
|
||||
• Seiten laden sofort
|
||||
• Google mag sie
|
||||
• Besucher bleiben
|
||||
• weniger Absprünge
|
||||
• bessere Sichtbarkeit
|
||||
|
||||
90+ Pagespeed ist bei mir kein Ziel.
|
||||
Es ist der Normalzustand.
|
||||
|
||||
⸻
|
||||
|
||||
Keine Plugins. Keine Updates. Keine Wartungshölle.
|
||||
|
||||
Ich nutze keine Baukästen.
|
||||
Keine Plugin-Sammlungen.
|
||||
Keine Systeme, die sich selbst zerstören.
|
||||
|
||||
Ihre Website besteht aus:
|
||||
• sauberem Code
|
||||
• klarer Struktur
|
||||
• festen Bausteinen
|
||||
|
||||
Das heißt:
|
||||
|
||||
Wenn etwas geändert wird, geht nichts kaputt.
|
||||
|
||||
⸻
|
||||
|
||||
Inhalte und Technik sind getrennt (absichtlich)
|
||||
|
||||
Wenn Sie Inhalte selbst pflegen wollen, können Sie das.
|
||||
Aber nur Inhalte.
|
||||
|
||||
Kein Design.
|
||||
Keine Struktur.
|
||||
Keine Technik.
|
||||
|
||||
Sie können nichts kaputt machen.
|
||||
Ich verspreche es.
|
||||
|
||||
Und wenn Sie nichts selbst pflegen wollen:
|
||||
Dann schreiben Sie mir einfach.
|
||||
Ich erledige das.
|
||||
|
||||
⸻
|
||||
|
||||
Änderungen sind einfach. Wirklich.
|
||||
|
||||
Neue Seite?
|
||||
Neue Funktion?
|
||||
Neue Idee?
|
||||
|
||||
Kein Ticket.
|
||||
Kein Formular.
|
||||
Kein Projektplan.
|
||||
|
||||
Sie schreiben mir, was Sie brauchen.
|
||||
Ich setze es um.
|
||||
Fertig.
|
||||
|
||||
⸻
|
||||
|
||||
Warum das alles so gebaut ist
|
||||
|
||||
Weil ich 15 Jahre Agenturen gesehen habe.
|
||||
|
||||
Zu viele Meetings.
|
||||
Zu viele Konzepte.
|
||||
Zu viele Übergaben.
|
||||
Zu viele „eigentlich müsste man mal“.
|
||||
|
||||
Meine Websites sind dafür gebaut,
|
||||
dass Dinge einfach passieren.
|
||||
|
||||
⸻
|
||||
|
||||
Das Ergebnis für Sie
|
||||
• schnelle Website
|
||||
• keine Pflegepflicht
|
||||
• keine Überraschungen
|
||||
• keine Abhängigkeit
|
||||
• keine Agentur
|
||||
• kein Stress
|
||||
|
||||
Oder anders gesagt:
|
||||
|
||||
Eine Website, die sich wie eine erledigte Aufgabe anfühlt.
|
||||
|
||||
⸻
|
||||
|
||||
Und technisch?
|
||||
|
||||
Technisch ist das alles sehr modern.
|
||||
Aber das ist mein Problem, nicht Ihres.
|
||||
|
||||
⸻
|
||||
|
||||
Wenn Sie wollen, erkläre ich Ihnen das gerne.
|
||||
|
||||
Wenn nicht, funktioniert es trotzdem.
|
||||
56
apps/web/docs/WORDING.md
Normal file
56
apps/web/docs/WORDING.md
Normal file
@@ -0,0 +1,56 @@
|
||||
1. Aktiv statt passiv
|
||||
|
||||
Sätze werden aktiv formuliert.
|
||||
Keine unpersönlichen Konstruktionen, kein „es wird“, „man sollte“, „könnte“.
|
||||
|
||||
2. Kurz und eindeutig
|
||||
|
||||
Sätze sind so kurz wie möglich, so lang wie nötig.
|
||||
Ein Gedanke pro Satz. Keine Schachtelsätze.
|
||||
|
||||
3. Keine Weichmacher
|
||||
|
||||
Keine Wörter wie:
|
||||
• eventuell
|
||||
• möglicherweise
|
||||
• grundsätzlich
|
||||
• in der Regel
|
||||
• normalerweise
|
||||
|
||||
Wenn etwas gilt, wird es gesagt. Wenn nicht, wird es ausgeschlossen.
|
||||
|
||||
4. Keine Marketingbegriffe
|
||||
|
||||
Keine Buzzwords, Superlative oder leeren Versprechen.
|
||||
Keine emotional aufgeladenen Begriffe. Keine Werbesprache.
|
||||
|
||||
5. Konkrete Aussagen
|
||||
|
||||
Keine abstrakten Formulierungen.
|
||||
Aussagen beziehen sich auf konkrete Ergebnisse, Zustände oder Abläufe.
|
||||
|
||||
6. Ich-Form
|
||||
|
||||
Kommunikation erfolgt konsequent in der Ich-Form.
|
||||
Kein „wir“, kein „unser Team“, keine künstliche Vergrößerung.
|
||||
|
||||
7. Keine Rechtfertigungen
|
||||
|
||||
Keine erklärenden Absicherungen im Satz.
|
||||
Aussagen stehen für sich und werden nicht relativiert.
|
||||
|
||||
8. Neutraler Ton
|
||||
|
||||
Keine Umgangssprache.
|
||||
Keine Ironie.
|
||||
Keine Emojis.
|
||||
|
||||
9. Verbindliche Sprache
|
||||
|
||||
Keine offenen Enden ohne Grund.
|
||||
Wenn etwas nicht garantiert wird, wird das klar benannt – ohne Abschwächung.
|
||||
|
||||
10. Technisch präzise, sprachlich einfach
|
||||
|
||||
Technische Inhalte werden präzise beschrieben, sprachlich jedoch simpel gehalten.
|
||||
Kein unnötiger Jargon.
|
||||
10
apps/web/eslint.config.js
Normal file
10
apps/web/eslint.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import mintelConfig from "@mintel/eslint-config/next";
|
||||
|
||||
export default [
|
||||
...mintelConfig,
|
||||
{
|
||||
rules: {
|
||||
"no-console": "warn",
|
||||
},
|
||||
},
|
||||
];
|
||||
7
apps/web/next.config.mjs
Normal file
7
apps/web/next.config.mjs
Normal file
@@ -0,0 +1,7 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
output: 'standalone',
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
73
apps/web/package.json
Normal file
73
apps/web/package.json
Normal file
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"name": "@mintel/web",
|
||||
"type": "module",
|
||||
"version": "0.1.0",
|
||||
"description": "Technical problem solver's blog - practical insights and learning notes",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"test": "npm run test:smoke",
|
||||
"test:smoke": "tsx ./scripts/smoke-test.ts",
|
||||
"test:links": "tsx ./scripts/test-links.ts",
|
||||
"test:file-examples": "tsx ./scripts/test-file-examples-comprehensive.ts",
|
||||
"clone-website": "tsx ./scripts/clone-recursive.ts",
|
||||
"clone-page": "tsx ./scripts/clone-page.ts",
|
||||
"generate-estimate": "tsx ./scripts/generate-estimate.ts",
|
||||
"ai-estimate": "tsx ./scripts/ai-estimate.ts",
|
||||
"video:preview": "remotion preview video/index.ts",
|
||||
"video:render": "remotion render video/index.ts ButtonShowcase out/button-showcase.mp4",
|
||||
"video:render:contact": "remotion render video/index.ts ContactFormShowcase out/contact-showcase.mp4 --concurrency=1 --codec=h264 --crf=16 --pixel-format=yuv420p --overwrite",
|
||||
"video:render:button": "remotion render video/index.ts ButtonShowcase out/button-showcase.mp4 --concurrency=1 --codec=h264 --crf=16 --pixel-format=yuv420p --overwrite",
|
||||
"video:render:all": "npm run video:render:contact && npm run video:render:button"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mintel/next-utils": "^1.0.1",
|
||||
"@react-pdf/renderer": "^4.3.2",
|
||||
"@remotion/bundler": "^4.0.414",
|
||||
"@remotion/cli": "^4.0.414",
|
||||
"@remotion/lottie": "^4.0.414",
|
||||
"@remotion/renderer": "^4.0.414",
|
||||
"@remotion/tailwind": "^4.0.414",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@types/ioredis": "^4.28.10",
|
||||
"@types/react": "^19.2.8",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vercel/og": "^0.8.6",
|
||||
"axios": "^1.13.4",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"clsx": "^2.1.1",
|
||||
"crawlee": "^3.15.3",
|
||||
"framer-motion": "^12.29.2",
|
||||
"ioredis": "^5.9.1",
|
||||
"lucide-react": "^0.468.0",
|
||||
"mermaid": "^11.12.2",
|
||||
"next": "^16.1.6",
|
||||
"playwright": "^1.58.1",
|
||||
"prismjs": "^1.30.0",
|
||||
"puppeteer": "^24.36.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"remotion": "^4.0.414",
|
||||
"shiki": "^1.24.2",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"website-scraper": "^6.0.0",
|
||||
"website-scraper-puppeteer": "^2.0.0",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mintel/eslint-config": "^1.0.1",
|
||||
"@mintel/tsconfig": "^1.0.1",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/node": "^25.0.6",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.49",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
}
|
||||
162
apps/web/plans/embed-architecture.md
Normal file
162
apps/web/plans/embed-architecture.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# Build-Time Embed Architecture for Astro Blog
|
||||
|
||||
## Overview
|
||||
Complete solution for embedding tweets, YouTube videos, and other rich content with **full styling control** and **build-time generation**.
|
||||
|
||||
## Architecture
|
||||
|
||||
### 1. Build-Time Embed Components
|
||||
- **YouTubeEmbed.astro** - Generates iframe at build time
|
||||
- **TwitterEmbed.astro** - Fetches tweet data at build time
|
||||
- **GenericEmbed.astro** - Handles any oEmbed provider
|
||||
- **EmbedContainer.astro** - Wrapper for consistent styling
|
||||
|
||||
### 2. Data Flow
|
||||
```
|
||||
Build Time:
|
||||
1. Component receives props (videoId, tweetId, url)
|
||||
2. Astro fetches embed data/API endpoints
|
||||
3. Generates HTML with your custom styles
|
||||
4. Injects into static page
|
||||
|
||||
Runtime:
|
||||
- No external API calls
|
||||
- Fast loading
|
||||
- Full styling control
|
||||
```
|
||||
|
||||
### 3. Key Features
|
||||
- ✅ **Build-time generation** - No client-side JS needed
|
||||
- ✅ **Full styling control** - CSS variables + custom classes
|
||||
- ✅ **Lazy loading** - Intersection Observer for performance
|
||||
- ✅ **No paid services** - Uses official APIs only
|
||||
- ✅ **TypeScript support** - Full type safety
|
||||
- ✅ **Responsive** - Mobile-first design
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### YouTube (Simplest)
|
||||
```astro
|
||||
---
|
||||
// Build time: Just generate iframe
|
||||
const { videoId, className } = Astro.props;
|
||||
const embedUrl = `https://www.youtube.com/embed/${videoId}`;
|
||||
---
|
||||
|
||||
<div class={`youtube-embed ${className}`}>
|
||||
<iframe src={embedUrl} loading="lazy" />
|
||||
</div>
|
||||
```
|
||||
|
||||
### Twitter (Requires API)
|
||||
```astro
|
||||
---
|
||||
// Build time: Fetch tweet data via Twitter API
|
||||
const { tweetId } = Astro.props;
|
||||
const tweetData = await fetchTweetData(tweetId); // Uses oEmbed or API
|
||||
---
|
||||
|
||||
<div class="twitter-embed">
|
||||
{tweetData.html}
|
||||
</div>
|
||||
```
|
||||
|
||||
### Generic (oEmbed)
|
||||
```astro
|
||||
---
|
||||
// Build time: Fetch oEmbed data
|
||||
const { url } = Astro.props;
|
||||
const oEmbedData = await fetchOEmbed(url);
|
||||
---
|
||||
|
||||
<div class="generic-embed" set:html={oEmbedData.html} />
|
||||
```
|
||||
|
||||
## File Structure
|
||||
```
|
||||
src/
|
||||
├── components/
|
||||
│ └── Embeds/
|
||||
│ ├── YouTubeEmbed.astro
|
||||
│ ├── TwitterEmbed.astro
|
||||
│ ├── GenericEmbed.astro
|
||||
│ ├── EmbedContainer.astro
|
||||
│ └── index.ts
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### In MDX/Markdown
|
||||
```mdx
|
||||
import { YouTubeEmbed, TwitterEmbed } from '../components/Embeds';
|
||||
|
||||
# My Blog Post
|
||||
|
||||
<YouTubeEmbed
|
||||
videoId="dQw4w9WgXcQ"
|
||||
className="my-custom-style"
|
||||
aspectRatio="56.25%"
|
||||
/>
|
||||
|
||||
<TwitterEmbed
|
||||
tweetId="1234567890123456789"
|
||||
theme="dark"
|
||||
/>
|
||||
```
|
||||
|
||||
### In Astro Templates
|
||||
```astro
|
||||
---
|
||||
import YouTubeEmbed from '../components/Embeds/YouTubeEmbed.astro';
|
||||
---
|
||||
|
||||
<YouTubeEmbed
|
||||
videoId={post.videoId}
|
||||
style="minimal"
|
||||
className="mt-8 mb-12"
|
||||
/>
|
||||
```
|
||||
|
||||
## Styling Control
|
||||
|
||||
### CSS Variables
|
||||
```css
|
||||
.youtube-embed {
|
||||
--aspect-ratio: 56.25%;
|
||||
--bg-color: #000;
|
||||
--border-radius: 12px;
|
||||
--shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
--transition: all 0.3s ease;
|
||||
}
|
||||
```
|
||||
|
||||
### Data Attributes
|
||||
```html
|
||||
<div data-style="minimal" data-aspect="square">
|
||||
<!-- Custom styling via CSS -->
|
||||
</div>
|
||||
```
|
||||
|
||||
### Custom Classes
|
||||
```astro
|
||||
<YouTubeEmbed className="custom-embed my-blog-style" />
|
||||
```
|
||||
|
||||
## Performance Benefits
|
||||
|
||||
1. **Zero Client JS** - Everything done at build time
|
||||
2. **Fast Loading** - Pre-rendered HTML
|
||||
3. **SEO Friendly** - Static content
|
||||
4. **No External Dependencies** - Only official APIs
|
||||
5. **CDN Compatible** - Works with any CDN
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Create embed components directory
|
||||
2. Implement YouTubeEmbed (simplest first)
|
||||
3. Implement TwitterEmbed (requires API setup)
|
||||
4. Create GenericEmbed for other platforms
|
||||
5. Add styling examples
|
||||
6. Document usage patterns
|
||||
|
||||
This gives you complete control while keeping everything free and fast!
|
||||
6
apps/web/postcss.config.js
Normal file
6
apps/web/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
1
apps/web/public/favicon.svg
Normal file
1
apps/web/public/favicon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 745 744" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect id="Favicon" x="0.855" y="0.277" width="743.633" height="743.633" style="fill:none;"/><path d="M739.657,49.961c-0,-24.755 -20.098,-44.852 -44.853,-44.852l-644.264,-0c-24.755,-0 -44.853,20.097 -44.853,44.852l-0,644.265c-0,24.755 20.098,44.853 44.853,44.853l644.264,-0c24.755,-0 44.853,-20.098 44.853,-44.853l-0,-644.265Z"/><path d="M135.778,286.282c0.157,0.236 0.52,0.761 0.852,1.092c1.2,1.192 2.956,2.358 5.296,3.587l0.005,0.003l2,1.327l1.693,1.752l1.239,2.072l0.713,2.215l0.211,2.192l-0.208,2.068l-1.385,3.646l-2.46,2.971l-1.634,1.187l-1.911,0.914l-2.161,0.551l-2.323,0.093l-2.343,-0.412l-2.211,-0.892c-11.589,-6.087 -16.814,-12.713 -18.39,-18.621c-1.835,-6.879 0.575,-13.751 6.509,-19.965c12.824,-13.43 44.389,-24.257 57.136,-27.454l0.001,-0c30.563,-7.658 62.165,-12.34 93.648,-13.693c25.54,-1.098 54.423,-1.803 78.831,7.271c15.51,5.768 26.031,14.726 32.759,25.559c56.484,-46.707 118.893,-93.116 187.414,-119.225c10.958,-4.178 27.246,-8.604 40.339,-7.415c7.908,0.718 14.744,3.442 19.461,8.573c4.662,5.072 7.541,12.672 6.521,23.945c-3.634,40.091 -31.543,80.109 -52.575,112.911c-25.848,40.321 -53.663,79.557 -82.723,117.821c29.191,-23.305 58.382,-46.61 87.573,-69.913l1.731,-1.145l1.85,-0.796l3.75,-0.585l3.526,0.562l3.17,1.552l2.672,2.595l1.028,1.728l0.705,1.995l0.285,2.178l-0.185,2.22l-0.631,2.115l-0.992,1.91c-10.505,16.533 -21.014,33.063 -31.523,49.592l-0.001,0.003c-1.852,2.909 -11.995,19.195 -18.14,30.842c-0.096,0.182 -0.192,0.366 -0.288,0.553c13.673,-3.721 27.615,-13.517 38.776,-19.936c10.441,-6.004 20.778,-12.208 30.865,-18.787l0.003,-0.002l2.134,-1.1l2.356,-0.626l2.421,-0.072l2.289,0.463l2.022,0.893l1.703,1.201l2.495,3.003l1.373,3.598l0.232,1.999l-0.139,2.106l-0.573,2.146l-1.048,2.067l-1.498,1.843l-1.854,1.496l-0.007,0.005c-15.715,10.242 -31.914,19.975 -48.604,28.526c-6.986,3.579 -18.808,10.744 -29.918,13.789c-9.41,2.579 -18.37,2.143 -24.958,-2.988c-5.648,-4.398 -7.104,-11.077 -5.042,-18.895c3.104,-11.773 15.551,-27.001 19.276,-32.858l2.604,-4.095c-37.274,29.759 -74.551,59.518 -111.826,89.274l-1.82,1.214l-2.004,0.868l-2.111,0.471l-2.12,0.061l-2.039,-0.329l-1.892,-0.676l-3.201,-2.224l-2.259,-3.202l-0.689,-1.912l-0.33,-2.068l0.077,-2.148l0.5,-2.129l0.902,-2.005l1.242,-1.804c59.233,-71.996 118.215,-147.452 163.946,-228.856l0.002,-0.003c3.532,-6.277 19.498,-32.912 25.637,-54.458c1.456,-5.11 2.365,-9.885 2.213,-13.918c-0.128,-3.403 -1.052,-6.169 -4.397,-6.847c-6.883,-1.395 -14.409,-0.182 -21.911,2.075c-12.591,3.787 -25.072,10.598 -34.871,15.502l-0.002,0.001c-37.202,18.606 -72.519,40.846 -106.083,65.396c-19.253,14.077 -38.067,28.773 -56.448,44.009c5.948,31.922 -8.725,71.663 -25.261,97.617c-26.624,41.789 -61.114,78.396 -97.084,112.241c-35.155,33.081 -71.676,66.504 -111.219,94.355l-0.002,0.001c-4.905,3.453 -13.056,9.944 -21.245,13.763c-7.008,3.268 -14.124,4.488 -20.021,2.432c-7.666,-2.674 -10.711,-8.481 -11.105,-15.23c-0.288,-4.928 1.117,-10.472 2.745,-14.802l0.001,-0.003c17.424,-46.26 54.722,-88.018 86.091,-125.21c52.005,-61.657 108.894,-119.681 170.402,-171.929c-5.142,-9.861 -13.608,-17.675 -25.833,-20.957c-27.596,-7.404 -57.826,-6.098 -86.019,-3.428c-30.452,2.883 -61.745,7.625 -90.667,17.984c-6.667,2.388 -17.118,6.215 -22.892,11.134c-0.89,0.758 -1.884,2.115 -2.149,2.485Zm-6.046,295.699c3.853,-1.713 7.757,-4.116 11.623,-6.805c12.067,-8.393 23.538,-19.805 31.614,-26.433c45.641,-37.472 90.707,-76.66 129.793,-121.083c26.644,-30.283 57.988,-66.814 65.641,-107.833c1.02,-5.466 1.414,-11.09 1.137,-16.634c-41.419,35.852 -80.575,74.39 -117.54,114.67c-41.853,45.61 -85.416,93.945 -115.619,148.393c-0.671,1.213 -4.049,9.375 -6.649,15.725Z" style="fill:#fff;"/></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
BIN
apps/web/public/header.webp
Executable file
BIN
apps/web/public/header.webp
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 MiB |
4
apps/web/public/robots.txt
Normal file
4
apps/web/public/robots.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://mintel.me/sitemap.xml
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
@@ -0,0 +1 @@
|
||||
!function(){"use strict";function e(e){try{if("undefined"==typeof console)return;"error"in console?console.error(e):console.log(e)}catch(e){}}function t(e){return d.innerHTML='<a href="'+e.replace(/"/g,""")+'"></a>',d.childNodes[0].getAttribute("href")||""}function r(e,t){var r=e.substr(t,2);return parseInt(r,16)}function n(n,c){for(var o="",a=r(n,c),i=c+2;i<n.length;i+=2){var l=r(n,i)^a;o+=String.fromCharCode(l)}try{o=decodeURIComponent(escape(o))}catch(u){e(u)}return t(o)}function c(t){for(var r=t.querySelectorAll("a"),c=0;c<r.length;c++)try{var o=r[c],a=o.href.indexOf(l);a>-1&&(o.href="mailto:"+n(o.href,a+l.length))}catch(i){e(i)}}function o(t){for(var r=t.querySelectorAll(u),c=0;c<r.length;c++)try{var o=r[c],a=o.parentNode,i=o.getAttribute(f);if(i){var l=n(i,0),d=document.createTextNode(l);a.replaceChild(d,o)}}catch(h){e(h)}}function a(t){for(var r=t.querySelectorAll("template"),n=0;n<r.length;n++)try{i(r[n].content)}catch(c){e(c)}}function i(t){try{c(t),o(t),a(t)}catch(r){e(r)}}var l="/cdn-cgi/l/email-protection#",u=".__cf_email__",f="data-cfemail",d=document.createElement("div");i(document),function(){var e=document.currentScript||document.scripts[document.scripts.length-1];e.parentNode.removeChild(e)}()}();
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
document.addEventListener('DOMContentLoaded',function(){if(phpVars.externalPermalinksEnabled){var links=document.getElementsByTagName('a');for(var i=0;i<links.length;i++){var url=links[i].getAttribute('href');var target=links[i].getAttribute('target');if(url!=null){if(url.indexOf('#new_tab')>=0){url=url.replace('#new_tab','');target='_blank';links[i].setAttribute('href',url);links[i].setAttribute('target',target);links[i].setAttribute('rel','noopener noreferrer nofollow')}}}}})
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,11 @@
|
||||
|
||||
/*!
|
||||
* hoverIntent v1.9.0 // 2017.09.01 // jQuery v1.7.0+
|
||||
* http://briancherne.github.io/jquery-hoverIntent/
|
||||
*
|
||||
* You may use hoverIntent under the terms of the MIT license. Basically that
|
||||
* means you are free to use hoverIntent as long as this header is left intact.
|
||||
* Copyright 2007-2017 Brian Cherne
|
||||
*/
|
||||
!function(factory){"use strict";"function"==typeof define&&define.amd?define(["jquery"],factory):jQuery&&!jQuery.fn.hoverIntent&&factory(jQuery)}(function($){"use strict";var cX,cY,_cfg={interval:100,sensitivity:6,timeout:0},INSTANCE_COUNT=0,track=function(ev){cX=ev.pageX,cY=ev.pageY},compare=function(ev,$el,s,cfg){if(Math.sqrt((s.pX-cX)*(s.pX-cX)+(s.pY-cY)*(s.pY-cY))<cfg.sensitivity)return $el.off(s.event,track),delete s.timeoutId,s.isActive=!0,ev.pageX=cX,ev.pageY=cY,delete s.pX,delete s.pY,cfg.over.apply($el[0],[ev]);s.pX=cX,s.pY=cY,s.timeoutId=setTimeout(function(){compare(ev,$el,s,cfg)},cfg.interval)},delay=function(ev,$el,s,out){return delete $el.data("hoverIntent")[s.id],out.apply($el[0],[ev])};$.fn.hoverIntent=function(handlerIn,handlerOut,selector){var instanceId=INSTANCE_COUNT++,cfg=$.extend({},_cfg);$.isPlainObject(handlerIn)?(cfg=$.extend(cfg,handlerIn),$.isFunction(cfg.out)||(cfg.out=cfg.over)):cfg=$.isFunction(handlerOut)?$.extend(cfg,{over:handlerIn,out:handlerOut,selector:selector}):$.extend(cfg,{over:handlerIn,out:handlerIn,selector:handlerOut});var handleHover=function(e){var ev=$.extend({},e),$el=$(this),hoverIntentData=$el.data("hoverIntent");hoverIntentData||$el.data("hoverIntent",hoverIntentData={});var state=hoverIntentData[instanceId];state||(hoverIntentData[instanceId]=state={id:instanceId}),state.timeoutId&&(state.timeoutId=clearTimeout(state.timeoutId));var mousemove=state.event="mousemove.hoverIntent.hoverIntent"+instanceId;if("mouseenter"===e.type){if(state.isActive)return;state.pX=ev.pageX,state.pY=ev.pageY,$el.off(mousemove,track).on(mousemove,track),state.timeoutId=setTimeout(function(){compare(ev,$el,state,cfg)},cfg.interval)}else{if(!state.isActive)return;$el.off(mousemove,track),state.timeoutId=setTimeout(function(){delay(ev,$el,state,cfg.out)},cfg.timeout)}};return this.on({"mouseenter.hoverIntent":handleHover,"mouseleave.hoverIntent":handleHover},cfg.selector)}});
|
||||
;
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
|
||||
|
||||
|
||||
!function(n){"function"==typeof define&&define.amd?define(["jquery"],function(e){return n(e)}):"object"==typeof module&&"object"==typeof module.exports?module.exports=n(require("jquery")):n(jQuery)}(function(n){function e(n){var e=7.5625,t=2.75;return n<1/t?e*n*n:n<2/t?e*(n-=1.5/t)*n+.75:n<2.5/t?e*(n-=2.25/t)*n+.9375:e*(n-=2.625/t)*n+.984375}void 0!==n.easing&&(n.easing.jswing=n.easing.swing);var t=Math.pow,u=Math.sqrt,r=Math.sin,i=Math.cos,a=Math.PI,o=1.70158,c=1.525*o,s=2*a/3,f=2*a/4.5;return n.extend(n.easing,{def:"easeOutQuad",swing:function(e){return n.easing[n.easing.def](e)},easeInQuad:function(n){return n*n},easeOutQuad:function(n){return 1-(1-n)*(1-n)},easeInOutQuad:function(n){return n<.5?2*n*n:1-t(-2*n+2,2)/2},easeInCubic:function(n){return n*n*n},easeOutCubic:function(n){return 1-t(1-n,3)},easeInOutCubic:function(n){return n<.5?4*n*n*n:1-t(-2*n+2,3)/2},easeInQuart:function(n){return n*n*n*n},easeOutQuart:function(n){return 1-t(1-n,4)},easeInOutQuart:function(n){return n<.5?8*n*n*n*n:1-t(-2*n+2,4)/2},easeInQuint:function(n){return n*n*n*n*n},easeOutQuint:function(n){return 1-t(1-n,5)},easeInOutQuint:function(n){return n<.5?16*n*n*n*n*n:1-t(-2*n+2,5)/2},easeInSine:function(n){return 1-i(n*a/2)},easeOutSine:function(n){return r(n*a/2)},easeInOutSine:function(n){return-(i(a*n)-1)/2},easeInExpo:function(n){return 0===n?0:t(2,10*n-10)},easeOutExpo:function(n){return 1===n?1:1-t(2,-10*n)},easeInOutExpo:function(n){return 0===n?0:1===n?1:n<.5?t(2,20*n-10)/2:(2-t(2,-20*n+10))/2},easeInCirc:function(n){return 1-u(1-t(n,2))},easeOutCirc:function(n){return u(1-t(n-1,2))},easeInOutCirc:function(n){return n<.5?(1-u(1-t(2*n,2)))/2:(u(1-t(-2*n+2,2))+1)/2},easeInElastic:function(n){return 0===n?0:1===n?1:-t(2,10*n-10)*r((10*n-10.75)*s)},easeOutElastic:function(n){return 0===n?0:1===n?1:t(2,-10*n)*r((10*n-.75)*s)+1},easeInOutElastic:function(n){return 0===n?0:1===n?1:n<.5?-t(2,20*n-10)*r((20*n-11.125)*f)/2:t(2,-20*n+10)*r((20*n-11.125)*f)/2+1},easeInBack:function(n){return 2.70158*n*n*n-o*n*n},easeOutBack:function(n){return 1+2.70158*t(n-1,3)+o*t(n-1,2)},easeInOutBack:function(n){return n<.5?t(2*n,2)*(7.189819*n-c)/2:(t(2*n-2,2)*((c+1)*(2*n-2)+c)+2)/2},easeInBounce:function(n){return 1-e(1-n)},easeOutBounce:e,easeInOutBounce:function(n){return n<.5?(1-e(1-2*n))/2:(1+e(2*n-1))/2}}),n});
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
|
||||
|
||||
/*!
|
||||
* jQuery Mousewheel 3.1.13
|
||||
* Copyright OpenJS Foundation and other contributors
|
||||
*/
|
||||
!function(e){"function"==typeof define&&define.amd?define(["jquery"],e):"object"==typeof exports?module.exports=e:e(jQuery)}(function(u){var f,d,e=["wheel","mousewheel","DOMMouseScroll","MozMousePixelScroll"],t="onwheel"in window.document||9<=window.document.documentMode?["wheel"]:["mousewheel","DomMouseScroll","MozMousePixelScroll"],w=Array.prototype.slice;if(u.event.fixHooks)for(var i=e.length;i;)u.event.fixHooks[e[--i]]=u.event.mouseHooks;var c=u.event.special.mousewheel={version:"3.1.12",setup:function(){if(this.addEventListener)for(var e=t.length;e;)this.addEventListener(t[--e],n,{passive:false});else this.onmousewheel=n;u.data(this,"mousewheel-line-height",c.getLineHeight(this)),u.data(this,"mousewheel-page-height",c.getPageHeight(this))},teardown:function(){if(this.removeEventListener)for(var e=t.length;e;)this.removeEventListener(t[--e],n,!1);else this.onmousewheel=null;u.removeData(this,"mousewheel-line-height"),u.removeData(this,"mousewheel-page-height")},getLineHeight:function(e){var t=u(e),i=t["offsetParent"in u.fn?"offsetParent":"parent"]();return i.length||(i=u("body")),parseInt(i.css("fontSize"),10)||parseInt(t.css("fontSize"),10)||16},getPageHeight:function(e){return u(e).height()},settings:{adjustOldDeltas:!0,normalizeOffset:!0}};function n(e){var t,i=e||window.event,n=w.call(arguments,1),o=0,l=0,s=0;if((e=u.event.fix(i)).type="mousewheel","detail"in i&&(s=-1*i.detail),"wheelDelta"in i&&(s=i.wheelDelta),"wheelDeltaY"in i&&(s=i.wheelDeltaY),"wheelDeltaX"in i&&(l=-1*i.wheelDeltaX),"axis"in i&&i.axis===i.HORIZONTAL_AXIS&&(l=-1*s,s=0),o=0===s?l:s,"deltaY"in i&&(o=s=-1*i.deltaY),"deltaX"in i&&(l=i.deltaX,0===s&&(o=-1*l)),0!==s||0!==l){if(1===i.deltaMode){var a=u.data(this,"mousewheel-line-height");o*=a,s*=a,l*=a}else if(2===i.deltaMode){var h=u.data(this,"mousewheel-page-height");o*=h,s*=h,l*=h}if(t=Math.max(Math.abs(s),Math.abs(l)),(!d||t<d)&&g(i,d=t)&&(d/=40),g(i,t)&&(o/=40,l/=40,s/=40),o=Math[1<=o?"floor":"ceil"](o/d),l=Math[1<=l?"floor":"ceil"](l/d),s=Math[1<=s?"floor":"ceil"](s/d),c.settings.normalizeOffset&&this.getBoundingClientRect){var r=this.getBoundingClientRect();e.offsetX=e.clientX-r.left,e.offsetY=e.clientY-r.top}return e.deltaX=l,e.deltaY=s,e.deltaFactor=d,e.deltaMode=0,n.unshift(e,o,l,s),f&&window.clearTimeout(f),f=window.setTimeout(m,200),(u.event.dispatch||u.event.handle).apply(this,n)}}function m(){d=null}function g(e,t){return c.settings.adjustOldDeltas&&"mousewheel"===e.type&&t%120==0}u.fn.extend({mousewheel:function(e){return e?this.on("mousewheel",e):this.trigger("mousewheel")},unmousewheel:function(e){return this.off("mousewheel",e)}})});
|
||||
;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
|
||||
(function($){'use strict';function validURL(str){var pattern=new RegExp('^(https?:\\/\\/)?'+'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+'((\\d{1,3}\\.){3}\\d{1,3}))'+'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+'(\\?[;&a-z\\d%_.~+=-]*)?'+'(\\#[-a-z\\d_]*)?$','i');return!!pattern.test(str)}
|
||||
$(document).ready(function(){var elem='.kc-us-copy-to-clipboard';if($(elem).get(0)){let clipboardLink=new ClipboardJS(elem);clipboardLink.on('success',function(e){let elem=e.trigger;$(elem).find('.kc-us-link').select();let id=elem.getAttribute('id');let copiedTextID='#copied-text-'+id;$(copiedTextID).text('Copied').fadeIn();$(copiedTextID).fadeOut('slow')})}
|
||||
$(".kc_us_create_short_link").click(function(e){e.preventDefault();var post_id=$(this).attr('data-post_id');var security=$(this).attr('data-us-security');$(this).find('.kc_us_loading').show();$.ajax({type:"post",dataType:"json",context:this,url:ajaxurl,data:{action:'us_handle_request',cmd:"create_short_link",post_id:post_id,security:security},success:function(response){if(response.status==="success"){$(this).parent('.us_short_link').html(response.html)}else{$(this).find('.kc_us_loading').hide()}},error:function(err){$(this).find('.kc_us_loading').hide()}})});$("#kc-us-dashboard-short-link").click(function(e){e.preventDefault();var targetURL=$('#kc-us-target-url').val();var slug=$('#kc-us-slug').val();var security=$('#kc-us-security').val();var domain=$('#kc-us-domain').val();if(!validURL(targetURL)){alert('Please Enter Valid Target URL');return}
|
||||
$(this).find('.kc_us_loading').show();$('#kc-us-error-message').hide();$('#kc-us-success-message').hide();$.ajax({type:"post",dataType:"json",context:this,url:ajaxurl,data:{action:'us_handle_request',cmd:"create_short_link",slug:slug,url:targetURL,security:security,domain:domain},success:function(response){if(response.status==="success"){var link=response.link;var html='Short Link : <span class="kc-flex kc-us-copy-to-clipboard" data-clipboard-text="'+link+'" id="link-25"><input type="text" readonly="true" style="width: 65%;" onclick="this.select();" value="'+link+'" class="kc-us-link"></span>';$('#kc-us-success-message').html(html);$('#kc-us-success-message').show()}else{var html='Something went wrong while creating short link';if(response.message){html=response.message}
|
||||
$('#kc-us-error-message').html(html);$('#kc-us-error-message').show()}
|
||||
$('.kc_us_loading').hide()},error:function(err){var html='Something went wrong while creating short link';$('#kc-us-error-message').html(html);$('#kc-us-error-message').show();$('.kc_us_loading').hide()}})});$("#kc-us-submit-btn").click(function(e){e.preventDefault();var targetURL=$('#kc-us-target-url').val();var security=$('#kc-us-security').val();if(!validURL(targetURL)){alert('Please Enter Valid Long URL');return}
|
||||
$(this).parents('.generate-short-link-form').find('.kc_us_loading').show();$.ajax({type:"post",dataType:"json",context:this,url:usParams.ajaxurl,data:{action:'us_handle_request',cmd:"create_short_link",url:targetURL,security:security},success:function(response){$(this).parents('.generate-short-link-form').find('.kc_us_loading').hide();if(response.status==="success"){var link=response.link;$('.generated-short-link-form #kc-us-short-url').val(link);$('.generate-short-link-form').hide();$('.generated-short-link-form').show()}else{var html='Something went wrong while creating short link';$('#kc-us-error-msg').text(html);$('#kc-us-error-msg').show()}},error:function(err){var html='Something went wrong while creating short link';$('#kc-us-error-msg').text(html);$('#kc-us-error-msg').show()}})})})})(jQuery)
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
var headerEl=document.getElementById("header-outer"),headerSpaceEl=document.getElementById("header-space");void 0!==headerEl&&null!=headerEl&&void 0!==headerSpaceEl&&null!=headerSpaceEl&&headerSpaceEl.hasAttribute("data-secondary-header-display")&&(headerSpaceEl.style.height=headerEl.clientHeight+"px"),jQuery(function(e){"use strict";var t,a,r,n,o,d,i=!1;function h(){var a=t.offset().top;e("#page-header-wrap.fullscreen-header").css("height","auto"),t.css("height",parseInt(window.innerHeight)-parseInt(a)+"px")}if(navigator.userAgent.match(/(Android|iPod|iPhone|iPad|BlackBerry|IEMobile|Opera Mini)/)&&(i=!0),i&&e("#page-header-bg.fullscreen-header").length>0){t=e("#page-header-bg"),h();var l=window.innerWidth,s=window.innerHeight;e(window).resize(function(){e(window).width()!=l&&e(window).height!=s&&(h(),l=window.innerWidth,s=window.innerHeight)})}i&&e(".nectar_fullscreen_zoom_recent_projects").length>0&&(a=e(".body-border-top").length>0&&e(window).width()>1e3?e(".body-border-top").height():0,e(".nectar_fullscreen_zoom_recent_projects").each(function(){e(this).parents(".first-section").length>0?e(this).css("height",e(window).height()-e(this).offset().top-a):e(this).css("height",e(window).height())})),e('#header-outer[data-format="centered-menu-bottom-bar"]').length>0&&(r=e('#header-outer[data-format="centered-menu-bottom-bar"] header#top .span_9'),n=e('#header-outer[data-format="centered-menu-bottom-bar"] header#top .span_3'),o=e("#header-secondary-outer"),(d=n.find("#logo").clone()).is('[data-supplied-ml="true"]')&&d.find("img:not(.mobile-only-logo)").remove(),d.find("img.starting-logo").remove(),o.length>0&&o.addClass("centered-menu-bottom-bar"),d.removeAttr("id").addClass("logo-clone"),e('#header-outer[data-condense="true"]').length>0&&0==e('#header-outer[data-menu-bottom-bar-align="left"]').length&&r.prepend(d)),e('#page-header-bg[data-animate-in-effect="zoom-out"]').addClass("loaded")})
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
!function(t){"use strict";const e=t.params,n=(document.querySelector.bind(document),(t,e)=>e.split(".").reduce((t,e)=>t&&t[e],t)),s=()=>null,i=t=>null===t||t===undefined?"":t,o="wc/store/checkout";function a(t){window.wp&&window.wp.data&&window.wp.data.dispatch&&window.wc&&window.wc.wcBlocksData&&window.wp.data.dispatch(window.wc.wcBlocksData.CHECKOUT_STORE_KEY).setExtensionData("woocommerce/order-attribution",t,!0)}function r(){return"undefined"!=typeof sbjs}function c(){if(window.wp&&window.wp.data&&"function"==typeof window.wp.data.subscribe){const e=window.wp.data.subscribe(function(){e(),a(t.getAttributionData())},o)}}t.getAttributionData=function(){const i=e.allowTracking&&r()?n:s,o=r()?sbjs.get:{},a=Object.entries(t.fields).map(([t,e])=>[t,i(o,e)]);return Object.fromEntries(a)},t.setOrderTracking=function(n){if(e.allowTracking=n,n){if(!r())return;sbjs.init({lifetime:Number(e.lifetime),session_length:Number(e.session),base64:Boolean(e.base64),timezone_offset:"0"})}else!function(){const t=window.location.hostname;["sbjs_current","sbjs_current_add","sbjs_first","sbjs_first_add","sbjs_session","sbjs_udata","sbjs_migrations","sbjs_promo"].forEach(e=>{document.cookie=`${e}=; path=/; max-age=-999; domain=.${t};`})}();const s=t.getAttributionData();!function(t){for(const e of document.querySelectorAll("wc-order-attribution-inputs"))e.values=t}(s),a(s)},t.setOrderTracking(e.allowTracking),"loading"===document.readyState?document.addEventListener("DOMContentLoaded",c):c(),window.customElements.define("wc-order-attribution-inputs",class extends HTMLElement{constructor(){if(super(),this._fieldNames=Object.keys(t.fields),this.hasOwnProperty("_values")){let t=this.values;delete this.values,this.values=t||{}}}connectedCallback(){this.innerHTML="";const t=new DocumentFragment;for(const n of this._fieldNames){const s=document.createElement("input");s.type="hidden",s.name=`${e.prefix}${n}`,s.value=i(this.values&&this.values[n]||""),t.appendChild(s)}this.appendChild(t)}set values(t){if(this._values=t,this.isConnected)for(const t of this._fieldNames){const n=this.querySelector(`input[name="${e.prefix}${t}"]`);n?n.value=i(this.values[t]):console.warn(`Field "${t}" not found. `+"Most likely, the '<wc-order-attribution-inputs>' element was manipulated.")}}get values(){return this._values}})}(window.wc_order_attribution);
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
function on_keydown_remove_from_cart(e){" "===e.key&&(e.preventDefault(),e.currentTarget.click())}function focus_populate_live_region(){var e=["woocommerce-message","woocommerce-error","wc-block-components-notice-banner"].map(function(e){return"."+e+'[role="alert"]'}).join(", "),o=document.querySelectorAll(e);if(0!==o.length){var t=o[0];t.setAttribute("tabindex","-1");var n=setTimeout(function(){t.focus(),clearTimeout(n)},500)}}function refresh_sorted_by_live_region(){var e=document.querySelector(".woocommerce-result-count");if(e){var o=e.innerHTML;e.setAttribute("aria-hidden","true");var t=setTimeout(function(){e.setAttribute("aria-hidden","false"),e.innerHTML="",e.innerHTML=o,clearTimeout(t)},2e3)}}function on_document_ready(){focus_populate_live_region(),refresh_sorted_by_live_region()}jQuery(function(e){e(".woocommerce-ordering").on("change","select.orderby",function(){e(this).closest("form").trigger("submit")}),e("input.qty:not(.product-quantity input.qty)").each(function(){var o=parseFloat(e(this).attr("min"));o>=0&&parseFloat(e(this).val())<o&&e(this).val(o)});var o="store_notice"+(e(".woocommerce-store-notice").data("noticeId")||"");if("hidden"===Cookies.get(o))e(".woocommerce-store-notice").hide();else{function t(o){["Enter"," "].includes(o.key)&&(o.preventDefault(),e(".woocommerce-store-notice__dismiss-link").click())}e(".woocommerce-store-notice").show(),e(".woocommerce-store-notice__dismiss-link").on("click",function n(r){Cookies.set(o,"hidden",{path:"/"}),e(".woocommerce-store-notice").hide(),r.preventDefault(),e(".woocommerce-store-notice__dismiss-link").off("click",n).off("keydown",t)}).on("keydown",t)}e(".woocommerce-input-wrapper span.description").length&&e(document.body).on("click",function(){e(".woocommerce-input-wrapper span.description:visible").prop("aria-hidden",!0).slideUp(250)}),e(".woocommerce-input-wrapper").on("click",function(e){e.stopPropagation()}),e(".woocommerce-input-wrapper :input").on("keydown",function(o){var t=e(this).parent().find("span.description");if(27===o.which&&t.length&&t.is(":visible"))return t.prop("aria-hidden",!0).slideUp(250),o.preventDefault(),!1}).on("click focus",function(){var o=e(this).parent(),t=o.find("span.description");o.addClass("currentTarget"),e(".woocommerce-input-wrapper:not(.currentTarget) span.description:visible").prop("aria-hidden",!0).slideUp(250),t.length&&t.is(":hidden")&&t.prop("aria-hidden",!1).slideDown(250),o.removeClass("currentTarget")}),e.scroll_to_notices=function(o){o.length&&e("html, body").animate({scrollTop:o.offset().top-100},1e3)},e('.woocommerce form .woocommerce-Input[type="password"]').wrap('<span class="password-input"></span>'),e(".woocommerce form input").filter(":password").parent("span").addClass("password-input"),e(".password-input").each(function(){const o=e(this).find("input").attr("id");e(this).append('<button type="button" class="show-password-input" aria-label="'+woocommerce_params.i18n_password_show+'" aria-describedBy="'+o+'"></button>')}),e(".show-password-input").on("click",function(o){o.preventDefault(),e(this).hasClass("display-password")?(e(this).removeClass("display-password"),e(this).attr("aria-label",woocommerce_params.i18n_password_show)):(e(this).addClass("display-password"),e(this).attr("aria-label",woocommerce_params.i18n_password_hide)),e(this).hasClass("display-password")?e(this).siblings(['input[type="password"]']).prop("type","text"):e(this).siblings('input[type="text"]').prop("type","password"),e(this).siblings("input").focus()}),e("a.coming-soon-footer-banner-dismiss").on("click",function(o){var t=e(o.target);e.ajax({type:"post",url:t.data("rest-url"),data:{woocommerce_meta:{coming_soon_banner_dismissed:"yes"}},beforeSend:function(e){e.setRequestHeader("X-WP-Nonce",t.data("rest-nonce"))},complete:function(){e("#coming-soon-footer-banner").hide()}})}),"undefined"==typeof wc_add_to_cart_params&&e(document.body).on("keydown",".remove_from_cart_button",on_keydown_remove_from_cart),e(document.body).on("item_removed_from_classic_cart updated_wc_div",focus_populate_live_region)}),document.addEventListener("DOMContentLoaded",on_document_ready);
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
|
||||
|
||||
/*! js-cookie v3.0.5 | MIT */
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self,function(){var n=e.Cookies,o=e.Cookies=t();o.noConflict=function(){return e.Cookies=n,o}}())}(this,function(){"use strict";function e(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var o in n)e[o]=n[o]}return e}return function t(n,o){function r(t,r,i){if("undefined"!=typeof document){"number"==typeof(i=e({},o,i)).expires&&(i.expires=new Date(Date.now()+864e5*i.expires)),i.expires&&(i.expires=i.expires.toUTCString()),t=encodeURIComponent(t).replace(/%(2[346B]|5E|60|7C)/g,decodeURIComponent).replace(/[()]/g,escape);var c="";for(var u in i)i[u]&&(c+="; "+u,!0!==i[u]&&(c+="="+i[u].split(";")[0]));return document.cookie=t+"="+n.write(r,t)+c}}return Object.create({set:r,get:function(e){if("undefined"!=typeof document&&(!arguments.length||e)){for(var t=document.cookie?document.cookie.split("; "):[],o={},r=0;r<t.length;r++){var i=t[r].split("="),c=i.slice(1).join("=");try{var u=decodeURIComponent(i[0]);if(o[u]=n.read(c,u),e===u)break}catch(f){}}return e?o[e]:o}},remove:function(t,n){r(t,"",e({},n,{expires:-1}))},withAttributes:function(n){return t(this.converter,e({},this.attributes,n))},withConverter:function(n){return t(e({},this.converter,n),this.attributes)}},{attributes:{value:Object.freeze(o)},converter:{value:Object.freeze(n)}})}({read:function(e){return'"'===e[0]&&(e=e.slice(1,-1)),e.replace(/(%[\dA-F]{2})+/gi,decodeURIComponent)},write:function(e){return encodeURIComponent(e).replace(/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g,decodeURIComponent)}},{path:"/"})});
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,4 @@
|
||||
|
||||
/*! Created by Cloudways
|
||||
On 22-01-2026 */
|
||||
var timer,ready=e=>{"loading"!=document.readyState?e():document.addEventListener("DOMContentLoaded",e)};function search_for_banned_links(t,r){if(!t.length)return!1;var e=-1!==(r=r.startsWith("/")?r.substring(1):r).indexOf("/")?r.indexOf("/"):r.indexOf("?"),n=-1!==e?r.substring(0,e):r;for(let e=0;e<t.length;e++){const i=t[e];if("wp-admin"===i||"wp-login.php"===i){if(n===i)return!0}else if(r.includes(i)||i.includes(r))return!0}return!1}function breeze_ignore_special_links(e,t){var r=!0;return-1!==t.indexOf("add-to-cart=")&&(r=!1),r=void 0!==e.dataset.product_id&&void 0!==e.dataset.quantity&&void 0!==e.dataset.product_sku?!1:r}ready(()=>{var a=[];addEventListener("mouseover",function(r){var n,i;r.target instanceof HTMLAnchorElement&&(n=new URL(breeze_prefetch.local_url).host,(i=r.target.attributes.href?r.target.attributes.href.value:null)&&(-1===i.indexOf(n)&&(i=r.target.href),timer=setTimeout(function(){var e,t;"#"!==i&&(t=i.replace(breeze_prefetch.local_url,""),e=new URL(i).host,""!==i.trim()&&!1===a.includes(i)&&n===e&&!1===search_for_banned_links(breeze_prefetch.ignore_list,t)&&!0===breeze_ignore_special_links(r.target,i)&&(a.push(i.trim()),(t=document.createElement("link")).href=i,t.rel="prefetch",document.head.appendChild(t)))},150)))}),addEventListener("mouseout",function(e){clearTimeout(timer)})});
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
document.addEventListener("DOMContentLoaded",(function(e){function t(e){if("IMG"!==e.nodeName)return;const t=e.classList.contains(i),n=e.parentElement.classList.contains(i),r=null!==e.closest("."+i);if(("true"===ForceInlineSVGActive||t||r)&&(!svgSettings.skipNested||t||n||!r)){var o=e.id,a=e.className,c=e.src;if(c.endsWith("svg")){var l=new XMLHttpRequest;l.onreadystatechange=function(){if(4===l.readyState&&200===l.status){var t=l.responseText;let c;const d=undefined;var n=(new DOMParser).parseFromString(t,"text/html").getElementsByTagName("svg")[0],i=n.id;if(void 0===o||""===o?void 0===i||""===i?(o="svg-replaced-"+s,n.setAttribute("id",o)):o=i:n.setAttribute("id",o),void 0!==a&&""!==a&&n.setAttribute("class",a+" replaced-svg svg-replaced-"+s),n.removeAttribute("xmlns:a"),"on"===frontSanitizationEnabled&&""!==n.outerHTML){var r=DOMPurify.sanitize(n.outerHTML);e.outerHTML=r}else e.replaceWith(n);s++}else 4===l.readyState&&l.status},l.open("GET",c,!0),l.send(null)}}}function n(e){if(e.childNodes.length>0)for(var s=0;s<e.childNodes.length;s++){var i;if("IMG"===e.childNodes[s].nodeName)t(e.childNodes[s]);else n(e.childNodes[s])}}let s=0,i;(bodhisvgsInlineSupport=function(){if("true"===ForceInlineSVGActive)for(var e=document.getElementsByTagName("img"),s=0;s<e.length;s++)void 0!==e[s].src&&e[s].src.match(/\.(svg)/)&&(e[s].classList.contains(cssTarget.ForceInlineSVG)||e[s].classList.add(cssTarget.ForceInlineSVG));String.prototype.endsWith||(String.prototype.endsWith=function(e,t){var n=this.toString();("number"!=typeof t||!isFinite(t)||Math.floor(t)!==t||t>n.length)&&(t=n.length),t-=e.length;var s=n.lastIndexOf(e,t);return-1!==s&&s===t}),String.prototype.endsWith=function(e){var t=this.length-e.length;return t>=0&&this.lastIndexOf(e)===t},i="true"===ForceInlineSVGActive?"img."!==cssTarget.Bodhi?cssTarget.ForceInlineSVG:"style-svg":"img."!==cssTarget.Bodhi?cssTarget.Bodhi:"style-svg","string"==typeof i&&(i=i.replace("img.",""),document.querySelectorAll("."+i).forEach((function(e){"IMG"===e.nodeName?t(e):n(e)})))})()}));
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
document.addEventListener('DOMContentLoaded',function(){if(phpVars.externalPermalinksEnabled){var links=document.getElementsByTagName('a');for(var i=0;i<links.length;i++){var url=links[i].getAttribute('href');var target=links[i].getAttribute('target');if(url!=null){if(url.indexOf('#new_tab')>=0){url=url.replace('#new_tab','');target='_blank';links[i].setAttribute('href',url);links[i].setAttribute('target',target);links[i].setAttribute('rel','noopener noreferrer nofollow')}}}}})
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,11 @@
|
||||
|
||||
/*!
|
||||
* hoverIntent v1.9.0 // 2017.09.01 // jQuery v1.7.0+
|
||||
* http://briancherne.github.io/jquery-hoverIntent/
|
||||
*
|
||||
* You may use hoverIntent under the terms of the MIT license. Basically that
|
||||
* means you are free to use hoverIntent as long as this header is left intact.
|
||||
* Copyright 2007-2017 Brian Cherne
|
||||
*/
|
||||
!function(factory){"use strict";"function"==typeof define&&define.amd?define(["jquery"],factory):jQuery&&!jQuery.fn.hoverIntent&&factory(jQuery)}(function($){"use strict";var cX,cY,_cfg={interval:100,sensitivity:6,timeout:0},INSTANCE_COUNT=0,track=function(ev){cX=ev.pageX,cY=ev.pageY},compare=function(ev,$el,s,cfg){if(Math.sqrt((s.pX-cX)*(s.pX-cX)+(s.pY-cY)*(s.pY-cY))<cfg.sensitivity)return $el.off(s.event,track),delete s.timeoutId,s.isActive=!0,ev.pageX=cX,ev.pageY=cY,delete s.pX,delete s.pY,cfg.over.apply($el[0],[ev]);s.pX=cX,s.pY=cY,s.timeoutId=setTimeout(function(){compare(ev,$el,s,cfg)},cfg.interval)},delay=function(ev,$el,s,out){return delete $el.data("hoverIntent")[s.id],out.apply($el[0],[ev])};$.fn.hoverIntent=function(handlerIn,handlerOut,selector){var instanceId=INSTANCE_COUNT++,cfg=$.extend({},_cfg);$.isPlainObject(handlerIn)?(cfg=$.extend(cfg,handlerIn),$.isFunction(cfg.out)||(cfg.out=cfg.over)):cfg=$.isFunction(handlerOut)?$.extend(cfg,{over:handlerIn,out:handlerOut,selector:selector}):$.extend(cfg,{over:handlerIn,out:handlerIn,selector:handlerOut});var handleHover=function(e){var ev=$.extend({},e),$el=$(this),hoverIntentData=$el.data("hoverIntent");hoverIntentData||$el.data("hoverIntent",hoverIntentData={});var state=hoverIntentData[instanceId];state||(hoverIntentData[instanceId]=state={id:instanceId}),state.timeoutId&&(state.timeoutId=clearTimeout(state.timeoutId));var mousemove=state.event="mousemove.hoverIntent.hoverIntent"+instanceId;if("mouseenter"===e.type){if(state.isActive)return;state.pX=ev.pageX,state.pY=ev.pageY,$el.off(mousemove,track).on(mousemove,track),state.timeoutId=setTimeout(function(){compare(ev,$el,state,cfg)},cfg.interval)}else{if(!state.isActive)return;$el.off(mousemove,track),state.timeoutId=setTimeout(function(){delay(ev,$el,state,cfg.out)},cfg.timeout)}};return this.on({"mouseenter.hoverIntent":handleHover,"mouseleave.hoverIntent":handleHover},cfg.selector)}});
|
||||
;
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
|
||||
|
||||
|
||||
!function(n){"function"==typeof define&&define.amd?define(["jquery"],function(e){return n(e)}):"object"==typeof module&&"object"==typeof module.exports?module.exports=n(require("jquery")):n(jQuery)}(function(n){function e(n){var e=7.5625,t=2.75;return n<1/t?e*n*n:n<2/t?e*(n-=1.5/t)*n+.75:n<2.5/t?e*(n-=2.25/t)*n+.9375:e*(n-=2.625/t)*n+.984375}void 0!==n.easing&&(n.easing.jswing=n.easing.swing);var t=Math.pow,u=Math.sqrt,r=Math.sin,i=Math.cos,a=Math.PI,o=1.70158,c=1.525*o,s=2*a/3,f=2*a/4.5;return n.extend(n.easing,{def:"easeOutQuad",swing:function(e){return n.easing[n.easing.def](e)},easeInQuad:function(n){return n*n},easeOutQuad:function(n){return 1-(1-n)*(1-n)},easeInOutQuad:function(n){return n<.5?2*n*n:1-t(-2*n+2,2)/2},easeInCubic:function(n){return n*n*n},easeOutCubic:function(n){return 1-t(1-n,3)},easeInOutCubic:function(n){return n<.5?4*n*n*n:1-t(-2*n+2,3)/2},easeInQuart:function(n){return n*n*n*n},easeOutQuart:function(n){return 1-t(1-n,4)},easeInOutQuart:function(n){return n<.5?8*n*n*n*n:1-t(-2*n+2,4)/2},easeInQuint:function(n){return n*n*n*n*n},easeOutQuint:function(n){return 1-t(1-n,5)},easeInOutQuint:function(n){return n<.5?16*n*n*n*n*n:1-t(-2*n+2,5)/2},easeInSine:function(n){return 1-i(n*a/2)},easeOutSine:function(n){return r(n*a/2)},easeInOutSine:function(n){return-(i(a*n)-1)/2},easeInExpo:function(n){return 0===n?0:t(2,10*n-10)},easeOutExpo:function(n){return 1===n?1:1-t(2,-10*n)},easeInOutExpo:function(n){return 0===n?0:1===n?1:n<.5?t(2,20*n-10)/2:(2-t(2,-20*n+10))/2},easeInCirc:function(n){return 1-u(1-t(n,2))},easeOutCirc:function(n){return u(1-t(n-1,2))},easeInOutCirc:function(n){return n<.5?(1-u(1-t(2*n,2)))/2:(u(1-t(-2*n+2,2))+1)/2},easeInElastic:function(n){return 0===n?0:1===n?1:-t(2,10*n-10)*r((10*n-10.75)*s)},easeOutElastic:function(n){return 0===n?0:1===n?1:t(2,-10*n)*r((10*n-.75)*s)+1},easeInOutElastic:function(n){return 0===n?0:1===n?1:n<.5?-t(2,20*n-10)*r((20*n-11.125)*f)/2:t(2,-20*n+10)*r((20*n-11.125)*f)/2+1},easeInBack:function(n){return 2.70158*n*n*n-o*n*n},easeOutBack:function(n){return 1+2.70158*t(n-1,3)+o*t(n-1,2)},easeInOutBack:function(n){return n<.5?t(2*n,2)*(7.189819*n-c)/2:(t(2*n-2,2)*((c+1)*(2*n-2)+c)+2)/2},easeInBounce:function(n){return 1-e(1-n)},easeOutBounce:e,easeInOutBounce:function(n){return n<.5?(1-e(1-2*n))/2:(1+e(2*n-1))/2}}),n});
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
|
||||
|
||||
/*!
|
||||
* jQuery Mousewheel 3.1.13
|
||||
* Copyright OpenJS Foundation and other contributors
|
||||
*/
|
||||
!function(e){"function"==typeof define&&define.amd?define(["jquery"],e):"object"==typeof exports?module.exports=e:e(jQuery)}(function(u){var f,d,e=["wheel","mousewheel","DOMMouseScroll","MozMousePixelScroll"],t="onwheel"in window.document||9<=window.document.documentMode?["wheel"]:["mousewheel","DomMouseScroll","MozMousePixelScroll"],w=Array.prototype.slice;if(u.event.fixHooks)for(var i=e.length;i;)u.event.fixHooks[e[--i]]=u.event.mouseHooks;var c=u.event.special.mousewheel={version:"3.1.12",setup:function(){if(this.addEventListener)for(var e=t.length;e;)this.addEventListener(t[--e],n,{passive:false});else this.onmousewheel=n;u.data(this,"mousewheel-line-height",c.getLineHeight(this)),u.data(this,"mousewheel-page-height",c.getPageHeight(this))},teardown:function(){if(this.removeEventListener)for(var e=t.length;e;)this.removeEventListener(t[--e],n,!1);else this.onmousewheel=null;u.removeData(this,"mousewheel-line-height"),u.removeData(this,"mousewheel-page-height")},getLineHeight:function(e){var t=u(e),i=t["offsetParent"in u.fn?"offsetParent":"parent"]();return i.length||(i=u("body")),parseInt(i.css("fontSize"),10)||parseInt(t.css("fontSize"),10)||16},getPageHeight:function(e){return u(e).height()},settings:{adjustOldDeltas:!0,normalizeOffset:!0}};function n(e){var t,i=e||window.event,n=w.call(arguments,1),o=0,l=0,s=0;if((e=u.event.fix(i)).type="mousewheel","detail"in i&&(s=-1*i.detail),"wheelDelta"in i&&(s=i.wheelDelta),"wheelDeltaY"in i&&(s=i.wheelDeltaY),"wheelDeltaX"in i&&(l=-1*i.wheelDeltaX),"axis"in i&&i.axis===i.HORIZONTAL_AXIS&&(l=-1*s,s=0),o=0===s?l:s,"deltaY"in i&&(o=s=-1*i.deltaY),"deltaX"in i&&(l=i.deltaX,0===s&&(o=-1*l)),0!==s||0!==l){if(1===i.deltaMode){var a=u.data(this,"mousewheel-line-height");o*=a,s*=a,l*=a}else if(2===i.deltaMode){var h=u.data(this,"mousewheel-page-height");o*=h,s*=h,l*=h}if(t=Math.max(Math.abs(s),Math.abs(l)),(!d||t<d)&&g(i,d=t)&&(d/=40),g(i,t)&&(o/=40,l/=40,s/=40),o=Math[1<=o?"floor":"ceil"](o/d),l=Math[1<=l?"floor":"ceil"](l/d),s=Math[1<=s?"floor":"ceil"](s/d),c.settings.normalizeOffset&&this.getBoundingClientRect){var r=this.getBoundingClientRect();e.offsetX=e.clientX-r.left,e.offsetY=e.clientY-r.top}return e.deltaX=l,e.deltaY=s,e.deltaFactor=d,e.deltaMode=0,n.unshift(e,o,l,s),f&&window.clearTimeout(f),f=window.setTimeout(m,200),(u.event.dispatch||u.event.handle).apply(this,n)}}function m(){d=null}function g(e,t){return c.settings.adjustOldDeltas&&"mousewheel"===e.type&&t%120==0}u.fn.extend({mousewheel:function(e){return e?this.on("mousewheel",e):this.trigger("mousewheel")},unmousewheel:function(e){return this.off("mousewheel",e)}})});
|
||||
;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
|
||||
(function($){'use strict';function validURL(str){var pattern=new RegExp('^(https?:\\/\\/)?'+'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+'((\\d{1,3}\\.){3}\\d{1,3}))'+'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+'(\\?[;&a-z\\d%_.~+=-]*)?'+'(\\#[-a-z\\d_]*)?$','i');return!!pattern.test(str)}
|
||||
$(document).ready(function(){var elem='.kc-us-copy-to-clipboard';if($(elem).get(0)){let clipboardLink=new ClipboardJS(elem);clipboardLink.on('success',function(e){let elem=e.trigger;$(elem).find('.kc-us-link').select();let id=elem.getAttribute('id');let copiedTextID='#copied-text-'+id;$(copiedTextID).text('Copied').fadeIn();$(copiedTextID).fadeOut('slow')})}
|
||||
$(".kc_us_create_short_link").click(function(e){e.preventDefault();var post_id=$(this).attr('data-post_id');var security=$(this).attr('data-us-security');$(this).find('.kc_us_loading').show();$.ajax({type:"post",dataType:"json",context:this,url:ajaxurl,data:{action:'us_handle_request',cmd:"create_short_link",post_id:post_id,security:security},success:function(response){if(response.status==="success"){$(this).parent('.us_short_link').html(response.html)}else{$(this).find('.kc_us_loading').hide()}},error:function(err){$(this).find('.kc_us_loading').hide()}})});$("#kc-us-dashboard-short-link").click(function(e){e.preventDefault();var targetURL=$('#kc-us-target-url').val();var slug=$('#kc-us-slug').val();var security=$('#kc-us-security').val();var domain=$('#kc-us-domain').val();if(!validURL(targetURL)){alert('Please Enter Valid Target URL');return}
|
||||
$(this).find('.kc_us_loading').show();$('#kc-us-error-message').hide();$('#kc-us-success-message').hide();$.ajax({type:"post",dataType:"json",context:this,url:ajaxurl,data:{action:'us_handle_request',cmd:"create_short_link",slug:slug,url:targetURL,security:security,domain:domain},success:function(response){if(response.status==="success"){var link=response.link;var html='Short Link : <span class="kc-flex kc-us-copy-to-clipboard" data-clipboard-text="'+link+'" id="link-25"><input type="text" readonly="true" style="width: 65%;" onclick="this.select();" value="'+link+'" class="kc-us-link"></span>';$('#kc-us-success-message').html(html);$('#kc-us-success-message').show()}else{var html='Something went wrong while creating short link';if(response.message){html=response.message}
|
||||
$('#kc-us-error-message').html(html);$('#kc-us-error-message').show()}
|
||||
$('.kc_us_loading').hide()},error:function(err){var html='Something went wrong while creating short link';$('#kc-us-error-message').html(html);$('#kc-us-error-message').show();$('.kc_us_loading').hide()}})});$("#kc-us-submit-btn").click(function(e){e.preventDefault();var targetURL=$('#kc-us-target-url').val();var security=$('#kc-us-security').val();if(!validURL(targetURL)){alert('Please Enter Valid Long URL');return}
|
||||
$(this).parents('.generate-short-link-form').find('.kc_us_loading').show();$.ajax({type:"post",dataType:"json",context:this,url:usParams.ajaxurl,data:{action:'us_handle_request',cmd:"create_short_link",url:targetURL,security:security},success:function(response){$(this).parents('.generate-short-link-form').find('.kc_us_loading').hide();if(response.status==="success"){var link=response.link;$('.generated-short-link-form #kc-us-short-url').val(link);$('.generate-short-link-form').hide();$('.generated-short-link-form').show()}else{var html='Something went wrong while creating short link';$('#kc-us-error-msg').text(html);$('#kc-us-error-msg').show()}},error:function(err){var html='Something went wrong while creating short link';$('#kc-us-error-msg').text(html);$('#kc-us-error-msg').show()}})})})})(jQuery)
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
(()=>{"use strict";const t=window.wp.hooks,e=(t,e)=>{const a={id:"gla_"+t.id,quantity:e,google_business_vertical:"retail"};return t.name&&(a.name=t.name),t?.categories?.length&&(a.category=t.categories[0].name),t?.prices?.price&&(a.price=parseInt(t.prices.price,10)/10**t.prices.currency_minor_unit),a},a=(t,a=1)=>{((t,e)=>{if("function"!=typeof gtag)throw new Error("Function gtag not implemented.");window.gtag("event","add_to_cart",{send_to:"GLA",...e})})(0,{ecomm_pagetype:"cart",event_category:"ecommerce",items:[e(t,a)]})},n=t=>{var e;return glaGtagData.products[t.id]&&(t.name=glaGtagData.products[t.id].name,t.prices=(e=glaGtagData.products[t.id].price,{price:Math.round(e*10**glaGtagData.currency_minor_unit),currency_minor_unit:glaGtagData.currency_minor_unit})),t};(0,t.addAction)("experimental__woocommerce_blocks-cart-add-item","google-listings-and-ads",({product:t,quantity:e=1})=>{a(t,e)});const r=function(t){const e=t.currentTarget.dataset,r=n({id:e.product_id});a(r,e.quantity||1)},o=function(t){const e=t.target.closest("form.cart");if(!e)return;const r=e.querySelector("[name=add-to-cart]");if(!r)return;const o=e.querySelector("[name=variation_id]"),c=e.querySelector("[name=quantity]"),i=n({id:parseInt(o?o.value:r.value,10)});a(i,c?parseInt(c.value,10):1)};document.defaultView.addEventListener("DOMContentLoaded",function(){document.querySelectorAll(".add_to_cart_button:not( .product_type_variable ):not( .product_type_grouped ):not( .wc-block-components-product-button__button )").forEach(t=>{t.addEventListener("click",r)}),document.querySelectorAll('[data-block-name="woocommerce/product-button"] > .add_to_cart_button:not( .product_type_variable ):not( .product_type_grouped )').forEach(t=>{t.addEventListener("click",r)}),document.querySelectorAll(".single_add_to_cart_button").forEach(t=>{t.addEventListener("click",o)})}),"function"==typeof jQuery&&jQuery(document).on("found_variation","form.cart",function(t,e){(t=>{t?.variation_id&&(glaGtagData.products[t.variation_id]={name:t.display_name,price:t.display_price})})(e)})})()
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user