fix: resolve all lint errors, improve type safety, and remove unused code
Some checks failed
Dev Deployment (Zero Downtime) / deploy-dev (push) Failing after 7m26s

Remove unused imports, replace `any` types with proper interfaces in directus.ts
and i18n-loader.ts, exclude scripts/ and coverage/ from ESLint, and fix
unused variable warnings across the codebase.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-17 14:46:35 +01:00
parent a5dba298f3
commit 6fd4756f35
12 changed files with 155 additions and 72 deletions

View File

@@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { getSnippets } from "@/lib/directus"; import { getSnippets } from "@/lib/directus";
import { Terminal, ArrowLeft, Code } from "lucide-react"; import { Terminal, ArrowLeft } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import SnippetsClient from "./SnippetsClient"; import SnippetsClient from "./SnippetsClient";

View File

@@ -18,6 +18,7 @@ jest.mock('next-intl', () => ({
describe('NotFound', () => { describe('NotFound', () => {
it('renders the 404 page with the new design text', () => { it('renders the 404 page with the new design text', () => {
render(<NotFound />); render(<NotFound />);
expect(screen.getByText("Lost in the Liquid.")).toBeInTheDocument(); expect(screen.getByText(/Page not/i)).toBeInTheDocument();
expect(screen.getByText(/Found/i)).toBeInTheDocument();
}); });
}); });

View File

@@ -12,7 +12,7 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ return NextResponse.json({
snippets: snippets || [] snippets: snippets || []
}); });
} catch (error) { } catch (_error) {
return NextResponse.json({ error: 'Failed to fetch snippets' }, { status: 500 }); return NextResponse.json({ error: 'Failed to fetch snippets' }, { status: 500 });
} }
} }

View File

@@ -2,7 +2,7 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Mail, MapPin, Send, Github, Linkedin, ExternalLink } from "lucide-react"; import { Mail, MapPin, Send, Github, Linkedin } from "lucide-react";
import { useToast } from "@/components/Toast"; import { useToast } from "@/components/Toast";
import { useLocale, useTranslations } from "next-intl"; import { useLocale, useTranslations } from "next-intl";
import type { JSONContent } from "@tiptap/react"; import type { JSONContent } from "@tiptap/react";
@@ -152,20 +152,6 @@ const Contact = () => {
validateForm(); validateForm();
}; };
const contactInfo = [
{
icon: Mail,
title: tInfo("email"),
value: "contact@dk0.dev",
href: "mailto:contact@dk0.dev",
},
{
icon: MapPin,
title: tInfo("location"),
value: tInfo("locationValue"),
},
];
return ( return (
<section <section
id="contact" id="contact"

View File

@@ -1,7 +1,6 @@
"use client"; "use client";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Github, Linkedin, Mail } from "lucide-react";
import { useLocale, useTranslations } from "next-intl"; import { useLocale, useTranslations } from "next-intl";
import Image from "next/image"; import Image from "next/image";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Home, ArrowLeft, Search, Ghost, RefreshCcw, Terminal } from "lucide-react"; import { ArrowLeft, Search, Terminal } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";

View File

@@ -16,6 +16,8 @@ const eslintConfig = [
".next/**", ".next/**",
"out/**", "out/**",
"build/**", "build/**",
"coverage/**",
"scripts/**",
"next-env.d.ts", "next-env.d.ts",
], ],
}, },

View File

