Files
portfolio/docs/DYNAMIC_ACTIVITY_CUSTOM_FIELDS.md
denshooter e431ff50fc feat: Add Directus setup scripts for collections, fields, and relations
- 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.
2026-01-23 02:53:31 +01:00

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! 🚀**