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:
2026-01-23 02:53:31 +01:00
parent 7604e00e0f
commit e431ff50fc
28 changed files with 5253 additions and 23 deletions

184
scripts/README.md Normal file
View File

@@ -0,0 +1,184 @@
# Directus Setup & Migration Scripts
Automatische Scripts zum Erstellen und Befüllen aller Collections in Directus.
## 📦 Verfügbare Scripts
### 1. Tech Stack (✅ Bereits ausgeführt)
```bash
# Collections erstellen
node scripts/setup-directus-collections.js
# Daten migrieren
node scripts/migrate-tech-stack-to-directus.js
```
**Was erstellt wird:**
- `tech_stack_categories` (4 Kategorien: Frontend, Backend, Tools, Security)
- `tech_stack_items` (~16 Items)
- Translations (DE + EN)
---
### 2. Projects (🔥 Neu)
```bash
# Collections erstellen
node scripts/setup-directus-projects.js
# Daten aus PostgreSQL migrieren
node scripts/migrate-projects-to-directus.js
```
**Was erstellt wird:**
- `projects` Collection mit 30+ Feldern:
- Basics: slug, title, description, content
- Meta: category, difficulty, tags, technologies
- Links: github, live, image_url, demo_video
- Details: challenges, lessons_learned, future_improvements
- Performance: lighthouse scores, bundle sizes
- `projects_translations` für mehrsprachige Inhalte
- Migriert ALLE Projekte aus PostgreSQL
**Hinweis:** Läuft nur wenn Projects Collection noch nicht existiert!
---
### 3. Hobbies (🎮 Neu)
```bash
# Collections erstellen
node scripts/setup-directus-hobbies.js
# Daten migrieren
node scripts/migrate-hobbies-to-directus.js
```
**Was erstellt wird:**
- `hobbies` Collection (4 Hobbies: Self-Hosting, Gaming, Game Servers, Jogging)
- Translations (DE + EN)
---
## 🚀 Komplette Migration (alles auf einmal)
```bash
# 1. Tech Stack
node scripts/setup-directus-collections.js
node scripts/migrate-tech-stack-to-directus.js
# 2. Projects
node scripts/setup-directus-projects.js
node scripts/migrate-projects-to-directus.js
# 3. Hobbies
node scripts/setup-directus-hobbies.js
node scripts/migrate-hobbies-to-directus.js
```
**Oder als One-Liner:**
```bash
node scripts/setup-directus-collections.js && \
node scripts/migrate-tech-stack-to-directus.js && \
node scripts/setup-directus-projects.js && \
node scripts/migrate-projects-to-directus.js && \
node scripts/setup-directus-hobbies.js && \
node scripts/migrate-hobbies-to-directus.js
```
---
## ⚙️ Voraussetzungen
```bash
# Dependencies installieren
npm install node-fetch@2 dotenv @prisma/client
```
**In .env:**
```env
DIRECTUS_URL=https://cms.dk0.dev
DIRECTUS_STATIC_TOKEN=your_token_here
DATABASE_URL=postgresql://...
```
---
## 📊 Nach der Migration
### Directus Admin Panel:
- **Tech Stack:** https://cms.dk0.dev/admin/content/tech_stack_categories
- **Projects:** https://cms.dk0.dev/admin/content/projects
- **Hobbies:** https://cms.dk0.dev/admin/content/hobbies
### API Endpoints (automatisch verfügbar):
```bash
# Tech Stack
GET https://cms.dk0.dev/items/tech_stack_categories?fields=*,translations.*,items.*
# Projects
GET https://cms.dk0.dev/items/projects?fields=*,translations.*
# Hobbies
GET https://cms.dk0.dev/items/hobbies?fields=*,translations.*
```
---
## 🔄 Code-Updates nach Migration
### 1. lib/directus.ts erweitern
```typescript
// Bereits implementiert:
export async function getTechStack(locale: string)
// TODO:
export async function getProjects(locale: string)
export async function getHobbies(locale: string)
```
### 2. Components anpassen
- `About.tsx` - ✅ Bereits updated für Tech Stack
- `About.tsx` - TODO: Hobbies aus Directus laden
- `Projects.tsx` - TODO: Projects aus Directus laden
---
## 🐛 Troubleshooting
### Error: "Collection already exists"
→ Normal! Script überspringt bereits existierende Collections automatisch.
### Error: "DIRECTUS_STATIC_TOKEN not found"
→ Stelle sicher dass `.env` vorhanden ist und `require('dotenv').config()` funktioniert.
### Error: "Unauthorized" oder HTTP 403
→ Überprüfe Token-Rechte in Directus Admin → Settings → Access Tokens
### Migration findet keine Projekte
→ Stelle sicher dass PostgreSQL läuft und `DATABASE_URL` korrekt ist.
---
## 📝 Nächste Schritte
1.**Alle Scripts ausführen** (siehe oben)
2.**Verifizieren** in Directus Admin Panel
3. ⏭️ **Code updaten** (lib/directus.ts + Components)
4. ⏭️ **Testen** auf localhost
5. ⏭️ **Deployen** auf Production
---
## 💡 Pro-Tipps
- **Backups:** Exportiere Schema regelmäßig via Directus UI
- **Version Control:** Committe Schema-Files ins Git
- **Incremental:** Scripts können mehrfach ausgeführt werden (idempotent)
- **Rollback:** Lösche Collections in Directus UI falls nötig

View File

@@ -0,0 +1,106 @@
#!/usr/bin/env node
/**
* Add German translations for projects in Directus (if missing).
* - Reads projects from Directus REST
* - If no de-DE translation exists, creates one using provided fallback strings
*/
const fetch = require('node-fetch');
require('dotenv').config();
const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev';
const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN;
if (!DIRECTUS_TOKEN) {
console.error('❌ DIRECTUS_STATIC_TOKEN missing');
process.exit(1);
}
const deFallback = {
'kernel-panic-404-interactive-terminal': {
title: 'Kernel Panic 404 Interaktives Terminal',
description: 'Ein spielerisches 404-Erlebnis als interaktives Terminal mit Retro-Feeling.',
},
'machine-learning-model-api': {
title: 'Machine-Learning-Modell API',
description: 'Produktionsreife API für ML-Modelle mit klarer Dokumentation und Monitoring.',
},
'weather-forecast-app': {
title: 'Wettervorhersage App',
description: 'Schnelle Wetter-UI mit klaren Prognosen und responsivem Design.',
},
'task-management-dashboard': {
title: 'Task-Management Dashboard',
description: 'Kanban-Board mit Kollaboration, Filtern und Realtime-Updates.',
},
'real-time-chat-application': {
title: 'Echtzeit Chat App',
description: 'Websocket-basierter Chat mit Typing-Status, Presence und Uploads.',
},
'e-commerce-platform-api': {
title: 'E-Commerce Plattform API',
description: 'Headless Commerce API mit Checkout, Inventory und Webhooks.',
},
'portfolio-website-modern-developer-showcase': {
title: 'Portfolio Website Moderner Entwicklerauftritt',
description: 'Schnelle, übersichtliche Portfolio-Seite mit Projekten und Aktivitäten.',
},
clarity: {
title: 'Clarity Dyslexie-Unterstützung',
description: 'Mobile App mit OpenDyslexic Schrift und AI-Textvereinfachung.',
},
};
async function directus(path, options = {}) {
const res = await fetch(`${DIRECTUS_URL}/${path}`, {
...options,
headers: {
Authorization: `Bearer ${DIRECTUS_TOKEN}`,
'Content-Type': 'application/json',
...(options.headers || {}),
},
});
if (!res.ok) {
const text = await res.text();
throw new Error(`HTTP ${res.status} ${path}: ${text}`);
}
return res.json();
}
async function main() {
console.log('🔍 Fetching projects from Directus...');
const { data: projects } = await directus(
'items/projects?fields=id,slug,translations.languages_code,translations.title,translations.description'
);
let created = 0;
for (const proj of projects) {
const hasDe = (proj.translations || []).some((t) => t.languages_code === 'de-DE');
if (hasDe) continue;
const fallback = deFallback[proj.slug] || {};
const en = (proj.translations || [])[0] || {};
const payload = {
projects_id: proj.id,
languages_code: 'de-DE',
title: fallback.title || en.title || proj.slug,
description: fallback.description || en.description || en.title || proj.slug,
content: en.content || null,
meta_description: null,
keywords: null,
};
await directus('items/projects_translations', {
method: 'POST',
body: JSON.stringify(payload),
});
created += 1;
console.log(` Added de-DE translation for ${proj.slug}`);
}
console.log(`✅ Done. Added ${created} de-DE translations.`);
}
main().catch((err) => {
console.error('❌ Failed:', err.message);
process.exit(1);
});