@@ -49,8 +49,10 @@ jest.mock('next/server', () => {
return { return {
...actual, ...actual,
NextResponse: { NextResponse: {
json: (data: Record<string, unknown>, init?: unknown) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any
json: (data: Record<string, unknown>, init?: any) => {
// Use global Response from whatwg-fetch // Use global Response from whatwg-fetch
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const res = new (global as any).Response(JSON.stringify(data), init); const res = new (global as any).Response(JSON.stringify(data), init);
res.headers.set('Content-Type', 'application/json'); res.headers.set('Content-Type', 'application/json');
return res; return res;

View File

@@ -130,7 +130,7 @@ export async function getMessages(locale: string): Promise<Record<string, string
}); });
return dictionary; return dictionary;
} catch (error) { } catch (_error) {
return {}; return {};
} }
} }
@@ -168,7 +168,7 @@ export async function getMessage(key: string, locale: string): Promise<string |
const translations = messages[0]?.translations || []; const translations = messages[0]?.translations || [];
const translation = translations.find((t) => t.languages_code?.code === directusLocale); const translation = translations.find((t) => t.languages_code?.code === directusLocale);
return translation?.value || null; return translation?.value || null;
} catch (error) { } catch (_error) {
return null; return null;
} }
} }
@@ -279,13 +279,6 @@ const fallbackTechStackData: Record<string, Array<{ key: string; items: string[]
] ]
}; };
const categoryIconMap: Record<string, string> = {
frontend: 'Globe',
backend: 'Server',
tools: 'Wrench',
security: 'Shield'
};
const categoryNames: Record<string, Record<string, string>> = { const categoryNames: Record<string, Record<string, string>> = {
'en-US': { 'en-US': {
frontend: 'Frontend & Mobile', frontend: 'Frontend & Mobile',
@@ -413,7 +406,7 @@ export async function getTechStack(locale: string): Promise<TechStackCategory[]
icon: cat.icon, icon: cat.icon,
sort: cat.sort, sort: cat.sort,
name: cat.translations?.[0]?.name || categoryNames[directusLocale]?.[cat.key] || cat.key, name: cat.translations?.[0]?.name || categoryNames[directusLocale]?.[cat.key] || cat.key,
items: itemsToUse.map((item: any) => ({ items: itemsToUse.map((item: TechStackItem) => ({
id: item.id, id: item.id,
name: item.name, name: item.name,
url: item.url, url: item.url,
@@ -424,8 +417,8 @@ export async function getTechStack(locale: string): Promise<TechStackCategory[]
}); });
return categoriesWithItems; return categoriesWithItems;
} catch (error) { } catch (_error) {
console.error(`Failed to fetch tech stack (${locale}):`, error); console.error(`Failed to fetch tech stack (${locale}):`, _error);
return null; return null;
} }
} }
@@ -468,12 +461,23 @@ export async function getHobbies(locale: string): Promise<Hobby[] | null> {
{ body: { query } } { body: { query } }
); );
const hobbies = (result as any)?.hobbies; interface HobbiesResult {
hobbies: Array<{
id: string;
key: string;
icon: string;
translations?: Array<{
title?: string;
description?: string;
}>;
}>;
}
const hobbies = (result as HobbiesResult | null)?.hobbies;
if (!hobbies || hobbies.length === 0) { if (!hobbies || hobbies.length === 0) {
return null; return null;
} }
return hobbies.map((hobby: any) => ({ return hobbies.map((hobby) => ({
id: hobby.id, id: hobby.id,
key: hobby.key, key: hobby.key,
icon: hobby.icon, icon: hobby.icon,
@@ -533,15 +537,30 @@ export async function getBookReviews(locale: string): Promise<BookReview[] | nul
{ body: { query } } { body: { query } }
); );
const reviews = (result as any)?.book_reviews; interface BookReviewsResult {
book_reviews: Array<{
id: string;
hardcover_id?: string;
book_title: string;
book_author: string;
book_image?: string;
rating: number | string;
finished_at?: string;
translations?: Array<{
review?: string;
languages_code?: { code: string };
}>;
}>;
}
const reviews = (result as BookReviewsResult | null)?.book_reviews;
if (!reviews || reviews.length === 0) { if (!reviews || reviews.length === 0) {
return null; return null;
} }
return reviews.map((item: any) => { return reviews.map((item) => {
// Filter die passende Übersetzung im Code // Filter die passende Übersetzung im Code
const translation = item.translations?.find( const translation = item.translations?.find(
(t: any) => t.languages_code?.code === directusLocale (t) => t.languages_code?.code === directusLocale
) || item.translations?.[0]; // Fallback auf die erste Übersetzung falls locale nicht passt ) || item.translations?.[0]; // Fallback auf die erste Übersetzung falls locale nicht passt
return { return {
@@ -682,19 +701,52 @@ export async function getProjects(
{ body: { query } } { body: { query } }
); );
const projects = (result as any)?.projects; interface ProjectsResult {
projects: Array<{
id: string;
slug: string;
category?: string;
difficulty?: string;
tags?: string[] | string;
technologies?: string[] | string;
challenges?: string;
lessons_learned?: string;
future_improvements?: string;
github?: string;
live?: string;
image_url?: string;
demo_video?: string;
performance_metrics?: string;
screenshots?: string[] | string;
date_created?: string;
date_updated?: string;
featured?: boolean | number;
status?: string;
translations?: Array<{
title?: string;
description?: string;
content?: string;
meta_description?: string;
keywords?: string;
button_live_label?: string;
button_github_label?: string;
languages_code?: { code: string };
}>;
}>;
}
const projects = (result as ProjectsResult | null)?.projects;
if (!projects || projects.length === 0) { if (!projects || projects.length === 0) {
return null; return null;
} }
return projects.map((proj: any) => { return projects.map((proj) => {
const trans = const trans =
proj.translations?.find((t: any) => t.languages_code?.code === directusLocale) || proj.translations?.find((t) => t.languages_code?.code === directusLocale) ||
proj.translations?.[0] || proj.translations?.[0] ||
{}; {};
// Parse JSON string fields if needed // Parse JSON string fields if needed
const parseTags = (tags: any) => { const parseTags = (tags: string[] | string | undefined): string[] => {
if (!tags) return []; if (!tags) return [];
if (Array.isArray(tags)) return tags; if (Array.isArray(tags)) return tags;
if (typeof tags === 'string') { if (typeof tags === 'string') {
@@ -706,7 +758,7 @@ export async function getProjects(
} }
return []; return [];
}; };
return { return {
id: proj.id, id: proj.id,
slug: proj.slug, slug: proj.slug,
@@ -734,8 +786,8 @@ export async function getProjects(
updated_at: proj.date_updated updated_at: proj.date_updated
}; };
}); });
} catch (error) { } catch (_error) {
console.error(`Failed to fetch projects (${locale}):`, error); console.error(`Failed to fetch projects (${locale}):`, _error);
return null; return null;
} }
} }
@@ -797,19 +849,51 @@ export async function getProjectBySlug(
{ body: { query } } { body: { query } }
); );
const projects = (result as any)?.projects; interface ProjectResult {
projects: Array<{
id: string;
slug: string;
category?: string;
difficulty?: string;
tags?: string[] | string;
technologies?: string[] | string;
challenges?: string;
lessons_learned?: string;
future_improvements?: string;
github?: string;
live?: string;
image_url?: string;
demo_video?: string;
screenshots?: string[] | string;
date_created?: string;
date_updated?: string;
featured?: boolean | number;
status?: string;
translations?: Array<{
title?: string;
description?: string;
content?: string;
meta_description?: string;
keywords?: string;
button_live_label?: string;
button_github_label?: string;
languages_code?: { code: string };
}>;
}>;
}
const projects = (result as ProjectResult | null)?.projects;
if (!projects || projects.length === 0) { if (!projects || projects.length === 0) {
return null; return null;
} }
const proj = projects[0]; const proj = projects[0];
const trans = const trans =
proj.translations?.find((t: any) => t.languages_code?.code === directusLocale) || proj.translations?.find((t) => t.languages_code?.code === directusLocale) ||
proj.translations?.[0] || proj.translations?.[0] ||
{}; {};
// Parse JSON string fields if needed // Parse JSON string fields if needed
const parseTags = (tags: any) => { const parseTags = (tags: string[] | string | undefined): string[] => {
if (!tags) return []; if (!tags) return [];
if (Array.isArray(tags)) return tags; if (Array.isArray(tags)) return tags;
if (typeof tags === 'string') { if (typeof tags === 'string') {
@@ -821,7 +905,7 @@ export async function getProjectBySlug(
} }
return []; return [];
}; };
return { return {
id: proj.id, id: proj.id,
slug: proj.slug, slug: proj.slug,
@@ -847,8 +931,8 @@ export async function getProjectBySlug(
created_at: proj.date_created, created_at: proj.date_created,
updated_at: proj.date_updated updated_at: proj.date_updated
}; };
} catch (error) { } catch (_error) {
console.error(`Failed to fetch project by slug ${slug} (${locale}):`, error); console.error(`Failed to fetch project by slug ${slug} (${locale}):`, _error);
return null; return null;
} }
} }
@@ -895,14 +979,17 @@ export async function getSnippets(limit = 10, featured?: boolean): Promise<Snipp
{ body: { query } } { body: { query } }
); );
const snippets = (result as any)?.snippets; interface SnippetsResult {
snippets: Snippet[];
}
const snippets = (result as SnippetsResult | null)?.snippets;
if (!snippets || snippets.length === 0) { if (!snippets || snippets.length === 0) {
return null; return null;
} }
return snippets; return snippets;
} catch (error) { } catch (_error) {
console.error('Failed to fetch snippets:', error); console.error('Failed to fetch snippets:', _error);
return null; return null;
} }
} }

