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:
184
scripts/README.md
Normal file
184
scripts/README.md
Normal 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
|
||||
106
scripts/add-de-project-translations.js
Normal file
106
scripts/add-de-project-translations.js
Normal 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);
|
||||
});
|
||||
123
scripts/migrate-content-pages-to-directus.js
Normal file
123
scripts/migrate-content-pages-to-directus.js
Normal 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();
|
||||
185
scripts/migrate-hobbies-to-directus.js
Normal file
185
scripts/migrate-hobbies-to-directus.js
Normal 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();
|
||||
225
scripts/migrate-projects-to-directus.js
Normal file
225
scripts/migrate-projects-to-directus.js
Normal 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();
|
||||
240
scripts/migrate-tech-stack-to-directus.js
Normal file
240
scripts/migrate-tech-stack-to-directus.js
Normal 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);
|
||||
}
|
||||
})();
|
||||
197
scripts/n8n-workflow-code-updated.js
Normal file
197
scripts/n8n-workflow-code-updated.js
Normal 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()
|
||||
}
|
||||
};
|
||||
435
scripts/setup-directus-collections.js
Normal file
435
scripts/setup-directus-collections.js
Normal 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();
|
||||
285
scripts/setup-directus-hobbies.js
Normal file
285
scripts/setup-directus-hobbies.js
Normal 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();
|
||||
503
scripts/setup-directus-projects.js
Normal file
503
scripts/setup-directus-projects.js
Normal 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();
|
||||
151
scripts/setup-tech-stack-directus.js
Normal file
151
scripts/setup-tech-stack-directus.js
Normal 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();
|
||||
Reference in New Issue
Block a user