/** * Directus API Client (REST-based, no SDK dependencies) */ 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 = { en: 'en-US', de: 'de-DE', }; function toDirectusLocale(locale: string): string { return localeToDirectus[locale] || locale; } interface FetchOptions { method?: 'GET' | 'POST' | 'PATCH' | 'DELETE'; body?: any; } async function directusRequest( endpoint: string, options: FetchOptions = {} ): Promise { // Wenn kein Token gesetzt, skip Directus (nutze JSON fallback) if (!DIRECTUS_TOKEN || DIRECTUS_TOKEN === '') { return null; } const url = `${DIRECTUS_URL}/graphql`; try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${DIRECTUS_TOKEN}`, }, body: JSON.stringify(options.body || {}), // Timeout nach 2 Sekunden signal: AbortSignal.timeout(2000), }); 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; } return null; } const data = await response.json(); // 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; } return data?.data || null; } 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?.message); } return null; } } export async function getMessage(key: string, locale: string): Promise { // Note: messages collection doesn't exist in Directus yet // The app uses JSON files as fallback via i18n-loader // Return null to skip Directus and use JSON fallback directly return null; /* Commented out until messages collection is created in Directus const directusLocale = toDirectusLocale(locale); // GraphQL Query für Directus Native Translations // Hole alle translations, filter client-side da GraphQL filter komplex ist const query = ` query { messages(filter: {key: {_eq: "${key}"}}, limit: 1) { key translations { value languages_code { code } } } } `; try { const result = await directusRequest( '', { body: { query } } ); const messages = (result as any)?.messages; if (!messages || messages.length === 0) { return null; } // Hole die Translation für die gewünschte Locale (client-side filter) const translations = messages[0]?.translations || []; const translation = translations.find((t: any) => t.languages_code?.code === directusLocale ); return translation?.value || null; } catch (error) { console.error(`Failed to fetch message ${key} (${locale}):`, error); return null; } */ } export async function getContentPage( slug: string, locale: string ): Promise { const directusLocale = toDirectusLocale(locale); const query = ` query { content_pages( filter: { _and: [ { slug: { _eq: "${slug}" } }, { locale: { _eq: "${directusLocale}" } } ] } limit: 1 ) { slug locale title content status } } `; try { const result = await directusRequest( '', { body: { query } } ); const pages = (result as any)?.content_pages || []; if (pages.length === 0) { // Try without locale filter const fallbackQuery = ` query { content_pages( filter: { slug: { _eq: "${slug}" } } limit: 1 ) { slug locale title content status } } `; const fallbackResult = await directusRequest('', { body: { query: fallbackQuery } }); const fallbackPages = (fallbackResult as any)?.content_pages || []; return fallbackPages[0] || null; } return 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> = { '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 = { frontend: 'Globe', backend: 'Server', tools: 'Wrench', security: 'Shield' }; const categoryNames: Record> = { '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 { 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 { 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 { const directusLocale = toDirectusLocale(locale); // Build filters const filters = ['status: { _eq: "published" }']; if (options?.featured !== undefined) { filters.push(`featured: { _eq: ${options.featured ? 'true' : 'false'} }`); } // Remove published filter since it doesn't exist in Directus schema // The status field already handles published/draft state 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", "-date_created"] ) { id slug category difficulty tags technologies challenges lessons_learned future_improvements github live image_url demo_video date_created date_updated featured status translations { title description content meta_description keywords 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: proj.challenges, lessons_learned: proj.lessons_learned, future_improvements: proj.future_improvements, github_url: proj.github, live_url: proj.live, image_url: proj.image_url, demo_video_url: proj.demo_video, performance_metrics: proj.performance_metrics, screenshots: proj.screenshots || [], featured: proj.featured === 1 || proj.featured === true, published: proj.status === 'published', created_at: proj.date_created, updated_at: proj.date_updated }; }); } catch (error) { console.error(`Failed to fetch projects (${locale}):`, error); return null; } }