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.
This commit is contained in:
410
docs/DYNAMIC_ACTIVITY_CUSTOM_FIELDS.md
Normal file
410
docs/DYNAMIC_ACTIVITY_CUSTOM_FIELDS.md
Normal file
@@ -0,0 +1,410 @@
|
||||
# 🎛️ 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! 🚀**
|
||||
Reference in New Issue
Block a user