View File

@@ -5,20 +5,20 @@
* - Caches results (5 min TTL) * - Caches results (5 min TTL)
*/ */
import { getMessage, getContentPage } from './directus'; import { getMessage, getContentPage, ContentPage } from './directus';
import enMessages from '@/messages/en.json'; import enMessages from '@/messages/en.json';
import deMessages from '@/messages/de.json'; import deMessages from '@/messages/de.json';
const jsonFallback = { en: enMessages, de: deMessages }; const jsonFallback = { en: enMessages, de: deMessages };
// Simple in-memory cache // Simple in-memory cache
const cache = new Map<string, { value: any; expires: number }>(); const cache = new Map<string, { value: unknown; expires: number }>();
function setCached(key: string, value: any, ttlSeconds = 300) { function setCached(key: string, value: unknown, ttlSeconds = 300) {
cache.set(key, { value, expires: Date.now() + ttlSeconds * 1000 }); cache.set(key, { value, expires: Date.now() + ttlSeconds * 1000 });
} }
function getCached(key: string): any | null { function getCached(key: string): unknown | null {
const hit = cache.get(key); const hit = cache.get(key);
if (!hit) return null; if (!hit) return null;
if (Date.now() > hit.expires) { if (Date.now() > hit.expires) {
@@ -38,7 +38,7 @@ export async function getLocalizedMessage(
): Promise<string> { ): Promise<string> {
const cacheKey = `msg:${key}:${locale}`; const cacheKey = `msg:${key}:${locale}`;
const cached = getCached(cacheKey); const cached = getCached(cacheKey);
if (cached !== null) return cached; if (cached !== null) return cached as string;
// Try Directus with requested locale // Try Directus with requested locale
const dbValue = await getMessage(key, locale); const dbValue = await getMessage(key, locale);
@@ -84,11 +84,11 @@ export async function getLocalizedMessage(
export async function getLocalizedContent( export async function getLocalizedContent(
slug: string, slug: string,
locale: string locale: string
): Promise<any | null> { ): Promise<ContentPage | null> {
const cacheKey = `page:${slug}:${locale}`; const cacheKey = `page:${slug}:${locale}`;
const cached = getCached(cacheKey); const cached = getCached(cacheKey);
if (cached !== null) return cached; if (cached !== null) return cached as ContentPage;
if (cached === null && cache.has(cacheKey)) return null; // Already checked, not found if (cache.has(cacheKey)) return null; // Already checked, not found
// Try Directus with requested locale // Try Directus with requested locale
const dbPage = await getContentPage(slug, locale); const dbPage = await getContentPage(slug, locale);
@@ -115,14 +115,18 @@ export async function getLocalizedContent(
* Helper: Get nested value from object * Helper: Get nested value from object
* Example: "nav.home" → obj.nav.home * Example: "nav.home" → obj.nav.home
*/ */
function getNestedValue(obj: any, path: string): any { function getNestedValue(obj: Record<string, unknown>, path: string): string | null {
const keys = path.split('.'); const keys = path.split('.');
let value = obj; let value: unknown = obj;
for (const key of keys) { for (const key of keys) {
value = value?.[key]; if (value && typeof value === 'object' && key in value) {
value = (value as Record<string, unknown>)[key];
} else {
return null;
}
if (value === undefined) return null; if (value === undefined) return null;
} }
return value; return typeof value === 'string' ? value : null;
} }
/** /**

View File

@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-require-imports */
const fetch = require('node-fetch'); const fetch = require('node-fetch');
require('dotenv').config(); require('dotenv').config();
@@ -19,7 +20,7 @@ async function setupSnippets() {
schema: { name: 'snippets' } schema: { name: 'snippets' }
}) })
}); });
} catch (e) {} } catch (_e) {}
// 2. Add Fields // 2. Add Fields
const fields = [ const fields = [
@@ -39,7 +40,7 @@ async function setupSnippets() {
headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' }, headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' },
body: JSON.stringify(f) body: JSON.stringify(f)
}); });
} catch (e) {} } catch (_e) {}
} }
// 3. Add Example Data // 3. Add Example Data
@@ -69,7 +70,7 @@ async function setupSnippets() {
headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' }, headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' },
body: JSON.stringify(s) body: JSON.stringify(s)
}); });
} catch (e) {} } catch (_e) {}
} }
console.log('✅ Snippets setup complete!'); console.log('✅ Snippets setup complete!');

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-require-imports */
const fetch = require('node-fetch'); const fetch = require('node-fetch');
require('dotenv').config(); require('dotenv').config();
@@ -136,8 +137,8 @@ async function syncHobbies() {
}) })
}); });
} }
} catch (e) { } catch (_e) {
console.error(`Failed to sync ${hobby.key}:`, e.message); console.error(`Failed to sync ${hobby.key}:`, _e.message);
} }
} }
@@ -154,7 +155,7 @@ async function syncHobbies() {
headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}` } headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}` }
}); });
} }
} catch (e) {} } catch (_e) {}
console.log('✅ Done!'); console.log('✅ Done!');
} }