View File

@@ -0,0 +1,123 @@
#!/usr/bin/env node
/**
* Migrate Content Pages from PostgreSQL (Prisma) to Directus
*
* - Copies `content_pages` + translations from Postgres into Directus
* - Creates or updates items per (slug, locale)
*
* Usage:
* DATABASE_URL=postgresql://... DIRECTUS_STATIC_TOKEN=... DIRECTUS_URL=... \
* node scripts/migrate-content-pages-to-directus.js
*/
const fetch = require('node-fetch');
const { PrismaClient } = require('@prisma/client');
require('dotenv').config();
const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev';
const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN;
if (!DIRECTUS_TOKEN) {
console.error('❌ Error: DIRECTUS_STATIC_TOKEN not found in env');
process.exit(1);
}
const prisma = new PrismaClient();
const localeMap = {
en: 'en-US',
de: 'de-DE',
};
function toDirectusLocale(locale) {
return localeMap[locale] || locale;
}
async function directusRequest(endpoint, method = 'GET', body = null) {
const url = `${DIRECTUS_URL}/${endpoint}`;
const options = {
method,
headers: {
Authorization: `Bearer ${DIRECTUS_TOKEN}`,
'Content-Type': 'application/json',
},
};
if (body) {
options.body = JSON.stringify(body);
}
const res = await fetch(url, options);
if (!res.ok) {
const text = await res.text();
throw new Error(`HTTP ${res.status} on ${endpoint}: ${text}`);
}
return res.json();
}
async function upsertContentIntoDirectus({ slug, locale, status, title, content }) {
const directusLocale = toDirectusLocale(locale);
// allow locale-specific slug variants: base for en, base-locale for others
const slugVariant = directusLocale === 'en-US' ? slug : `${slug}-${directusLocale.toLowerCase()}`;
const payload = {
slug: slugVariant,
locale: directusLocale,
status: status?.toLowerCase?.() === 'published' ? 'published' : status || 'draft',
title: title || slug,
content: content || null,
};
try {
const { data } = await directusRequest('items/content_pages', 'POST', payload);
console.log(` Created ${slugVariant} (${directusLocale}) [id=${data?.id}]`);
return data?.id;
} catch (error) {
const msg = error?.message || '';
if (msg.includes('already exists') || msg.includes('duplicate key') || msg.includes('UNIQUE')) {
console.log(` ⚠️ Skipping ${slugVariant} (${directusLocale}) already exists`);
return null;
}
throw error;
}
}
async function migrateContentPages() {
console.log('\n📦 Migrating Content Pages from PostgreSQL to Directus...');
const pages = await prisma.contentPage.findMany({
include: { translations: true },
});
console.log(`Found ${pages.length} pages in PostgreSQL`);
for (const page of pages) {
const status = page.status || 'PUBLISHED';
for (const tr of page.translations) {
await upsertContentIntoDirectus({
slug: page.key,
locale: tr.locale,
status,
title: tr.title,
content: tr.content,
});
}
}
console.log('✅ Content page migration finished.');
}
async function main() {
try {
await prisma.$connect();
await migrateContentPages();
} catch (error) {
console.error('❌ Migration failed:', error.message);
process.exit(1);
} finally {
await prisma.$disconnect();
}
}
main();

View File

