- 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.
8.1 KiB
8.1 KiB
🎛️ 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_activitiesfür alles - ✅ Zero Downtime
- ✅ Kein Deployment nötig
📊 Schema
ALTER TABLE activity_status
ADD COLUMN custom_activities JSONB DEFAULT '{}';
Struktur:
{
"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
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
interface CustomActivity {
enabled: boolean;
[key: string]: any; // Dynamisch!
}
interface StatusData {
// ... existing fields
customActivities?: Record<string, CustomActivity>;
}
API Route Update
// 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
// 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:
{
"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:
{
"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:
{
"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:
{
"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:
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:
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
# 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
// 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! 🚀