- Created setup-directus-collections.js to automate the creation of tech stack collections, fields, and relations in Directus. - Created setup-directus-hobbies.js for setting up hobbies collection with translations. - Created setup-directus-projects.js for establishing projects collection with comprehensive fields and translations. - Added setup-tech-stack-directus.js to populate tech_stack_items with predefined data.
411 lines
8.1 KiB
Markdown
411 lines
8.1 KiB
Markdown
# 🎛️ Dynamic Activity System - Custom Fields ohne Deployment
|
|
|
|
## 🚀 Problem gelöst
|
|
|
|
**Vorher:**
|
|
- Neue Activity = Schema-Änderung + Code-Update + Deployment
|
|
- Hardcoded fields wie `reading_book`, `working_out_activity`, etc.
|
|
|
|
**Jetzt:**
|
|
- Neue Activity = Nur n8n Workflow anpassen
|
|
- JSON field `custom_activities` für alles
|
|
- ✅ Zero Downtime
|
|
- ✅ Kein Deployment nötig
|
|
|
|
---
|
|
|
|
## 📊 Schema
|
|
|
|
```sql
|
|
ALTER TABLE activity_status
|
|
ADD COLUMN custom_activities JSONB DEFAULT '{}';
|
|
```
|
|
|
|
**Struktur:**
|
|
```json
|
|
{
|
|
"reading": {
|
|
"enabled": true,
|
|
"book_title": "Clean Code",
|
|
"author": "Robert C. Martin",
|
|
"progress": 65,
|
|
"platform": "hardcover",
|
|
"cover_url": "https://..."
|
|
},
|
|
"working_out": {
|
|
"enabled": true,
|
|
"activity": "Running",
|
|
"duration_minutes": 45,
|
|
"calories": 350
|
|
},
|
|
"learning": {
|
|
"enabled": true,
|
|
"course": "Docker Deep Dive",
|
|
"platform": "Udemy",
|
|
"progress": 23
|
|
},
|
|
"streaming": {
|
|
"enabled": true,
|
|
"platform": "Twitch",
|
|
"viewers": 42,
|
|
"game": "Minecraft"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🔧 n8n Workflow Beispiel
|
|
|
|
### Workflow: "Update Custom Activity"
|
|
|
|
**Node 1: Webhook (POST)**
|
|
```
|
|
URL: /webhook/custom-activity
|
|
Method: POST
|
|
Body: {
|
|
"type": "reading",
|
|
"data": {
|
|
"enabled": true,
|
|
"book_title": "Clean Code",
|
|
"author": "Robert C. Martin",
|
|
"progress": 65
|
|
}
|
|
}
|
|
```
|
|
|
|
**Node 2: Function - Build JSON**
|
|
```javascript
|
|
const { type, data } = items[0].json;
|
|
|
|
return [{
|
|
json: {
|
|
type,
|
|
data,
|
|
query: `
|
|
UPDATE activity_status
|
|
SET custom_activities = jsonb_set(
|
|
COALESCE(custom_activities, '{}'::jsonb),
|
|
'{${type}}',
|
|
$1::jsonb
|
|
),
|
|
updated_at = NOW()
|
|
WHERE id = 1
|
|
`,
|
|
params: [JSON.stringify(data)]
|
|
}
|
|
}];
|
|
```
|
|
|
|
**Node 3: PostgreSQL**
|
|
- Query: `={{$json.query}}`
|
|
- Parameters: `={{$json.params}}`
|
|
|
|
---
|
|
|
|
## 🎨 Frontend Integration
|
|
|
|
### TypeScript Interface
|
|
|
|
```typescript
|
|
interface CustomActivity {
|
|
enabled: boolean;
|
|
[key: string]: any; // Dynamisch!
|
|
}
|
|
|
|
interface StatusData {
|
|
// ... existing fields
|
|
customActivities?: Record<string, CustomActivity>;
|
|
}
|
|
```
|
|
|
|
### API Route Update
|
|
|
|
```typescript
|
|
// app/api/n8n/status/route.ts
|
|
export async function GET() {
|
|
const statusData = await fetch(n8nWebhookUrl);
|
|
|
|
return NextResponse.json({
|
|
// ... existing fields
|
|
customActivities: statusData.custom_activities || {}
|
|
});
|
|
}
|
|
```
|
|
|
|
### Component Rendering
|
|
|
|
```tsx
|
|
// app/components/ActivityFeed.tsx
|
|
{Object.entries(data.customActivities || {}).map(([type, activity]) => {
|
|
if (!activity.enabled) return null;
|
|
|
|
return (
|
|
<motion.div key={type} className="custom-activity-card">
|
|
<h3>{type.charAt(0).toUpperCase() + type.slice(1)}</h3>
|
|
|
|
{/* Generic renderer basierend auf Feldern */}
|
|
{Object.entries(activity).map(([key, value]) => {
|
|
if (key === 'enabled') return null;
|
|
|
|
return (
|
|
<div key={key}>
|
|
<span>{key.replace(/_/g, ' ')}: </span>
|
|
<strong>{value}</strong>
|
|
</div>
|
|
);
|
|
})}
|
|
</motion.div>
|
|
);
|
|
})}
|
|
```
|
|
|
|
---
|
|
|
|
## 📱 Beispiele
|
|
|
|
### 1. Reading Activity (Hardcover Integration)
|
|
|
|
**n8n Workflow:**
|
|
```
|
|
Hardcover API → Get Currently Reading → Update Database
|
|
```
|
|
|
|
**Webhook Body:**
|
|
```json
|
|
{
|
|
"type": "reading",
|
|
"data": {
|
|
"enabled": true,
|
|
"book_title": "Clean Architecture",
|
|
"author": "Robert C. Martin",
|
|
"progress": 45,
|
|
"platform": "hardcover",
|
|
"cover_url": "https://covers.openlibrary.org/...",
|
|
"started_at": "2025-01-20"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Frontend zeigt:**
|
|
```
|
|
📖 Reading
|
|
Clean Architecture by Robert C. Martin
|
|
Progress: 45%
|
|
[Progress Bar]
|
|
```
|
|
|
|
---
|
|
|
|
### 2. Workout Activity (Strava/Apple Health)
|
|
|
|
**Webhook Body:**
|
|
```json
|
|
{
|
|
"type": "working_out",
|
|
"data": {
|
|
"enabled": true,
|
|
"activity": "Running",
|
|
"duration_minutes": 45,
|
|
"distance_km": 7.2,
|
|
"calories": 350,
|
|
"avg_pace": "6:15 /km",
|
|
"started_at": "2025-01-23T06:30:00Z"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Frontend zeigt:**
|
|
```
|
|
🏃 Working Out
|
|
Running - 7.2 km in 45 minutes
|
|
350 calories burned
|
|
```
|
|
|
|
---
|
|
|
|
### 3. Learning Activity (Udemy/Coursera)
|
|
|
|
**Webhook Body:**
|
|
```json
|
|
{
|
|
"type": "learning",
|
|
"data": {
|
|
"enabled": true,
|
|
"course": "Docker Deep Dive",
|
|
"platform": "Udemy",
|
|
"instructor": "Nigel Poulton",
|
|
"progress": 67,
|
|
"time_spent_hours": 8.5
|
|
}
|
|
}
|
|
```
|
|
|
|
**Frontend zeigt:**
|
|
```
|
|
🎓 Learning
|
|
Docker Deep Dive on Udemy
|
|
Progress: 67% (8.5 hours)
|
|
```
|
|
|
|
---
|
|
|
|
### 4. Live Streaming
|
|
|
|
**Webhook Body:**
|
|
```json
|
|
{
|
|
"type": "streaming",
|
|
"data": {
|
|
"enabled": true,
|
|
"platform": "Twitch",
|
|
"title": "Building a Portfolio with Next.js",
|
|
"viewers": 42,
|
|
"game": "Software Development",
|
|
"url": "https://twitch.tv/yourname"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Frontend zeigt:**
|
|
```
|
|
📺 LIVE on Twitch
|
|
Building a Portfolio with Next.js
|
|
👥 42 viewers
|
|
[Watch Stream →]
|
|
```
|
|
|
|
---
|
|
|
|
## 🔥 Clear Activity
|
|
|
|
**Webhook zum Deaktivieren:**
|
|
```bash
|
|
curl -X POST https://n8n.example.com/webhook/custom-activity \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"type": "reading",
|
|
"data": {
|
|
"enabled": false
|
|
}
|
|
}'
|
|
```
|
|
|
|
**Alle Custom Activities clearen:**
|
|
```sql
|
|
UPDATE activity_status
|
|
SET custom_activities = '{}'::jsonb
|
|
WHERE id = 1;
|
|
```
|
|
|
|
---
|
|
|
|
## 🎯 Vorteile
|
|
|
|
| Feature | Vorher | Nachher |
|
|
|---------|--------|---------|
|
|
| **Neue Activity** | Schema + Code + Deploy | Nur n8n Workflow |
|
|
| **Activity entfernen** | Schema + Code + Deploy | Webhook mit `enabled: false` |
|
|
| **Deployment** | Ja | Nein |
|
|
| **Downtime** | Ja | Nein |
|
|
| **Flexibilität** | Starr | Komplett dynamisch |
|
|
|
|
---
|
|
|
|
## 🚀 Migration
|
|
|
|
```bash
|
|
# 1. Schema erweitern
|
|
psql -d portfolio_dev -f prisma/migrations/add_custom_activities.sql
|
|
|
|
# 2. Prisma Schema updaten
|
|
# prisma/schema.prisma
|
|
# customActivities Json? @map("custom_activities")
|
|
|
|
# 3. Prisma Generate
|
|
npx prisma generate
|
|
|
|
# 4. Fertig! Keine weiteren Code-Änderungen nötig
|
|
```
|
|
|
|
---
|
|
|
|
## 🎨 Smart Renderer Component
|
|
|
|
```tsx
|
|
// components/CustomActivityCard.tsx
|
|
interface CustomActivityCardProps {
|
|
type: string;
|
|
data: Record<string, any>;
|
|
}
|
|
|
|
export function CustomActivityCard({ type, data }: CustomActivityCardProps) {
|
|
const icon = getIconForType(type); // Mapping: reading → 📖, working_out → 🏃
|
|
const title = type.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
|
|
|
return (
|
|
<motion.div className="bg-gradient-to-br from-purple-500/10 to-blue-500/5 rounded-xl p-4">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<span className="text-2xl">{icon}</span>
|
|
<h3 className="font-bold">{title}</h3>
|
|
</div>
|
|
|
|
{/* Render fields dynamically */}
|
|
<div className="space-y-1">
|
|
{Object.entries(data).map(([key, value]) => {
|
|
if (key === 'enabled') return null;
|
|
|
|
// Special handling for specific fields
|
|
if (key === 'progress' && typeof value === 'number') {
|
|
return (
|
|
<div key={key}>
|
|
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
|
|
<div
|
|
className="h-full bg-blue-500 transition-all"
|
|
style={{ width: `${value}%` }}
|
|
/>
|
|
</div>
|
|
<span className="text-xs text-gray-500">{value}%</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Default: key-value pair
|
|
return (
|
|
<div key={key} className="text-sm">
|
|
<span className="text-gray-500">{formatKey(key)}: </span>
|
|
<span className="font-medium">{formatValue(value)}</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
function getIconForType(type: string): string {
|
|
const icons: Record<string, string> = {
|
|
reading: '📖',
|
|
working_out: '🏃',
|
|
learning: '🎓',
|
|
streaming: '📺',
|
|
cooking: '👨🍳',
|
|
traveling: '✈️',
|
|
};
|
|
return icons[type] || '✨';
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🎯 Zusammenfassung
|
|
|
|
Mit dem `custom_activities` JSONB Field kannst du:
|
|
- ✅ Beliebig viele Activity-Typen hinzufügen
|
|
- ✅ Ohne Schema-Änderungen
|
|
- ✅ Ohne Code-Deployments
|
|
- ✅ Nur über n8n Webhooks steuern
|
|
- ✅ Frontend rendert automatisch alles
|
|
|
|
**Das ist TRUE DYNAMIC! 🚀**
|