feat: major UI/UX overhaul, snippets system, and performance fixes
Some checks failed
Dev Deployment (Zero Downtime) / deploy-dev (push) Failing after 9m26s

This commit is contained in:
2026-02-16 12:31:40 +01:00
parent 6f62b37c3a
commit a5dba298f3
41 changed files with 1610 additions and 499 deletions

View File

@@ -24,7 +24,11 @@ function toDirectusLocale(locale: string): string {
interface FetchOptions {
method?: 'GET' | 'POST' | 'PATCH' | 'DELETE';
body?: any;
body?: {
query?: string;
variables?: Record<string, unknown>;
[key: string]: unknown;
};
}
async function directusRequest<T>(
@@ -75,9 +79,9 @@ async function directusRequest<T>(
}
return data?.data || null;
} catch (error: any) {
} catch (error: unknown) {
// Timeout oder Network Error - stille fallback
if (error?.name === 'TimeoutError' || error?.name === 'AbortError') {
if (error && typeof error === 'object' && 'name' in error && (error.name === 'TimeoutError' || error.name === 'AbortError')) {
if (process.env.NODE_ENV === 'development') {
console.error('Directus timeout');
}
@@ -85,7 +89,8 @@ async function directusRequest<T>(
}
// Andere Errors nur in dev loggen
if (process.env.NODE_ENV === 'development') {
console.error('Directus request failed:', error?.message);
const message = error && typeof error === 'object' && 'message' in error ? String(error.message) : 'Unknown error';
console.error('Directus request failed:', message);
}
return null;
}
@@ -107,11 +112,20 @@ export async function getMessages(locale: string): Promise<Record<string, string
try {
const result = await directusRequest('', { body: { query } });
const messages = (result as any)?.messages || [];
interface MessageData {
messages: Array<{
key: string;
translations?: Array<{
languages_code?: { code: string };
value?: string;
}>;
}>;
}
const messages = (result as MessageData | null)?.messages || [];
const dictionary: Record<string, string> = {};
messages.forEach((m: any) => {
const trans = m.translations?.find((t: any) => t.languages_code?.code === directusLocale);
messages.forEach((m) => {
const trans = m.translations?.find((t) => t.languages_code?.code === directusLocale);
if (trans?.value) dictionary[m.key] = trans.value;
});
@@ -140,21 +154,35 @@ export async function getMessage(key: string, locale: string): Promise<string |
try {
const result = await directusRequest('', { body: { query } });
const messages = (result as any)?.messages;
interface SingleMessageData {
messages: Array<{
translations?: Array<{
languages_code?: { code: string };
value?: string;
}>;
}>;
}
const messages = (result as SingleMessageData | null)?.messages;
if (!messages || messages.length === 0) return null;
const translations = messages[0]?.translations || [];
const translation = translations.find((t: any) => t.languages_code?.code === directusLocale);
const translation = translations.find((t) => t.languages_code?.code === directusLocale);
return translation?.value || null;
} catch (error) {
return null;
}
}
export interface ContentPage {
slug: string;
content?: string;
[key: string]: unknown;
}
export async function getContentPage(
slug: string,
locale: string
): Promise<any | null> {
): Promise<ContentPage | null> {
const directusLocale = toDirectusLocale(locale);
const query = `
query {
@@ -182,7 +210,10 @@ export async function getContentPage(
{ body: { query } }
);
const pages = (result as any)?.content_pages || [];
interface ContentPagesResult {
content_pages: ContentPage[];
}
const pages = (result as ContentPagesResult | null)?.content_pages || [];
if (pages.length === 0) {
// Try without locale filter
const fallbackQuery = `
@@ -200,7 +231,7 @@ export async function getContentPage(
}
`;
const fallbackResult = await directusRequest('', { body: { query: fallbackQuery } });
const fallbackPages = (fallbackResult as any)?.content_pages || [];
const fallbackPages = (fallbackResult as ContentPagesResult | null)?.content_pages || [];
return fallbackPages[0] || null;
}
@@ -301,7 +332,19 @@ export async function getTechStack(locale: string): Promise<TechStackCategory[]
{ body: { query: categoriesQuery } }
);
const categories = (categoriesResult as any)?.tech_stack_categories;
interface TechStackCategoriesResult {
tech_stack_categories: Array<{
id: string;
key: string;
icon: string;
sort: number;
translations?: Array<{
languages_code?: { code: string };
name?: string;
}>;
}>;
}
const categories = (categoriesResult as TechStackCategoriesResult | null)?.tech_stack_categories;
if (!categories || categories.length === 0) {
if (process.env.NODE_ENV === 'development') {
@@ -325,15 +368,25 @@ export async function getTechStack(locale: string): Promise<TechStackCategory[]
);
const itemsData = await itemsResponse.json();
const allItems = itemsData?.data || [];
interface ItemsResponseData {
data: Array<{
id: string;
name: string;
category: string | number;
url?: string;
icon_url?: string;
sort: number;
}>;
}
const allItems = (itemsData as ItemsResponseData | null)?.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) =>
const categoriesWithItems = categories.map((cat) => {
const categoryItems = allItems.filter((item) =>
item.category === cat.id || item.category === parseInt(cat.id)
);
@@ -346,6 +399,7 @@ export async function getTechStack(locale: string): Promise<TechStackCategory[]
itemsToUse = categoryFallback.items.map((name, idx) => ({
id: `fallback-${cat.key}-${idx}`,
name: name,
category: cat.id,
url: undefined,
icon_url: undefined,
sort: idx + 1
@@ -509,7 +563,7 @@ export async function getBookReviews(locale: string): Promise<BookReview[] | nul
// Projects Types
export interface Project {
id: string;
id: string | number; // Allow both string (from Directus) and number (from Prisma)
slug: string;
title: string;
description: string;
@@ -798,3 +852,57 @@ export async function getProjectBySlug(
return null;
}
}
// Snippets Types
export interface Snippet {
id: string;
title: string;
category: string;
code: string;
description: string;
language: string;
}
/**
* Get Snippets from Directus
*/
export async function getSnippets(limit = 10, featured?: boolean): Promise<Snippet[] | null> {
const filters = ['status: { _eq: "published" }'];
if (featured !== undefined) {
filters.push(`featured: { _eq: ${featured} }`);
}
const filterString = `filter: { _and: [{ ${filters.join(' }, { ')} }] }`;
const query = `
query {
snippets(
${filterString}
limit: ${limit}
) {
id
title
category
code
description
language
}
}
`;
try {
const result = await directusRequest(
'',
{ body: { query } }
);
const snippets = (result as any)?.snippets;
if (!snippets || snippets.length === 0) {
return null;
}
return snippets;
} catch (error) {
console.error('Failed to fetch snippets:', error);
return null;
}
}