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

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_activities fü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! 🚀