@@ -0,0 +1,185 @@
#!/usr/bin/env node
/**
* Migrate Hobbies to Directus
*
* Migriert Hobbies-Daten aus messages/en.json und messages/de.json nach Directus
*
* Usage:
* node scripts/migrate-hobbies-to-directus.js
*/
const fetch = require('node-fetch');
const fs = require('fs');
const path = require('path');
require('dotenv').config();
const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev';
const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN;
if (!DIRECTUS_TOKEN) {
console.error('❌ Error: DIRECTUS_STATIC_TOKEN not found in .env');
process.exit(1);
}
const messagesEn = JSON.parse(
fs.readFileSync(path.join(__dirname, '../messages/en.json'), 'utf-8')
);
const messagesDe = JSON.parse(
fs.readFileSync(path.join(__dirname, '../messages/de.json'), 'utf-8')
);
const hobbiesEn = messagesEn.home.about.hobbies;
const hobbiesDe = messagesDe.home.about.hobbies;
const HOBBIES_DATA = [
{
key: 'self_hosting',
icon: 'Code',
titleEn: hobbiesEn.selfHosting,
titleDe: hobbiesDe.selfHosting
},
{
key: 'gaming',
icon: 'Gamepad2',
titleEn: hobbiesEn.gaming,
titleDe: hobbiesDe.gaming
},
{
key: 'game_servers',
icon: 'Server',
titleEn: hobbiesEn.gameServers,
titleDe: hobbiesDe.gameServers
},
{
key: 'jogging',
icon: 'Activity',
titleEn: hobbiesEn.jogging,
titleDe: hobbiesDe.jogging
}
];
async function directusRequest(endpoint, method = 'GET', body = null) {
const url = `${DIRECTUS_URL}/${endpoint}`;
const options = {
method,
headers: {
'Authorization': `Bearer ${DIRECTUS_TOKEN}`,
'Content-Type': 'application/json'
}
};
if (body) {
options.body = JSON.stringify(body);
}
try {
const response = await fetch(url, options);
if (!response.ok) {
const text = await response.text();
throw new Error(`HTTP ${response.status}: ${text}`);
}
return await response.json();
} catch (error) {
console.error(`Error calling ${method} ${endpoint}:`, error.message);
throw error;
}
}
async function migrateHobbies() {
console.log('\n📦 Migrating Hobbies to Directus...\n');
for (const hobby of HOBBIES_DATA) {
console.log(`\n🎮 Hobby: ${hobby.key}`);
try {
// 1. Create Hobby
console.log(' Creating hobby...');
const hobbyData = {
key: hobby.key,
icon: hobby.icon,
status: 'published',
sort: HOBBIES_DATA.indexOf(hobby) + 1
};
const { data: createdHobby } = await directusRequest(
'items/hobbies',
'POST',
hobbyData
);
console.log(` ✅ Hobby created with ID: ${createdHobby.id}`);
// 2. Create Translations
console.log(' Creating translations...');
// English Translation
await directusRequest(
'items/hobbies_translations',
'POST',
{
hobbies_id: createdHobby.id,
languages_code: 'en-US',
title: hobby.titleEn
}
);
// German Translation
await directusRequest(
'items/hobbies_translations',
'POST',
{
hobbies_id: createdHobby.id,
languages_code: 'de-DE',
title: hobby.titleDe
}
);
console.log(' ✅ Translations created (en-US, de-DE)');
} catch (error) {
console.error(` ❌ Error migrating ${hobby.key}:`, error.message);
}
}
console.log('\n✨ Migration complete!\n');
}
async function verifyMigration() {
console.log('\n🔍 Verifying Migration...\n');
try {
const { data: hobbies } = await directusRequest(
'items/hobbies?fields=key,icon,status,translations.title,translations.languages_code'
);
console.log(`✅ Found ${hobbies.length} hobbies in Directus:`);
hobbies.forEach(h => {
const enTitle = h.translations?.find(t => t.languages_code === 'en-US')?.title;
console.log(` - ${h.key}: "${enTitle}"`);
});
console.log('\n🎉 Hobbies successfully migrated!\n');
console.log('Next steps:');
console.log(' 1. Visit: https://cms.dk0.dev/admin/content/hobbies');
console.log(' 2. Update About.tsx to load hobbies from Directus\n');
} catch (error) {
console.error('❌ Verification failed:', error.message);
}
}
async function main() {
console.log('\n╔════════════════════════════════════════╗');
console.log('║ Hobbies Migration to Directus ║');
console.log('╚════════════════════════════════════════╝\n');
try {
await migrateHobbies();
await verifyMigration();
} catch (error) {
console.error('\n❌ Migration failed:', error);
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,225 @@
#!/usr/bin/env node
/**
* Migrate Projects from PostgreSQL to Directus
*
* Migriert ALLE bestehenden Projects aus deiner PostgreSQL Datenbank nach Directus
* inklusive aller Felder und Translations.
*
* Usage:
* node scripts/migrate-projects-to-directus.js
*/
const fetch = require('node-fetch');
const { PrismaClient } = require('@prisma/client');
require('dotenv').config();
const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev';
const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN;
if (!DIRECTUS_TOKEN) {
console.error('❌ Error: DIRECTUS_STATIC_TOKEN not found in .env');
process.exit(1);
}
const prisma = new PrismaClient();
async function directusRequest(endpoint, method = 'GET', body = null) {
const url = `${DIRECTUS_URL}/${endpoint}`;
const options = {
method,
headers: {
'Authorization': `Bearer ${DIRECTUS_TOKEN}`,
'Content-Type': 'application/json'
}
};
if (body) {
options.body = JSON.stringify(body);
}
try {
const response = await fetch(url, options);
if (!response.ok) {
const text = await response.text();
throw new Error(`HTTP ${response.status}: ${text}`);
}
return await response.json();
} catch (error) {
console.error(`Error calling ${method} ${endpoint}:`, error.message);
throw error;
}
}
async function migrateProjects() {
console.log('\n📦 Migrating Projects from PostgreSQL to Directus...\n');
// Load all published projects from PostgreSQL
const projects = await prisma.project.findMany({
where: { published: true },
include: {
translations: true
},
orderBy: { createdAt: 'desc' }
});
console.log(`Found ${projects.length} published projects in PostgreSQL\n`);
let successCount = 0;
let errorCount = 0;
for (const project of projects) {
console.log(`\n📁 Migrating: ${project.title}`);
try {
// 1. Create project in Directus
console.log(' Creating project...');
const projectData = {
slug: project.slug,
status: 'published',
featured: project.featured,
category: project.category,
difficulty: project.difficulty,
date: project.date,
time_to_complete: project.timeToComplete,
github: project.github,
live: project.live,
image_url: project.imageUrl,
demo_video: project.demoVideo,
color_scheme: project.colorScheme,
accessibility: project.accessibility,
tags: project.tags,
technologies: project.technologies,
challenges: project.challenges,
lessons_learned: project.lessonsLearned,
future_improvements: project.futureImprovements,
screenshots: project.screenshots,
performance: project.performance
};
const { data: createdProject } = await directusRequest(
'items/projects',
'POST',
projectData
);
console.log(` ✅ Project created with ID: ${createdProject.id}`);
// 2. Create Translations
console.log(' Creating translations...');
// Default locale translation (from main project fields)
await directusRequest(
'items/projects_translations',
'POST',
{
projects_id: createdProject.id,
languages_code: project.defaultLocale === 'en' ? 'en-US' : 'de-DE',
title: project.title,
description: project.description,
content: project.content,
meta_description: project.metaDescription,
keywords: project.keywords
}
);
// Additional translations from ProjectTranslation table
for (const translation of project.translations) {
// Skip if it's the same as default locale (already created above)
if (translation.locale === project.defaultLocale) {
continue;
}
await directusRequest(
'items/projects_translations',
'POST',
{
projects_id: createdProject.id,
languages_code: translation.locale === 'en' ? 'en-US' : 'de-DE',
title: translation.title,
description: translation.description,
content: translation.content ? JSON.stringify(translation.content) : null,
meta_description: translation.metaDescription,
keywords: translation.keywords
}
);
}
console.log(` ✅ Translations created (${project.translations.length + 1} locales)`);
successCount++;
} catch (error) {
console.error(` ❌ Error migrating ${project.title}:`, error.message);
errorCount++;
}
}
console.log('\n╔════════════════════════════════════════╗');
console.log(`║ Migration Complete! ║`);
console.log('╚════════════════════════════════════════╝\n');
console.log(`✅ Successfully migrated: ${successCount} projects`);
console.log(`❌ Failed: ${errorCount} projects\n`);
if (successCount > 0) {
console.log('🎉 Projects are now in Directus!\n');
console.log('Next steps:');
console.log(' 1. Visit: https://cms.dk0.dev/admin/content/projects');
console.log(' 2. Verify all projects are visible');
console.log(' 3. Update lib/directus.ts with getProjects() function');
console.log(' 4. Update components to use Directus API\n');
}
}
async function verifyMigration() {
console.log('\n🔍 Verifying Migration...\n');
try {
const { data: projects } = await directusRequest(
'items/projects?fields=slug,status,translations.title,translations.languages_code'
);
console.log(`✅ Found ${projects.length} projects in Directus:`);
projects.slice(0, 5).forEach(p => {
const enTitle = p.translations?.find(t => t.languages_code === 'en-US')?.title;
console.log(` - ${p.slug}: "${enTitle || 'No title'}"`);
});
if (projects.length > 5) {
console.log(` ... and ${projects.length - 5} more`);
}
} catch (error) {
console.error('❌ Verification failed:', error.message);
}
}
async function main() {
console.log('\n╔════════════════════════════════════════╗');
console.log('║ Project Migration: PostgreSQL → Directus ║');
console.log('╚════════════════════════════════════════╝\n');
try {
// Test database connection first
console.log('🔍 Testing database connection...');
await prisma.$connect();
console.log('✅ Database connected\n');
await migrateProjects();
await verifyMigration();
} catch (error) {
if (error.message?.includes("Can't reach database")) {
console.error('\n❌ PostgreSQL ist nicht erreichbar!');
console.error('\n💡 Lösungen:');
console.error(' 1. Starte PostgreSQL: npm run dev');
console.error(' 2. Oder nutze Docker: docker-compose up -d postgres');
console.error(' 3. Oder skip diesen Schritt - Projects Collection existiert bereits in Directus\n');
console.error('Du kannst Projects später manuell in Directus erstellen oder die Migration erneut ausführen.\n');
process.exit(0); // Graceful exit
}
console.error('\n❌ Migration failed:', error);
process.exit(1);
} finally {
await prisma.$disconnect();
}
}
main();

View File

@@ -0,0 +1,240 @@
#!/usr/bin/env node
/**
* Directus Tech Stack Migration Script
*
* Migriert bestehende Tech Stack Daten aus messages/en.json und messages/de.json
* nach Directus Collections.
*
* Usage:
* npm install node-fetch@2 dotenv
* node scripts/migrate-tech-stack-to-directus.js
*/
const fetch = require('node-fetch');
const fs = require('fs');
const path = require('path');
require('dotenv').config();
const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev';
const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN;
if (!DIRECTUS_TOKEN) {
console.error('❌ Error: DIRECTUS_STATIC_TOKEN not found in .env');
process.exit(1);
}
// Lade aktuelle Tech Stack Daten aus messages files
const messagesEn = JSON.parse(
fs.readFileSync(path.join(__dirname, '../messages/en.json'), 'utf-8')
);
const messagesDe = JSON.parse(
fs.readFileSync(path.join(__dirname, '../messages/de.json'), 'utf-8')
);
const techStackEn = messagesEn.home.about.techStack;
const techStackDe = messagesDe.home.about.techStack;
// Tech Stack Struktur aus About.tsx
const TECH_STACK_DATA = [
{
key: 'frontend',
icon: 'Globe',
nameEn: techStackEn.categories.frontendMobile,
nameDe: techStackDe.categories.frontendMobile,
items: ['Next.js', 'Tailwind CSS', 'Flutter']
},
{
key: 'backend',
icon: 'Server',
nameEn: techStackEn.categories.backendDevops,
nameDe: techStackDe.categories.backendDevops,
items: ['Docker', 'PostgreSQL', 'Redis', 'Traefik']
},
{
key: 'tools',
icon: 'Wrench',
nameEn: techStackEn.categories.toolsAutomation,
nameDe: techStackDe.categories.toolsAutomation,
items: ['Git', 'CI/CD', 'n8n', techStackEn.items.selfHostedServices]
},
{
key: 'security',
icon: 'Shield',
nameEn: techStackEn.categories.securityAdmin,
nameDe: techStackDe.categories.securityAdmin,
items: ['CrowdSec', 'Suricata', 'Proxmox']
}
];
async function directusRequest(endpoint, method = 'GET', body = null) {
const url = `${DIRECTUS_URL}/${endpoint}`;
const options = {
method,
headers: {
'Authorization': `Bearer ${DIRECTUS_TOKEN}`,
'Content-Type': 'application/json'
}
};
if (body) {
options.body = JSON.stringify(body);
}
try {
const response = await fetch(url, options);
if (!response.ok) {
const text = await response.text();
throw new Error(`HTTP ${response.status}: ${text}`);
}
return await response.json();
} catch (error) {
console.error(`Error calling ${method} ${endpoint}:`, error.message);
throw error;
}
}
async function ensureLanguagesExist() {
console.log('\n🌍 Checking Languages...');
try {
const { data: languages } = await directusRequest('items/languages');
const hasEnUS = languages.some(l => l.code === 'en-US');
const hasDeDE = languages.some(l => l.code === 'de-DE');
if (!hasEnUS) {
console.log(' Creating en-US language...');
await directusRequest('items/languages', 'POST', {
code: 'en-US',
name: 'English (United States)'
});
}
if (!hasDeDE) {
console.log(' Creating de-DE language...');
await directusRequest('items/languages', 'POST', {
code: 'de-DE',
name: 'German (Germany)'
});
}
console.log(' ✅ Languages ready');
} catch (error) {
console.log(' ⚠️ Languages collection might not exist yet');
}
}
async function migrateTechStack() {
console.log('\n📦 Migrating Tech Stack to Directus...\n');
await ensureLanguagesExist();
for (const category of TECH_STACK_DATA) {
console.log(`\n📁 Category: ${category.key}`);
try {
// 1. Create Category
console.log(' Creating category...');
const categoryData = {
key: category.key,
icon: category.icon,
status: 'published',
sort: TECH_STACK_DATA.indexOf(category) + 1
};
const { data: createdCategory } = await directusRequest(
'items/tech_stack_categories',
'POST',
categoryData
);
console.log(` ✅ Category created with ID: ${createdCategory.id}`);
// 2. Create Translations
console.log(' Creating translations...');
// English Translation
await directusRequest(
'items/tech_stack_categories_translations',
'POST',
{
tech_stack_categories_id: createdCategory.id,
languages_code: 'en-US',
name: category.nameEn
}
);
// German Translation
await directusRequest(
'items/tech_stack_categories_translations',
'POST',
{
tech_stack_categories_id: createdCategory.id,
languages_code: 'de-DE',
name: category.nameDe
}
);
console.log(' ✅ Translations created (en-US, de-DE)');
// 3. Create Items
console.log(` Creating ${category.items.length} items...`);
for (let i = 0; i < category.items.length; i++) {
const itemName = category.items[i];
await directusRequest(
'items/tech_stack_items',
'POST',
{
category: createdCategory.id,
name: itemName,
sort: i + 1
}
);
console.log(`${itemName}`);
}
} catch (error) {
console.error(` ❌ Error migrating ${category.key}:`, error.message);
}
}
console.log('\n✨ Migration complete!\n');
}
async function verifyMigration() {
console.log('\n🔍 Verifying Migration...\n');
try {
const { data: categories } = await directusRequest(
'items/tech_stack_categories?fields=*,translations.*,items.*'
);
console.log(`✅ Found ${categories.length} categories:`);
categories.forEach(cat => {
const enTranslation = cat.translations?.find(t => t.languages_code === 'en-US');
const itemCount = cat.items?.length || 0;
console.log(` - ${cat.key}: "${enTranslation?.name}" (${itemCount} items)`);
});
console.log('\n🎉 All data migrated successfully!\n');
console.log('Next steps:');
console.log(' 1. Visit https://cms.dk0.dev/admin/content/tech_stack_categories');
console.log(' 2. Verify data looks correct');
console.log(' 3. Run: npm run dev:directus (to test GraphQL queries)');
console.log(' 4. Update About.tsx to use Directus data\n');
} catch (error) {
console.error('❌ Verification failed:', error.message);
}
}
// Main execution
(async () => {
try {
await migrateTechStack();
await verifyMigration();
} catch (error) {
console.error('\n❌ Migration failed:', error);
process.exit(1);
}
})();

View File

@@ -0,0 +1,197 @@
// --------------------------------------------------------
// DATEN AUS DEN VORHERIGEN NODES HOLEN
// --------------------------------------------------------
// 1. Spotify Node
let spotifyData = null;
try {
spotifyData = $('Spotify').first().json;
} catch (e) {}
// 2. Lanyard Node (Discord)
let lanyardData = null;
try {
lanyardData = $('Lanyard').first().json.data;
} catch (e) {}
// 3. Wakapi Summary (Tages-Statistik)
let wakapiStats = null;
try {
const wRaw = $('Wakapi').first().json;
// Manchmal ist es direkt im Root, manchmal unter data
wakapiStats = wRaw.grand_total ? wRaw : (wRaw.data ? wRaw.data : null);
} catch (e) {}
// 4. Wakapi Heartbeats (Live Check)
let heartbeatsList = [];
try {
const response = $('WakapiLast').last().json;
if (response.data && Array.isArray(response.data)) {
heartbeatsList = response.data;
}
} catch (e) {}
// 5. Hardcover Reading (Neu!)
let hardcoverData = null;
try {
// Falls du einen Node "Hardcover" hast
hardcoverData = $('Hardcover').first().json;
} catch (e) {}
// --------------------------------------------------------
// LOGIK & FORMATIERUNG
// --------------------------------------------------------
// --- A. SPOTIFY / MUSIC ---
let music = null;
if (spotifyData && spotifyData.item && spotifyData.is_playing) {
music = {
isPlaying: true,
track: spotifyData.item.name,
artist: spotifyData.item.artists.map(a => a.name).join(', '),
album: spotifyData.item.album.name,
albumArt: spotifyData.item.album.images[0]?.url,
url: spotifyData.item.external_urls.spotify
};
} else if (lanyardData?.listening_to_spotify && lanyardData.spotify) {
music = {
isPlaying: true,
track: lanyardData.spotify.song,
artist: lanyardData.spotify.artist.replace(/;/g, ", "),
album: lanyardData.spotify.album,
albumArt: lanyardData.spotify.album_art_url,
url: `https://open.spotify.com/track/${lanyardData.spotify.track_id}`
};
}
// --- B. GAMING & STATUS ---
let gaming = null;
let status = {
text: lanyardData?.discord_status || "offline",
color: 'gray'
};
// Farben mapping
if (status.text === 'online') status.color = 'green';
if (status.text === 'idle') status.color = 'yellow';
if (status.text === 'dnd') status.color = 'red';
if (lanyardData?.activities) {
lanyardData.activities.forEach(act => {
// Type 0 = Game (Spotify ignorieren)
if (act.type === 0 && act.name !== "Spotify") {
let image = null;
if (act.assets?.large_image) {
if (act.assets.large_image.startsWith("mp:external")) {
image = act.assets.large_image.replace(/mp:external\/([^\/]*)\/(https?)\/(^\/]*)\/(.*)/,"$2://$3/$4");
} else {
image = `https://cdn.discordapp.com/app-assets/${act.application_id}/${act.assets.large_image}.png`;
}
}
gaming = {
isPlaying: true,
name: act.name,
details: act.details,
state: act.state,
image: image
};
}
});
}
// --- C. CODING (Wakapi Logic) ---
let coding = null;
// 1. Basis-Stats von heute (Fallback)
if (wakapiStats && wakapiStats.grand_total) {
coding = {
isActive: false,
stats: {
time: wakapiStats.grand_total.text,
topLang: wakapiStats.languages?.[0]?.name || "Code",
topProject: wakapiStats.projects?.[0]?.name || "Project"
}
};
}
// 2. Live Check via Heartbeats
if (heartbeatsList.length > 0) {
const latestBeat = heartbeatsList[heartbeatsList.length - 1];
if (latestBeat && latestBeat.time) {
const beatTime = new Date(latestBeat.time * 1000).getTime();
const now = new Date().getTime();
const diffMinutes = (now - beatTime) / 1000 / 60;
// Wenn jünger als 15 Minuten -> AKTIV
if (diffMinutes < 15) {
if (!coding) coding = { stats: { time: "Just started" } };
coding.isActive = true;
coding.project = latestBeat.project || coding.stats?.topProject;
if (latestBeat.entity) {
const parts = latestBeat.entity.split(/[/\\]/);
coding.file = parts[parts.length - 1];
}
coding.language = latestBeat.language;
}
}
}
// --- D. CUSTOM ACTIVITIES (Komplett dynamisch!) ---
// Hier kannst du beliebige Activities hinzufügen ohne Website Code zu ändern
let customActivities = {};
// Beispiel: Reading Activity (Hardcover Integration)
if (hardcoverData && hardcoverData.user_book) {
const book = hardcoverData.user_book;
customActivities.reading = {
enabled: true,
title: book.book?.title,
author: book.book?.contributions?.[0]?.author?.name,
progress: book.progress_pages && book.book?.pages
? Math.round((book.progress_pages / book.book.pages) * 100)
: undefined,
coverUrl: book.book?.image_url
};
}
// Beispiel: Manuell gesetzt via separatem Webhook
// Du kannst einen Webhook erstellen der customActivities setzt:
// POST /webhook/set-custom-activity
// {
// "type": "working_out",
// "data": {
// "enabled": true,
// "activity": "Running",
// "duration_minutes": 45,
// "distance_km": 7.2,
// "calories": 350
// }
// }
// Dann hier einfach: customActivities.working_out = $('SetCustomActivity').first().json.data;
// WICHTIG: Du kannst auch mehrere Activities gleichzeitig haben!
// customActivities.learning = { enabled: true, course: "Docker", platform: "Udemy", progress: 67 };
// customActivities.streaming = { enabled: true, platform: "Twitch", viewers: 42 };
// etc.
// --------------------------------------------------------
// OUTPUT
// --------------------------------------------------------
return {
json: {
status,
music,
gaming,
coding,
customActivities, // NEU! Komplett dynamisch
timestamp: new Date().toISOString()
}
};

View File

@@ -0,0 +1,435 @@
#!/usr/bin/env node
/**
* Directus Schema Setup via REST API
*
* Erstellt automatisch alle benötigten Collections, Fields und Relations
* für Tech Stack in Directus via REST API.
*
* Usage:
* npm install node-fetch@2
* node scripts/setup-directus-collections.js
*/
const fetch = require('node-fetch');
require('dotenv').config();
const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev';
const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN;
if (!DIRECTUS_TOKEN) {
console.error('❌ Error: DIRECTUS_STATIC_TOKEN not found in .env');
process.exit(1);
}
console.log(`🔗 Connecting to: ${DIRECTUS_URL}`);
async function directusRequest(endpoint, method = 'GET', body = null) {
const url = `${DIRECTUS_URL}/${endpoint}`;
const options = {
method,
headers: {
'Authorization': `Bearer ${DIRECTUS_TOKEN}`,
'Content-Type': 'application/json'
}
};
if (body) {
options.body = JSON.stringify(body);
}
try {
const response = await fetch(url, options);
const text = await response.text();
if (!response.ok) {
// Ignore "already exists" errors
if (text.includes('already exists') || text.includes('RECORD_NOT_UNIQUE')) {
console.log(` ⚠️ Already exists, skipping...`);
return { data: null, alreadyExists: true };
}
throw new Error(`HTTP ${response.status}: ${text}`);
}
return text ? JSON.parse(text) : {};
} catch (error) {
console.error(`❌ Error calling ${method} ${endpoint}:`, error.message);
throw error;
}
}
async function ensureLanguages() {
console.log('\n🌍 Setting up Languages...');
try {
// Check if languages collection exists
const { data: existing } = await directusRequest('items/languages');
if (!existing) {
console.log(' Creating languages collection...');
await directusRequest('collections', 'POST', {
collection: 'languages',
meta: {
icon: 'translate',
translations: [
{ language: 'en-US', translation: 'Languages' }
]
},
schema: { name: 'languages' }
});
}
// Add en-US
await directusRequest('items/languages', 'POST', {
code: 'en-US',
name: 'English (United States)'
});
// Add de-DE
await directusRequest('items/languages', 'POST', {
code: 'de-DE',
name: 'German (Germany)'
});
console.log(' ✅ Languages ready (en-US, de-DE)');
} catch (error) {
console.log(' ⚠️ Languages might already exist');
}
}
async function createTechStackCollections() {
console.log('\n📦 Creating Tech Stack Collections...\n');
// 1. Create tech_stack_categories collection
console.log('1⃣ Creating tech_stack_categories...');
try {
await directusRequest('collections', 'POST', {
collection: 'tech_stack_categories',
meta: {
icon: 'layers',
display_template: '{{translations.name}}',
hidden: false,
singleton: false,
translations: [
{ language: 'en-US', translation: 'Tech Stack Categories' },
{ language: 'de-DE', translation: 'Tech Stack Kategorien' }
],
sort_field: 'sort'
},
schema: {
name: 'tech_stack_categories'
}
});
console.log(' ✅ Collection created');
} catch (error) {
console.log(' ⚠️ Collection might already exist');
}
// 2. Create tech_stack_categories_translations collection
console.log('\n2⃣ Creating tech_stack_categories_translations...');
try {
await directusRequest('collections', 'POST', {
collection: 'tech_stack_categories_translations',
meta: {
hidden: true,
icon: 'import_export'
},
schema: {
name: 'tech_stack_categories_translations'
}
});
console.log(' ✅ Collection created');
} catch (error) {
console.log(' ⚠️ Collection might already exist');
}
// 3. Create tech_stack_items collection
console.log('\n3⃣ Creating tech_stack_items...');
try {
await directusRequest('collections', 'POST', {
collection: 'tech_stack_items',
meta: {
icon: 'code',
display_template: '{{name}}',
hidden: false,
singleton: false,
sort_field: 'sort'
},
schema: {
name: 'tech_stack_items'
}
});
console.log(' ✅ Collection created');
} catch (error) {
console.log(' ⚠️ Collection might already exist');
}
}
async function createFields() {
console.log('\n🔧 Creating Fields...\n');
// Fields for tech_stack_categories
console.log('1⃣ Fields for tech_stack_categories:');
const categoryFields = [
{
field: 'status',
type: 'string',
meta: {
interface: 'select-dropdown',
options: {
choices: [
{ text: 'Published', value: 'published' },
{ text: 'Draft', value: 'draft' },
{ text: 'Archived', value: 'archived' }
]
}
},
schema: { default_value: 'draft', is_nullable: false }
},
{
field: 'sort',
type: 'integer',
meta: { interface: 'input', hidden: true },
schema: {}
},
{
field: 'key',
type: 'string',
meta: {
interface: 'input',
note: 'Unique identifier (e.g. frontend, backend)'
},
schema: { is_unique: true, is_nullable: false }
},
{
field: 'icon',
type: 'string',
meta: {
interface: 'select-dropdown',
options: {
choices: [
{ text: 'Globe', value: 'Globe' },
{ text: 'Server', value: 'Server' },
{ text: 'Wrench', value: 'Wrench' },
{ text: 'Shield', value: 'Shield' }
]
}
},
schema: { default_value: 'Code' }
},
{
field: 'date_created',
type: 'timestamp',
meta: { special: ['date-created'], interface: 'datetime', readonly: true, hidden: true },
schema: {}
},
{
field: 'date_updated',
type: 'timestamp',
meta: { special: ['date-updated'], interface: 'datetime', readonly: true, hidden: true },
schema: {}
},
{
field: 'translations',
type: 'alias',
meta: {
special: ['translations'],
interface: 'translations',
options: { languageField: 'languages_code' }
}
}
];
for (const field of categoryFields) {
try {
await directusRequest('fields/tech_stack_categories', 'POST', field);
console.log(`${field.field}`);
} catch (error) {
console.log(` ⚠️ ${field.field} (might already exist)`);
}
}
// Fields for tech_stack_categories_translations
console.log('\n2⃣ Fields for tech_stack_categories_translations:');
const translationFields = [
{
field: 'tech_stack_categories_id',
type: 'uuid',
meta: { hidden: true },
schema: {}
},
{
field: 'languages_code',
type: 'string',
meta: { interface: 'select-dropdown-m2o' },
schema: {}
},
{
field: 'name',
type: 'string',
meta: {
interface: 'input',
note: 'Translated category name'
},
schema: {}
}
];
for (const field of translationFields) {
try {
await directusRequest('fields/tech_stack_categories_translations', 'POST', field);
console.log(`${field.field}`);
} catch (error) {
console.log(` ⚠️ ${field.field} (might already exist)`);
}
}
// Fields for tech_stack_items
console.log('\n3⃣ Fields for tech_stack_items:');
const itemFields = [
{
field: 'sort',
type: 'integer',
meta: { interface: 'input', hidden: true },
schema: {}
},
{
field: 'category',
type: 'uuid',
meta: { interface: 'select-dropdown-m2o' },
schema: {}
},
{
field: 'name',
type: 'string',
meta: {
interface: 'input',
note: 'Technology name (e.g. Next.js, Docker)'
},
schema: { is_nullable: false }
},
{
field: 'url',
type: 'string',
meta: {
interface: 'input',
note: 'Official website (optional)'
},
schema: {}
},
{
field: 'icon_url',
type: 'string',
meta: {
interface: 'input',
note: 'Custom icon URL (optional)'
},
schema: {}
},
{
field: 'date_created',
type: 'timestamp',
meta: { special: ['date-created'], interface: 'datetime', readonly: true, hidden: true },
schema: {}
},
{
field: 'date_updated',
type: 'timestamp',
meta: { special: ['date-updated'], interface: 'datetime', readonly: true, hidden: true },
schema: {}
}
];
for (const field of itemFields) {
try {
await directusRequest('fields/tech_stack_items', 'POST', field);
console.log(`${field.field}`);
} catch (error) {
console.log(` ⚠️ ${field.field} (might already exist)`);
}
}
}
async function createRelations() {
console.log('\n🔗 Creating Relations...\n');
const relations = [
{
collection: 'tech_stack_categories_translations',
field: 'tech_stack_categories_id',
related_collection: 'tech_stack_categories',
meta: {
one_field: 'translations',
sort_field: null,
one_deselect_action: 'delete'
},
schema: { on_delete: 'CASCADE' }
},
{
collection: 'tech_stack_categories_translations',
field: 'languages_code',
related_collection: 'languages',
meta: {
one_field: null,
sort_field: null,
one_deselect_action: 'nullify'
},
schema: { on_delete: 'SET NULL' }
},
{
collection: 'tech_stack_items',
field: 'category',
related_collection: 'tech_stack_categories',
meta: {
one_field: 'items',
sort_field: 'sort',
one_deselect_action: 'nullify'
},
schema: { on_delete: 'SET NULL' }
}
];
for (let i = 0; i < relations.length; i++) {
try {
await directusRequest('relations', 'POST', relations[i]);
console.log(` ✅ Relation ${i + 1}/${relations.length}`);
} catch (error) {
console.log(` ⚠️ Relation ${i + 1}/${relations.length} (might already exist)`);
}
}
}
async function main() {
console.log('\n╔════════════════════════════════════════╗');
console.log('║ Directus Tech Stack Setup via API ║');
console.log('╚════════════════════════════════════════╝\n');
try {
await ensureLanguages();
await createTechStackCollections();
await createFields();
await createRelations();
console.log('\n╔════════════════════════════════════════╗');
console.log('║ ✅ Setup Complete! ║');
console.log('╚════════════════════════════════════════╝\n');
console.log('🎉 Tech Stack Collections sind bereit!\n');
console.log('Nächste Schritte:');
console.log(' 1. Besuche: https://cms.dk0.dev/admin/content/tech_stack_categories');
console.log(' 2. Führe aus: node scripts/migrate-tech-stack-to-directus.js');
console.log(' 3. Verifiziere die Daten im Directus Admin Panel\n');
} catch (error) {
console.error('\n❌ Setup failed:', error);
console.error('\nTroubleshooting:');
console.error(' - Überprüfe DIRECTUS_URL und DIRECTUS_STATIC_TOKEN in .env');
console.error(' - Stelle sicher, dass der Token Admin-Rechte hat');
console.error(' - Prüfe ob Directus erreichbar ist: curl ' + DIRECTUS_URL);
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,285 @@
#!/usr/bin/env node
/**
* Directus Hobbies Collection Setup via REST API
*
* Erstellt die Hobbies Collection mit Translations
*
* Usage:
* node scripts/setup-directus-hobbies.js
*/
const fetch = require('node-fetch');
require('dotenv').config();
const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev';
const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN;
if (!DIRECTUS_TOKEN) {
console.error('❌ Error: DIRECTUS_STATIC_TOKEN not found in .env');
process.exit(1);
}
async function directusRequest(endpoint, method = 'GET', body = null) {
const url = `${DIRECTUS_URL}/${endpoint}`;
const options = {
method,
headers: {
'Authorization': `Bearer ${DIRECTUS_TOKEN}`,
'Content-Type': 'application/json'
}
};
if (body) {
options.body = JSON.stringify(body);
}
try {
const response = await fetch(url, options);
const text = await response.text();
if (!response.ok) {
if (text.includes('already exists') || text.includes('RECORD_NOT_UNIQUE')) {
console.log(` ⚠️ Already exists, skipping...`);
return { data: null, alreadyExists: true };
}
throw new Error(`HTTP ${response.status}: ${text}`);
}
return text ? JSON.parse(text) : {};
} catch (error) {
console.error(`❌ Error calling ${method} ${endpoint}:`, error.message);
throw error;
}
}
async function createHobbiesCollections() {
console.log('\n📦 Creating Hobbies Collections...\n');
console.log('1⃣ Creating hobbies...');
try {
await directusRequest('collections', 'POST', {
collection: 'hobbies',
meta: {
icon: 'sports_esports',
display_template: '{{translations.title}}',
hidden: false,
singleton: false,
sort_field: 'sort'
},
schema: {
name: 'hobbies'
}
});
console.log(' ✅ Collection created');
} catch (error) {
console.log(' ⚠️ Collection might already exist');
}
console.log('\n2⃣ Creating hobbies_translations...');
try {
await directusRequest('collections', 'POST', {
collection: 'hobbies_translations',
meta: {
hidden: true,
icon: 'import_export'
},
schema: {
name: 'hobbies_translations'
}
});
console.log(' ✅ Collection created');
} catch (error) {
console.log(' ⚠️ Collection might already exist');
}
}
async function createHobbyFields() {
console.log('\n🔧 Creating Fields...\n');
const hobbyFields = [
{
field: 'status',
type: 'string',
meta: {
interface: 'select-dropdown',
options: {
choices: [
{ text: 'Published', value: 'published' },
{ text: 'Draft', value: 'draft' }
]
}
},
schema: { default_value: 'draft', is_nullable: false }
},
{
field: 'sort',
type: 'integer',
meta: { interface: 'input', hidden: true },
schema: {}
},
{
field: 'key',
type: 'string',
meta: {
interface: 'input',
note: 'Unique identifier (e.g. self_hosting, gaming)'
},
schema: { is_unique: true, is_nullable: false }
},
{
field: 'icon',
type: 'string',
meta: {
interface: 'select-dropdown',
options: {
choices: [
{ text: 'Code', value: 'Code' },
{ text: 'Gamepad2', value: 'Gamepad2' },
{ text: 'Server', value: 'Server' },
{ text: 'Activity', value: 'Activity' }
]
}
},
schema: { default_value: 'Code' }
},
{
field: 'date_created',
type: 'timestamp',
meta: { special: ['date-created'], interface: 'datetime', readonly: true, hidden: true },
schema: {}
},
{
field: 'date_updated',
type: 'timestamp',
meta: { special: ['date-updated'], interface: 'datetime', readonly: true, hidden: true },
schema: {}
},
{
field: 'translations',
type: 'alias',
meta: {
special: ['translations'],
interface: 'translations',
options: { languageField: 'languages_code' }
}
}
];
console.log('Adding fields to hobbies:');
for (const field of hobbyFields) {
try {
await directusRequest('fields/hobbies', 'POST', field);
console.log(`${field.field}`);
} catch (error) {
console.log(` ⚠️ ${field.field} (might already exist)`);
}
}
const translationFields = [
{
field: 'hobbies_id',
type: 'uuid',
meta: { hidden: true },
schema: {}
},
{
field: 'languages_code',
type: 'string',
meta: { interface: 'select-dropdown-m2o' },
schema: {}
},
{
field: 'title',
type: 'string',
meta: {
interface: 'input',
note: 'Hobby title'
},
schema: {}
},
{
field: 'description',
type: 'text',
meta: {
interface: 'input-multiline',
note: 'Hobby description (optional)'
},
schema: {}
}
];
console.log('\nAdding fields to hobbies_translations:');
for (const field of translationFields) {
try {
await directusRequest('fields/hobbies_translations', 'POST', field);
console.log(`${field.field}`);
} catch (error) {
console.log(` ⚠️ ${field.field} (might already exist)`);
}
}
}
async function createHobbyRelations() {
console.log('\n🔗 Creating Relations...\n');
const relations = [
{
collection: 'hobbies_translations',
field: 'hobbies_id',
related_collection: 'hobbies',
meta: {
one_field: 'translations',
sort_field: null,
one_deselect_action: 'delete'
},
schema: { on_delete: 'CASCADE' }
},
{
collection: 'hobbies_translations',
field: 'languages_code',
related_collection: 'languages',
meta: {
one_field: null,
sort_field: null,
one_deselect_action: 'nullify'
},
schema: { on_delete: 'SET NULL' }
}
];
for (let i = 0; i < relations.length; i++) {
try {
await directusRequest('relations', 'POST', relations[i]);
console.log(` ✅ Relation ${i + 1}/${relations.length}`);
} catch (error) {
console.log(` ⚠️ Relation ${i + 1}/${relations.length} (might already exist)`);
}
}
}
async function main() {
console.log('\n╔════════════════════════════════════════╗');
console.log('║ Directus Hobbies Setup via API ║');
console.log('╚════════════════════════════════════════╝\n');
try {
await createHobbiesCollections();
await createHobbyFields();
await createHobbyRelations();
console.log('\n╔════════════════════════════════════════╗');
console.log('║ ✅ Setup Complete! ║');
console.log('╚════════════════════════════════════════╝\n');
console.log('🎉 Hobbies Collection ist bereit!\n');
console.log('Nächste Schritte:');
console.log(' 1. Führe aus: node scripts/migrate-hobbies-to-directus.js');
console.log(' 2. Verifiziere: https://cms.dk0.dev/admin/content/hobbies\n');
} catch (error) {
console.error('\n❌ Setup failed:', error);
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,503 @@
#!/usr/bin/env node
/**
* Directus Projects Collection Setup via REST API
*
* Erstellt die komplette Projects Collection mit allen Feldern und Translations
*
* Usage:
* node scripts/setup-directus-projects.js
*/
const fetch = require('node-fetch');
require('dotenv').config();
const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev';
const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN;
if (!DIRECTUS_TOKEN) {
console.error('❌ Error: DIRECTUS_STATIC_TOKEN not found in .env');
process.exit(1);
}
console.log(`🔗 Connecting to: ${DIRECTUS_URL}`);
async function directusRequest(endpoint, method = 'GET', body = null) {
const url = `${DIRECTUS_URL}/${endpoint}`;
const options = {
method,
headers: {
'Authorization': `Bearer ${DIRECTUS_TOKEN}`,
'Content-Type': 'application/json'
}
};
if (body) {
options.body = JSON.stringify(body);
}
try {
const response = await fetch(url, options);
const text = await response.text();
if (!response.ok) {
if (text.includes('already exists') || text.includes('RECORD_NOT_UNIQUE')) {
console.log(` ⚠️ Already exists, skipping...`);
return { data: null, alreadyExists: true };
}
throw new Error(`HTTP ${response.status}: ${text}`);
}
return text ? JSON.parse(text) : {};
} catch (error) {
console.error(`❌ Error calling ${method} ${endpoint}:`, error.message);
throw error;
}
}
async function createProjectsCollections() {
console.log('\n📦 Creating Projects Collections...\n');
// 1. Create projects collection
console.log('1⃣ Creating projects...');
try {
await directusRequest('collections', 'POST', {
collection: 'projects',
meta: {
icon: 'folder',
display_template: '{{title}}',
hidden: false,
singleton: false,
translations: [
{ language: 'en-US', translation: 'Projects' },
{ language: 'de-DE', translation: 'Projekte' }
],
sort_field: 'sort'
},
schema: {
name: 'projects'
}
});
console.log(' ✅ Collection created');
} catch (error) {
console.log(' ⚠️ Collection might already exist');
}
// 2. Create projects_translations collection
console.log('\n2⃣ Creating projects_translations...');
try {
await directusRequest('collections', 'POST', {
collection: 'projects_translations',
meta: {
hidden: true,
icon: 'import_export'
},
schema: {
name: 'projects_translations'
}
});
console.log(' ✅ Collection created');
} catch (error) {
console.log(' ⚠️ Collection might already exist');
}
}
async function createProjectFields() {
console.log('\n🔧 Creating Project Fields...\n');
const projectFields = [
{
field: 'status',
type: 'string',
meta: {
interface: 'select-dropdown',
options: {
choices: [
{ text: 'Published', value: 'published' },
{ text: 'Draft', value: 'draft' },
{ text: 'Archived', value: 'archived' }
]
}
},
schema: { default_value: 'draft', is_nullable: false }
},
{
field: 'sort',
type: 'integer',
meta: { interface: 'input', hidden: true },
schema: {}
},
{
field: 'slug',
type: 'string',
meta: {
interface: 'input',
note: 'URL-friendly identifier (e.g. my-portfolio-website)',
required: true
},
schema: { is_unique: true, is_nullable: false }
},
{
field: 'featured',
type: 'boolean',
meta: {
interface: 'boolean',
note: 'Show on homepage'
},
schema: { default_value: false }
},
{
field: 'category',
type: 'string',
meta: {
interface: 'select-dropdown',
options: {
choices: [
{ text: 'Web Application', value: 'Web Application' },
{ text: 'Mobile App', value: 'Mobile App' },
{ text: 'Backend Development', value: 'Backend Development' },
{ text: 'DevOps', value: 'DevOps' },
{ text: 'AI/ML', value: 'AI/ML' },
{ text: 'Other', value: 'Other' }
]
}
},
schema: { default_value: 'Web Application' }
},
{
field: 'difficulty',
type: 'string',
meta: {
interface: 'select-dropdown',
options: {
choices: [
{ text: 'Beginner', value: 'BEGINNER' },
{ text: 'Intermediate', value: 'INTERMEDIATE' },
{ text: 'Advanced', value: 'ADVANCED' },
{ text: 'Expert', value: 'EXPERT' }
]
}
},
schema: { default_value: 'INTERMEDIATE' }
},
{
field: 'date',
type: 'string',
meta: {
interface: 'input',
note: 'Project date (e.g. "2024" or "2023-2024")'
},
schema: {}
},
{
field: 'time_to_complete',
type: 'string',
meta: {
interface: 'input',
note: 'e.g. "4-6 weeks"',
placeholder: '4-6 weeks'
},
schema: {}
},
{
field: 'github',
type: 'string',
meta: {
interface: 'input',
note: 'GitHub repository URL',
placeholder: 'https://github.com/...'
},
schema: {}
},
{
field: 'live',
type: 'string',
meta: {
interface: 'input',
note: 'Live demo URL',
placeholder: 'https://...'
},
schema: {}
},
{
field: 'image_url',
type: 'string',
meta: {
interface: 'input',
note: 'Main project image URL'
},
schema: {}
},
{
field: 'demo_video',
type: 'string',
meta: {
interface: 'input',
note: 'Demo video URL (YouTube, Vimeo, etc.)'
},
schema: {}
},
{
field: 'color_scheme',
type: 'string',
meta: {
interface: 'input',
note: 'e.g. "Dark theme with blue accents"'
},
schema: { default_value: 'Dark' }
},
{
field: 'accessibility',
type: 'boolean',
meta: {
interface: 'boolean',
note: 'Is the project accessible?'
},
schema: { default_value: true }
},
{
field: 'tags',
type: 'json',
meta: {
interface: 'tags',
note: 'Technology tags (e.g. React, Node.js, Docker)'
},
schema: {}
},
{
field: 'technologies',
type: 'json',
meta: {
interface: 'tags',
note: 'Detailed tech stack'
},
schema: {}
},
{
field: 'challenges',
type: 'json',
meta: {
interface: 'list',
note: 'Challenges faced during development'
},
schema: {}
},
{
field: 'lessons_learned',
type: 'json',
meta: {
interface: 'list',
note: 'What you learned from this project'
},
schema: {}
},
{
field: 'future_improvements',
type: 'json',
meta: {
interface: 'list',
note: 'Planned improvements'
},
schema: {}
},
{
field: 'screenshots',
type: 'json',
meta: {
interface: 'list',
note: 'Array of screenshot URLs'
},
schema: {}
},
{
field: 'performance',
type: 'json',
meta: {
interface: 'input-code',
options: {
language: 'json'
},
note: 'Performance metrics (lighthouse, bundle size, load time)'
},
schema: {}
},
{
field: 'date_created',
type: 'timestamp',
meta: {
special: ['date-created'],
interface: 'datetime',
readonly: true,
hidden: true
},
schema: {}
},
{
field: 'date_updated',
type: 'timestamp',
meta: {
special: ['date-updated'],
interface: 'datetime',
readonly: true,
hidden: true
},
schema: {}
},
{
field: 'translations',
type: 'alias',
meta: {
special: ['translations'],
interface: 'translations',
options: { languageField: 'languages_code' }
}
}
];
console.log('Adding fields to projects:');
for (const field of projectFields) {
try {
await directusRequest('fields/projects', 'POST', field);
console.log(`${field.field}`);
} catch (error) {
console.log(` ⚠️ ${field.field} (might already exist)`);
}
}
// Translation fields
console.log('\nAdding fields to projects_translations:');
const translationFields = [
{
field: 'projects_id',
type: 'uuid',
meta: { hidden: true },
schema: {}
},
{
field: 'languages_code',
type: 'string',
meta: { interface: 'select-dropdown-m2o' },
schema: {}
},
{
field: 'title',
type: 'string',
meta: {
interface: 'input',
note: 'Project title',
required: true
},
schema: { is_nullable: false }
},
{
field: 'description',
type: 'text',
meta: {
interface: 'input-multiline',
note: 'Short description (1-2 sentences)'
},
schema: {}
},
{
field: 'content',
type: 'text',
meta: {
interface: 'input-rich-text-md',
note: 'Full project content (Markdown)'
},
schema: {}
},
{
field: 'meta_description',
type: 'string',
meta: {
interface: 'input',
note: 'SEO meta description'
},
schema: {}
},
{
field: 'keywords',
type: 'string',
meta: {
interface: 'input',
note: 'SEO keywords (comma separated)'
},
schema: {}
}
];
for (const field of translationFields) {
try {
await directusRequest('fields/projects_translations', 'POST', field);
console.log(`${field.field}`);
} catch (error) {
console.log(` ⚠️ ${field.field} (might already exist)`);
}
}
}
async function createProjectRelations() {
console.log('\n🔗 Creating Relations...\n');
const relations = [
{
collection: 'projects_translations',
field: 'projects_id',
related_collection: 'projects',
meta: {
one_field: 'translations',
sort_field: null,
one_deselect_action: 'delete'
},
schema: { on_delete: 'CASCADE' }
},
{
collection: 'projects_translations',
field: 'languages_code',
related_collection: 'languages',
meta: {
one_field: null,
sort_field: null,
one_deselect_action: 'nullify'
},
schema: { on_delete: 'SET NULL' }
}
];
for (let i = 0; i < relations.length; i++) {
try {
await directusRequest('relations', 'POST', relations[i]);
console.log(` ✅ Relation ${i + 1}/${relations.length}`);
} catch (error) {
console.log(` ⚠️ Relation ${i + 1}/${relations.length} (might already exist)`);
}
}
}
async function main() {
console.log('\n╔════════════════════════════════════════╗');
console.log('║ Directus Projects Setup via API ║');
console.log('╚════════════════════════════════════════╝\n');
try {
await createProjectsCollections();
await createProjectFields();
await createProjectRelations();
console.log('\n╔════════════════════════════════════════╗');
console.log('║ ✅ Setup Complete! ║');
console.log('╚════════════════════════════════════════╝\n');
console.log('🎉 Projects Collection ist bereit!\n');
console.log('Nächste Schritte:');
console.log(' 1. Besuche: https://cms.dk0.dev/admin/content/projects');
console.log(' 2. Führe aus: node scripts/migrate-projects-to-directus.js');
console.log(' 3. Verifiziere die Daten im Directus Admin Panel\n');
} catch (error) {
console.error('\n❌ Setup failed:', error);
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,151 @@
#!/usr/bin/env node
/**
* Setup tech stack items in Directus
* Creates tech_stack_items collection and populates it with data
*/
const https = require('https');
const DIRECTUS_URL = 'https://cms.dk0.dev';
const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN || '';
if (!DIRECTUS_TOKEN) {
console.error('❌ DIRECTUS_STATIC_TOKEN not set');
process.exit(1);
}
// Tech stack items to create
const techStackItems = [
// Frontend & Mobile (category 1)
{ category: '1', name: 'Next.js', sort: 1 },
{ category: '1', name: 'Tailwind CSS', sort: 2 },
{ category: '1', name: 'Flutter', sort: 3 },
// Backend & DevOps (category 2)
{ category: '2', name: 'Docker Swarm', sort: 1 },
{ category: '2', name: 'Traefik', sort: 2 },
{ category: '2', name: 'Nginx Proxy Manager', sort: 3 },
{ category: '2', name: 'Redis', sort: 4 },
// Tools & Automation (category 3)
{ category: '3', name: 'Git', sort: 1 },
{ category: '3', name: 'CI/CD', sort: 2 },
{ category: '3', name: 'n8n', sort: 3 },
{ category: '3', name: 'Self-hosted Services', sort: 4 },
// Security & Admin (category 4)
{ category: '4', name: 'CrowdSec', sort: 1 },
{ category: '4', name: 'Suricata', sort: 2 },
{ category: '4', name: 'Mailcow', sort: 3 },
];
async function makeRequest(method, endpoint, body = null) {
return new Promise((resolve, reject) => {
const url = new URL(endpoint, DIRECTUS_URL);
const options = {
hostname: url.hostname,
port: 443,
path: url.pathname + url.search,
method: method,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${DIRECTUS_TOKEN}`,
},
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
const parsed = JSON.parse(data);
if (res.statusCode >= 400) {
reject(new Error(`HTTP ${res.statusCode}: ${data}`));
} else {
resolve(parsed);
}
} catch (e) {
resolve(data);
}
});
});
req.on('error', reject);
if (body) req.write(JSON.stringify(body));
req.end();
});
}
async function checkCollectionExists() {
try {
const response = await makeRequest('GET', '/api/items/tech_stack_items?limit=1');
if (response.data !== undefined) {
console.log('✅ Collection tech_stack_items already exists');
return true;
}
} catch (e) {
if (e.message.includes('does not exist') || e.message.includes('ROUTE_NOT_FOUND')) {
console.log(' Collection tech_stack_items does not exist yet');
return false;
}
throw e;
}
return false;
}
async function addTechStackItems() {
console.log(`📝 Adding ${techStackItems.length} tech stack items...`);
let created = 0;
for (const item of techStackItems) {
try {
const response = await makeRequest('POST', '/api/items/tech_stack_items', {
category: item.category,
name: item.name,
sort: item.sort,
status: 'published'
});
if (response.data) {
created++;
console.log(` ✅ Created: ${item.name} (category ${item.category})`);
}
} catch (error) {
console.error(` ❌ Failed to create "${item.name}":`, error.message);
}
}
console.log(`\n✅ Successfully created ${created}/${techStackItems.length} items`);
return created === techStackItems.length;
}
async function main() {
try {
console.log('🚀 Setting up Tech Stack in Directus...\n');
const exists = await checkCollectionExists();
if (exists) {
// Count existing items
const response = await makeRequest('GET', '/api/items/tech_stack_items?limit=1000');
const count = response.data?.length || 0;
if (count > 0) {
console.log(`✅ Tech stack already populated with ${count} items`);
return;
}
}
// Add items
await addTechStackItems();
console.log('\n✅ Tech stack setup complete!');
} catch (error) {
console.error('❌ Error setting up tech stack:', error.message);
process.exit(1);
}
}
main();