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:
412
lib/directus.ts
412
lib/directus.ts
@@ -5,6 +5,13 @@
|
||||
const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev';
|
||||
const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN || '';
|
||||
|
||||
// Debug: Log if token is set
|
||||
if (process.env.NODE_ENV === 'development' && typeof process !== 'undefined' && process.env.DIRECTUS_STATIC_TOKEN) {
|
||||
console.log('✓ Directus token loaded:', DIRECTUS_TOKEN.substring(0, 5) + '...');
|
||||
} else if (process.env.NODE_ENV === 'development') {
|
||||
console.log('⚠ Directus token NOT loaded from .env');
|
||||
}
|
||||
|
||||
// Mapping: next-intl locale → Directus language code
|
||||
const localeToDirectus: Record<string, string> = {
|
||||
en: 'en-US',
|
||||
@@ -46,11 +53,13 @@ async function directusRequest<T>(
|
||||
if (!response.ok) {
|
||||
// Collection noch nicht erstellt? Stille fallback zu JSON
|
||||
const text = await response.text();
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error(`Directus error: ${response.status}`, text.substring(0, 200));
|
||||
}
|
||||
if (text.includes('GRAPHQL_VALIDATION') || text.includes('Cannot query field')) {
|
||||
// Stille: Collection existiert noch nicht
|
||||
return null;
|
||||
}
|
||||
console.error(`Directus error: ${response.status}`, text);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -58,6 +67,9 @@ async function directusRequest<T>(
|
||||
|
||||
// Prüfe auf GraphQL errors
|
||||
if (data?.errors) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('Directus GraphQL errors:', JSON.stringify(data.errors).substring(0, 200));
|
||||
}
|
||||
// Stille: Collection noch nicht ready
|
||||
return null;
|
||||
}
|
||||
@@ -66,11 +78,14 @@ async function directusRequest<T>(
|
||||
} catch (error: any) {
|
||||
// Timeout oder Network Error - stille fallback
|
||||
if (error?.name === 'TimeoutError' || error?.name === 'AbortError') {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('Directus timeout');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// Andere Errors nur in dev loggen
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('Directus request failed:', error);
|
||||
console.error('Directus request failed:', error?.message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -126,7 +141,10 @@ export async function getContentPage(
|
||||
const directusLocale = toDirectusLocale(locale);
|
||||
const query = `
|
||||
query {
|
||||
content_pages(filter: {slug: {_eq: "${slug}"}, locale: {_eq: "${directusLocale}"}}, limit: 1) {
|
||||
content_pages(
|
||||
filter: { slug: { _starts_with: "${slug}" } }
|
||||
limit: 25
|
||||
) {
|
||||
id
|
||||
slug
|
||||
locale
|
||||
@@ -142,10 +160,394 @@ export async function getContentPage(
|
||||
{ body: { query } }
|
||||
);
|
||||
|
||||
const pages = (result as any)?.content_pages;
|
||||
return pages?.[0] || null;
|
||||
const pages = (result as any)?.content_pages || [];
|
||||
if (pages.length === 0) return null;
|
||||
|
||||
// Prefer exact locale, otherwise fall back to first available
|
||||
const exact = pages.find((p: any) => p.locale === directusLocale);
|
||||
return exact || pages[0];
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch content page ${slug} (${locale}):`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Tech Stack Types
|
||||
export interface TechStackItem {
|
||||
id: string;
|
||||
name: string;
|
||||
url?: string;
|
||||
icon_url?: string;
|
||||
sort: number;
|
||||
}
|
||||
|
||||
export interface TechStackCategory {
|
||||
id: string;
|
||||
key: string;
|
||||
icon: string;
|
||||
sort: number;
|
||||
name: string; // Translated name
|
||||
items: TechStackItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Tech Stack from Directus with translations
|
||||
*/
|
||||
// Fallback tech stack data (used when Directus items aren't available)
|
||||
const fallbackTechStackData: Record<string, Array<{ key: string; items: string[] }>> = {
|
||||
'en-US': [
|
||||
{ key: 'frontend', items: ['Next.js', 'Tailwind CSS', 'Flutter'] },
|
||||
{ key: 'backend', items: ['Docker Swarm', 'Traefik', 'Nginx Proxy Manager', 'Redis'] },
|
||||
{ key: 'tools', items: ['Git', 'CI/CD', 'n8n', 'Self-hosted Services'] },
|
||||
{ key: 'security', items: ['CrowdSec', 'Suricata', 'Mailcow'] }
|
||||
],
|
||||
'de-DE': [
|
||||
{ key: 'frontend', items: ['Next.js', 'Tailwind CSS', 'Flutter'] },
|
||||
{ key: 'backend', items: ['Docker Swarm', 'Traefik', 'Nginx Proxy Manager', 'Redis'] },
|
||||
{ key: 'tools', items: ['Git', 'CI/CD', 'n8n', 'Self-Hosted-Services'] },
|
||||
{ key: 'security', items: ['CrowdSec', 'Suricata', 'Mailcow'] }
|
||||
]
|
||||
};
|
||||
|
||||
const categoryIconMap: Record<string, string> = {
|
||||
frontend: 'Globe',
|
||||
backend: 'Server',
|
||||
tools: 'Wrench',
|
||||
security: 'Shield'
|
||||
};
|
||||
|
||||
const categoryNames: Record<string, Record<string, string>> = {
|
||||
'en-US': {
|
||||
frontend: 'Frontend & Mobile',
|
||||
backend: 'Backend & DevOps',
|
||||
tools: 'Tools & Automation',
|
||||
security: 'Security & Admin'
|
||||
},
|
||||
'de-DE': {
|
||||
frontend: 'Frontend & Mobile',
|
||||
backend: 'Backend & DevOps',
|
||||
tools: 'Tools & Automation',
|
||||
security: 'Security & Admin'
|
||||
}
|
||||
};
|
||||
|
||||
export async function getTechStack(locale: string): Promise<TechStackCategory[] | null> {
|
||||
const directusLocale = toDirectusLocale(locale);
|
||||
|
||||
try {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[getTechStack] Fetching with locale:', directusLocale);
|
||||
}
|
||||
|
||||
// Fetch categories via GraphQL with translations
|
||||
const categoriesQuery = `
|
||||
query {
|
||||
tech_stack_categories(
|
||||
filter: { status: { _eq: "published" } }
|
||||
sort: "sort"
|
||||
) {
|
||||
id
|
||||
key
|
||||
icon
|
||||
sort
|
||||
translations(filter: { languages_code: { code: { _eq: "${directusLocale}" } } }) {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const categoriesResult = await directusRequest(
|
||||
'',
|
||||
{ body: { query: categoriesQuery } }
|
||||
);
|
||||
|
||||
const categories = (categoriesResult as any)?.tech_stack_categories;
|
||||
|
||||
if (!categories || categories.length === 0) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[getTechStack] No categories found, using fallback');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[getTechStack] Found categories:', categories.length);
|
||||
}
|
||||
|
||||
// Fetch items via REST API (since GraphQL category relationship returns null)
|
||||
const itemsResponse = await fetch(
|
||||
`${DIRECTUS_URL}/items/tech_stack_items?fields=id,name,category,url,icon_url,sort&sort=sort&limit=100`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${DIRECTUS_TOKEN}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const itemsData = await itemsResponse.json();
|
||||
const allItems = itemsData?.data || [];
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[getTechStack] Fetched items:', allItems.length);
|
||||
}
|
||||
|
||||
// Group items by category
|
||||
const categoriesWithItems = categories.map((cat: any) => {
|
||||
const categoryItems = allItems.filter((item: any) =>
|
||||
item.category === cat.id || item.category === parseInt(cat.id)
|
||||
);
|
||||
|
||||
// Fallback: if no items linked by category, use fallback data
|
||||
let itemsToUse = categoryItems;
|
||||
if (itemsToUse.length === 0) {
|
||||
const fallbackData = fallbackTechStackData[directusLocale];
|
||||
const categoryFallback = fallbackData?.find(f => f.key === cat.key);
|
||||
if (categoryFallback) {
|
||||
itemsToUse = categoryFallback.items.map((name, idx) => ({
|
||||
id: `fallback-${cat.key}-${idx}`,
|
||||
name: name,
|
||||
url: undefined,
|
||||
icon_url: undefined,
|
||||
sort: idx + 1
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: cat.id,
|
||||
key: cat.key,
|
||||
icon: cat.icon,
|
||||
sort: cat.sort,
|
||||
name: cat.translations?.[0]?.name || categoryNames[directusLocale]?.[cat.key] || cat.key,
|
||||
items: itemsToUse.map((item: any) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
url: item.url,
|
||||
icon_url: item.icon_url,
|
||||
sort: item.sort
|
||||
}))
|
||||
};
|
||||
});
|
||||
|
||||
return categoriesWithItems;
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch tech stack (${locale}):`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Hobbies Types
|
||||
export interface Hobby {
|
||||
id: string;
|
||||
key: string;
|
||||
icon: string;
|
||||
title: string; // Translated title
|
||||
description?: string; // Translated description
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Hobbies from Directus with translations
|
||||
*/
|
||||
export async function getHobbies(locale: string): Promise<Hobby[] | null> {
|
||||
const directusLocale = toDirectusLocale(locale);
|
||||
|
||||
const query = `
|
||||
query {
|
||||
hobbies(
|
||||
filter: { status: { _eq: "published" } }
|
||||
sort: "sort"
|
||||
) {
|
||||
id
|
||||
key
|
||||
icon
|
||||
translations(filter: { languages_code: { code: { _eq: "${directusLocale}" } } }) {
|
||||
title
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await directusRequest(
|
||||
'',
|
||||
{ body: { query } }
|
||||
);
|
||||
|
||||
const hobbies = (result as any)?.hobbies;
|
||||
if (!hobbies || hobbies.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return hobbies.map((hobby: any) => ({
|
||||
id: hobby.id,
|
||||
key: hobby.key,
|
||||
icon: hobby.icon,
|
||||
title: hobby.translations?.[0]?.title || hobby.key,
|
||||
description: hobby.translations?.[0]?.description
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch hobbies (${locale}):`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Projects Types
|
||||
export interface Project {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
content?: string;
|
||||
category?: string;
|
||||
difficulty?: string;
|
||||
tags: string[];
|
||||
technologies: string[];
|
||||
challenges?: string;
|
||||
lessons_learned?: string;
|
||||
future_improvements?: string;
|
||||
github_url?: string;
|
||||
live_url?: string;
|
||||
image_url?: string;
|
||||
demo_video_url?: string;
|
||||
performance_metrics?: string;
|
||||
screenshots?: string[];
|
||||
featured: boolean;
|
||||
published: boolean;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Projects from Directus with translations
|
||||
*
|
||||
* @param locale - Language code (en or de)
|
||||
* @param options - Filter options
|
||||
* @returns Array of projects or null
|
||||
*/
|
||||
export async function getProjects(
|
||||
locale: string,
|
||||
options?: {
|
||||
featured?: boolean;
|
||||
published?: boolean;
|
||||
category?: string;
|
||||
difficulty?: string;
|
||||
search?: string;
|
||||
limit?: number;
|
||||
}
|
||||
): Promise<Project[] | null> {
|
||||
const directusLocale = toDirectusLocale(locale);
|
||||
|
||||
// Build filters
|
||||
const filters = ['status: { _eq: "published" }'];
|
||||
|
||||
if (options?.featured !== undefined) {
|
||||
filters.push(`featured: { _eq: ${options.featured} }`);
|
||||
}
|
||||
|
||||
if (options?.published !== undefined) {
|
||||
filters.push(`published: { _eq: ${options.published} }`);
|
||||
}
|
||||
|
||||
if (options?.category) {
|
||||
filters.push(`category: { _eq: "${options.category}" }`);
|
||||
}
|
||||
|
||||
if (options?.difficulty) {
|
||||
filters.push(`difficulty: { _eq: "${options.difficulty}" }`);
|
||||
}
|
||||
|
||||
if (options?.search) {
|
||||
// Search in translations title and description
|
||||
filters.push(`_or: [
|
||||
{ translations: { title: { _icontains: "${options.search}" } } },
|
||||
{ translations: { description: { _icontains: "${options.search}" } } }
|
||||
]`);
|
||||
}
|
||||
|
||||
const filterString = filters.length > 0 ? `filter: { _and: [{ ${filters.join(' }, { ')} }] }` : '';
|
||||
const limitString = options?.limit ? `limit: ${options.limit}` : '';
|
||||
|
||||
const query = `
|
||||
query {
|
||||
projects(
|
||||
${filterString}
|
||||
${limitString}
|
||||
sort: ["-featured", "-created_at"]
|
||||
) {
|
||||
id
|
||||
slug
|
||||
category
|
||||
difficulty
|
||||
tags
|
||||
technologies
|
||||
github_url
|
||||
live_url
|
||||
image_url
|
||||
demo_video_url
|
||||
performance_metrics
|
||||
screenshots
|
||||
featured
|
||||
published
|
||||
date_created
|
||||
date_updated
|
||||
translations {
|
||||
title
|
||||
description
|
||||
content
|
||||
challenges
|
||||
lessons_learned
|
||||
future_improvements
|
||||
languages_code { code }
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await directusRequest(
|
||||
'',
|
||||
{ body: { query } }
|
||||
);
|
||||
|
||||
const projects = (result as any)?.projects;
|
||||
if (!projects || projects.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return projects.map((proj: any) => {
|
||||
const trans =
|
||||
proj.translations?.find((t: any) => t.languages_code?.code === directusLocale) ||
|
||||
proj.translations?.[0] ||
|
||||
{};
|
||||
return {
|
||||
id: proj.id,
|
||||
slug: proj.slug,
|
||||
title: trans.title || proj.slug,
|
||||
description: trans.description || '',
|
||||
content: trans.content,
|
||||
category: proj.category,
|
||||
difficulty: proj.difficulty,
|
||||
tags: proj.tags || [],
|
||||
technologies: proj.technologies || [],
|
||||
challenges: trans.challenges,
|
||||
lessons_learned: trans.lessons_learned,
|
||||
future_improvements: trans.future_improvements,
|
||||
github_url: proj.github_url,
|
||||
live_url: proj.live_url,
|
||||
image_url: proj.image_url,
|
||||
demo_video_url: proj.demo_video_url,
|
||||
performance_metrics: proj.performance_metrics,
|
||||
screenshots: proj.screenshots || [],
|
||||
featured: proj.featured || false,
|
||||
published: proj.published || false,
|
||||
created_at: proj.date_created,
|
||||
updated_at: proj.date_updated
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch projects (${locale}):`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user