= ({ children }) => {
- const { t } = useTranslation();
-
- const containerVariants = {
- hidden: {},
- visible: {
- transition: {
- staggerChildren: 0.1
- }
- }
- };
-
- const itemVariants = {
- hidden: { opacity: 0, y: 20 },
- visible: { opacity: 1, y: 0 }
- };
-
- return (
-
- {/* Header */}
-
-
-
-
-
- {t('portfolio.ui.projectDetails', 'PROJECT DETAILS')}
-
-
-
-
-
- {/* Main Content */}
-
-
- {/* Background Effects */}
-
-
-
-
- {[...Array(9)].map((_, i) => (
-
- ))}
-
-
-
-
- {/* Scroll Decoration */}
-
-
- );
-};
-
-export default ProjectLayout;
\ No newline at end of file
diff --git a/src/pages-vite/portfolio/index.tsx b/src/pages-vite/portfolio/index.tsx
deleted file mode 100644
index bdb4360..0000000
--- a/src/pages-vite/portfolio/index.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import { useTranslation } from 'react-i18next';
-import { motion } from 'framer-motion';
-import SEO from '../../components/SEO';
-import PortfolioGrid from './components/PortfolioGrid';
-
-const PortfolioPage = () => {
- const { t } = useTranslation();
-
- const sectionVariants = {
- hidden: { opacity: 0, y: 20 },
- visible: { opacity: 1, y: 0 }
- };
-
-
- return (
- <>
-
-
-
- {/* Main Content */}
-
-
-
-
- {t('portfolio.sections.title')}
-
-
- {t('portfolio.sections.subtitle')}
-
-
-
-
- {/* Portfolio Grid */}
-
-
-
-
-
-
- >
- );
-};
-
-export default PortfolioPage;
\ No newline at end of file
diff --git a/src/pages-vite/portfolio/projects/ai-data-reader.mdx b/src/pages-vite/portfolio/projects/ai-data-reader.mdx
deleted file mode 100644
index 07020c9..0000000
--- a/src/pages-vite/portfolio/projects/ai-data-reader.mdx
+++ /dev/null
@@ -1,154 +0,0 @@
----
-slug: "ai-data-reader"
-title: "KI-basierte Produktdatenpflege aus PDF-Dateien"
-description: "Entwicklung eines automatisierten Systems zur Extraktion und Strukturierung von Produktdaten aus PDF-Dokumenten"
-excerpt: "Integration von Claude AI für die intelligente Verarbeitung von Produktinformationen und nahtlose JTL-ERP-Integration."
-date: "2024-02-09"
-category: "Data Processing"
-coverImage: "/images/projects/ai-data-reader/cover.jpg"
-client: "Amazon Seller"
-duration: "2 Monate"
-url: "https://www.damjan-savic.com"
-repository: ""
-documentation: ""
-published: true
-featured: true
-technologies: ["Python", "FastAPI", "Claude AI", "PyPDF", "Pydantic", "JTL-Wawi"]
-tags: ["Data Extraction", "ERP", "Process Automation", "PDF Processing"]
----
-
-
-
- Ein KI-gestütztes System zur automatisierten Extraktion von Produktdaten aus PDF-Dokumenten mit direkter Integration in JTL-Warenwirtschaftssysteme, entwickelt für maximale Effizienz in der Produktdatenpflege.
-
-
-
-
-
-Systemarchitektur
-
-
- Die Lösung basiert auf einer modularen Python-Architektur:
-
-
- - Robuste PDF-Textextraktion mit PyPDF
- - KI-gestützte Datenextraktion mit Claude AI
- - Validierung durch Pydantic Models
- - JTL-ERP Integration über CSV-Export
-
-
-
-Datenmodelle
-
-
-
-{`class ProductData(BaseModel):
- artikel_nummer: str
- name: str
- hersteller: str
- inhaltsstoffe: List[str]
- allergene: List[str]
- naehrwerte: NaehrwertData
- nettogewicht: str
- vegan: bool
- vegetarisch: bool`}
-
-
-
-
-Extraktionsprozess
-
-
- Der automatisierte Workflow umfasst:
-
-
- - PDF-Batch-Verarbeitung aus Input-Verzeichnis
- - Intelligente Texterkennung und -strukturierung
- - KI-basierte Datenzuordnung
- - Automatische Datenvalidierung
- - JTL-konformer CSV-Export
-
-
-
-Datenvalidierung
-
-
- Mehrschichtige Validierungslogik:
-
-
- - Strukturvalidierung durch Pydantic
- - Geschäftsregelvalidierung
- - Format- und Typprüfungen
- - JTL-Kompatibilitätsprüfung
-
-
-
-KI-Implementierung
-
-
-
-{`def _extract_with_claude(self, prompt: str) -> Dict[str, Any]:
- """Extrahiert strukturierte Daten mit Claude AI."""
- response = self.client.messages.create(
- model="claude-3-sonnet-20240229",
- messages=[{
- "role": "user",
- "content": prompt
- }]
- )
- return self._clean_json_string(response.content)`}
-
-
-
-
-Fehlerbehandlung
-
-
- Robuste Fehlerbehandlung auf mehreren Ebenen:
-
-
- - PDF-Lesefehler-Management
- - KI-Extraktionsfehlerbehandlung
- - Datenvalidierungsfehler-Reporting
- - Automatische Wiederholungsversuche
-
-
-
-JTL-Integration
-
-
- Nahtlose Integration in JTL-Systeme:
-
-
- - Standardisierte CSV-Exportformate
- - Automatische Feldmappings
- - Datentyp-Konvertierung
- - Validierung der JTL-Kompatibilität
-
-
-
-Logging & Monitoring
-
-
- Umfassende Überwachung:
-
-
- - Detaillierte Verarbeitungslogs
- - Fehlerprotokolle
- - Performance-Metriken
- - Erfolgsraten-Tracking
-
-
-
-Ergebnisse
-
-
- Die Implementierung führte zu signifikanten Verbesserungen:
-
-
- - 90% Zeitersparnis bei der Produktdatenpflege
- - 99% Genauigkeit bei der Datenextraktion
- - Eliminierung manueller Dateneingaben
- - Standardisierte Produktdatenqualität
-
-
\ No newline at end of file
diff --git a/src/pages-vite/portfolio/projects/ai-music-production.mdx b/src/pages-vite/portfolio/projects/ai-music-production.mdx
deleted file mode 100644
index 5e2978f..0000000
--- a/src/pages-vite/portfolio/projects/ai-music-production.mdx
+++ /dev/null
@@ -1,132 +0,0 @@
----
-slug: "ai-music-production"
-title: "Suno AI - KI-gestützte Musikproduktion"
-description: "Kompletter Workflow für Musikproduktion mit Suno AI: Von Songtext zum fertigen Track"
-excerpt: "Musikproduktion mit KI-Unterstützung von Suno AI. Vom handgeschriebenen Text zum fertigen Track in 2-3 Stunden."
-date: "2024-12-01"
-category: "AI & Automation"
-coverImage: "/images/projects/ai-music-production/cover.jpg"
-client: "Eigenprojekt"
-duration: "Laufend"
-url: "https://soundcloud.com/desetka"
-repository: ""
-documentation: ""
-published: true
-featured: true
-technologies: ["Suno AI", "Soundcloud", "Pinterest", "Photoshop", "KI-Musikproduktion"]
-tags: ["Suno AI", "Musikproduktion", "KI Musik", "Desetka", "Soundcloud", "Kreative KI"]
----
-
-
-
- Musikproduktion mit KI-Unterstützung: Kompletter Workflow zur Erstellung professioneller Songs mit Suno AI. Von der Idee über den Text zum fertigen Track auf Soundcloud - alles in einem strukturierten Prozess.
-
-
-
-
-
-Die Herausforderung
-
-
- Traditionelle Musikproduktion erfordert umfangreiche Ressourcen und Fähigkeiten:
-
-
- - Klassische Musikproduktion erfordert teure Software und Hardware
- - Instrumentalisten und Produzenten müssen koordiniert werden
- - Der Produktionsprozess dauert oft Wochen oder Monate
- - Hohe Einstiegshürden für Künstler ohne musikalische Ausbildung
- - Kreative Visionen lassen sich nur schwer schnell umsetzen
-
-
-
-Der Lösungsansatz
-
-
- Mit Suno AI lässt sich der gesamte Musikproduktionsprozess auf wenige Stunden reduzieren. Der Workflow kombiniert menschliche Kreativität (Texte) mit KI-generierter Musik für authentische, professionelle Ergebnisse.
-
-
- - Texte werden von Hand geschrieben - volle kreative Kontrolle
- - Suno AI generiert Instrumentals basierend auf Style Prompts
- - Iteratives Remixen für den perfekten Sound
- - Cover-Design mit Pinterest-Inspiration und Photoshop
- - Direkter Upload auf Soundcloud unter dem Alias 'Desetka'
-
-
-
-Der Workflow
-
-
- Schritt für Schritt Prozess für KI-Musikproduktion:
-
-
- - Text schreiben mit klarem Reimschema (z.B. Endungen auf e, a, i)
- - Melodie gedanklich beim Schreiben entwickeln
- - Instrumental in Suno AI mit Style Prompts generieren
- - Weirdness (35%) und Audio Influence (75%) konfigurieren
- - Verschiedene Styles mischen (z.B. Synthwave → Luxury Rap)
- - Favoriten liken für KI-Lerneffekt
- - Cover auf Pinterest finden und in Photoshop bearbeiten (1950x1950px)
- - WAV Export und Upload auf Soundcloud
-
-
-
-Suno AI Konfiguration
-
-
-
-{`// Suno AI Style Konfiguration
-const sunoConfig = {
- name: "205 Matrica Instrumental",
- settings: {
- weirdness: "35%",
- styleInfluence: "75%",
- audioInfluence: "25-35%" // Niedriger für Remastered
- },
- styles: [
- "Synthwave",
- "Melodic RIP",
- "Luxury Rap"
- ],
- tips: [
- "Gelikte Songs beeinflussen zukünftige Generierungen",
- "Bei Artefakten: Audio Influence reduzieren",
- "Styles kombinieren für einzigartigen Sound"
- ]
-};`}
-
-
-
-
-Kreativer Prozess
-
-
- Die Kunst des Songschreibens mit KI-Unterstützung:
-
-
- - Texte auf Serbisch schreiben (melodischer Klang)
- - Klares Reimschema für Hook und Strophen
- - Melodie gedanklich vor Produktion entwickeln
- - Keine Instrumentals für Anfangsideen nötig
- - Übersetzung ins Englische später möglich
- - 2-3 Stunden für kompletten Song
- - An guten Tagen: 3 Songs pro Stunde möglich
-
-
-
-Ergebnisse
-
-
- - Produktionszeit von Wochen auf Stunden reduziert
- - Volle kreative Kontrolle über Texte und Richtung
- - Professionelle Instrumentals ohne Studio
- - Iteratives Arbeiten bis zum perfekten Sound
- - Direkter Weg vom Konzept zur Veröffentlichung
- - Künstler-Alias 'Desetka' auf Soundcloud etabliert
-
-
-
-
-
- KI-Musikproduktion mit Suno AI revolutioniert den kreativen Prozess. Die Kombination aus handgeschriebenen Texten und KI-generierten Instrumentals ermöglicht schnelle und professionelle Umsetzung musikalischer Visionen. Der Mensch bleibt kreativ im Mittelpunkt - KI ist das Werkzeug zur Realisierung.
-
-
diff --git a/src/pages-vite/portfolio/projects/automated-ad-creatives.mdx b/src/pages-vite/portfolio/projects/automated-ad-creatives.mdx
deleted file mode 100644
index be7d02d..0000000
--- a/src/pages-vite/portfolio/projects/automated-ad-creatives.mdx
+++ /dev/null
@@ -1,146 +0,0 @@
----
-slug: "automated-ad-creatives"
-title: "Automatisierte Ad Creatives: KI-gestützte Werbemittel-Erstellung"
-description: "Entwicklung eines Workflows zur automatisierten Erstellung von Facebook Ad Creatives mit Claude Opus 4.5 - ohne Designsoftware, komplett im Chat"
-excerpt: "Von der Designvorlage zum fertigen Werbemittel: Wie KI die Ad-Creative-Erstellung revolutioniert."
-date: "2024-12-12"
-category: "KI & Automatisierung"
-coverImage: "/images/projects/automated-ad-creatives/cover.jpg"
-client: "Eigenprojekt"
-duration: "1 Tag"
-url: ""
-repository: ""
-documentation: ""
-published: true
-featured: true
-technologies: ["Claude Opus 4.5", "HTML/CSS", "PNG Export", "Prompt Engineering"]
-tags: ["KI", "Automatisierung", "Facebook Ads", "Marketing", "No-Code"]
-videoUrl: "https://www.tella.tv/video/automatisierte-ad-creatives-1kyw"
----
-
-
-
-
-
-
-
- Programme wie Illustrator, InDesign, Figma oder Canva können kompliziert sein. Dieses Projekt zeigt, wie sich Facebook Ad Creatives vollständig im KI-Chat erstellen lassen - ohne Designsoftware, ohne Programmierkenntnisse.
-
-
-
-
-
-Die Herausforderung
-
-
- Die Erstellung von Werbemitteln für Facebook-Kampagnen erfordert traditionell:
-
-
- - Kenntnisse in Designsoftware (Illustrator, Figma, Canva)
- - Zeit für Einarbeitung und Umsetzung
- - Verständnis für Formatvorgaben und Best Practices
- - Iterationen zwischen Design und Marketing
- - Budget für Designer oder Design-Tools
-
-
-
-Der Lösungsansatz
-
-
- Ein KI-gestützter Workflow, der den gesamten Prozess im Chat-Fenster abbildet:
-
-
- - Designvorlage als visuelle Referenz für die KI
- - HTML-Generierung durch natürlichsprachliche Prompts
- - Automatische Konvertierung zu PNG-Bilddateien
- - Integration zusätzlicher Bilder per Drag & Drop
- - Iterationen durch einfache Chat-Anweisungen
-
-
-
-Technische Umsetzung
-
-
Der Workflow im Detail
-
-
Schritt 1: Designgrundlage
-
- Auf Envato Elements nach "Facebook Ads Kampagne" suchen und ein passendes Template als Designreferenz herunterladen. Dieses Bild definiert Stil, Farbgebung und Layout.
-
-
-
Schritt 2: Erster Prompt
-
- Das Referenzbild zusammen mit einem beschreibenden Prompt an Claude Opus 4.5 senden. Die KI analysiert das Bild und generiert eine HTML-Datei, die das Design nachbildet.
-
-
-
Schritt 3: HTML zu PNG
-
- Im zweiten Prompt die KI bitten, die HTML-Datei als PNG zu exportieren. Das Ergebnis ist eine fertige Bilddatei für Facebook Ads.
-
-
-
Schritt 4: Bilder hinzufügen
-
- Zusätzliche Bilder (z.B. von Unsplash) per Drag & Drop in den Chat ziehen. Die KI integriert sie in das bestehende Creative.
-
-
-
-Verwendete Tools
-
-
-
-
- | Tool |
- Zweck |
-
-
-
-
- | Claude Opus 4.5 |
- KI für HTML-Generierung und Bildexport |
-
-
- | Envato Elements |
- Designvorlagen als Referenz |
-
-
- | Unsplash |
- Kostenlose Bilder für die Creatives |
-
-
-
-
-
-Vorteile des Ansatzes
-
-
- - Keine Design-Skills nötig: Die KI übernimmt die technische Umsetzung
- - Schnelle Iterationen: Änderungen in Sekunden durch Chat-Anweisungen
- - Skalierbar: Beliebig viele Varianten für A/B-Tests
- - Kosteneffizient: Keine teuren Design-Tools erforderlich
- - Flexibel: Funktioniert auch mit Gemini oder ChatGPT
-
-
-
-Einsatzszenarien
-
-
- - Performance-Marketing: Schnelle Creative-Tests ohne Wartezeit
- - E-Commerce: Produktwerbung in großem Umfang
- - Startups: Professionelle Ads ohne Design-Team
- - Agenturen: Effiziente Kundenarbeit bei Ad-Erstellung
-
-
-
-Fazit
-
-
- Der Workflow demonstriert, wie KI die Erstellung von Ad Creatives vereinfacht. Das Ergebnis ist vielleicht noch nicht auf dem Niveau eines professionellen Grafikdesigners - aber für schnelle Tests, erste Entwürfe oder Teams ohne Designressourcen ist dieser Ansatz eine echte Alternative.
-
-
- Die Technologie entwickelt sich rasant weiter. Was heute noch "nicht perfekt integriert" ist, wird in wenigen Monaten deutlich besser funktionieren.
-
-
diff --git a/src/pages-vite/portfolio/projects/cursor-ide.mdx b/src/pages-vite/portfolio/projects/cursor-ide.mdx
deleted file mode 100644
index fa38939..0000000
--- a/src/pages-vite/portfolio/projects/cursor-ide.mdx
+++ /dev/null
@@ -1,155 +0,0 @@
----
-slug: "cursor-ide"
-title: "Cursor IDE: KI-gestützte Softwareentwicklung"
-description: "Einführung in Cursor - die revolutionäre KI-gestützte Entwicklungsumgebung, die Softwareprojekte durch natürliche Sprache ermöglicht"
-excerpt: "Entdecke, wie Cursor IDE als persönlicher KI-Programmierer fungiert und komplette Projekte aus natürlicher Sprache erstellt."
-date: "2024-12-15"
-category: "KI & Entwicklung"
-coverImage: "/images/projects/cursor-ide/cover.jpg"
-client: "Tutorial"
-duration: "Einführung"
-url: "https://cursor.com"
-repository: ""
-documentation: ""
-published: true
-featured: true
-technologies: ["Cursor IDE", "KI", "Next.js", "React", "TypeScript", "Node.js"]
-tags: ["KI", "IDE", "Entwicklung", "Tutorial", "Code-Generierung", "Automatisierung"]
-videoUrl: "https://www.tella.tv/video/cursor-ide-1-1kjg"
----
-
-
-
-
-
-
-
- Cursor ist eine KI-gestützte Entwicklungsumgebung, die wie ein persönlicher Programmierer funktioniert. Sie versteht natürliches Deutsch, generiert Code eigenständig, findet und behebt Fehler - und baut komplette Softwareprojekte auf Zuruf.
-
-
-
-
-
-Was ist Cursor?
-
-
- Stell dir vor, du hättest einen persönlichen Programmierer, der neben dir sitzt und alle deine Softwareideen in natürlichem gesprochenen Deutsch verstehen und aufnehmen kann. Dieser würde diese gesamten Softwareprojekte auch eigenständig aufbauen können. Genau das ist Cursor.
-
-
- Cursor ist ein Texteditor mit integrierter KI:
-
-
- - Code für dich schreiben
- - Fragen beantworten
- - Komplette Projekte aufbauen
- - Fehler finden und beheben
-
-
-
-Früher vs. Heute
-
-
-
-
- | Früher |
- Mit Cursor |
-
-
-
-
- | Programmiersprachen lernen musste |
- Einfach beschreiben, was du haben möchtest |
-
-
- | Fehlersuche dauerte Stunden |
- KI findet und behebt Fehler automatisch |
-
-
- | Jede Zeile selbst tippen |
- KI generiert den Code |
-
-
- | Google und Stackoverflow als Begleiter |
- KI erklärt das gesamte Projekt im Editor |
-
-
-
-
-
-Das Interface
-
-
- Nach dem Download über cursor.com und Installation besteht das Interface aus:
-
-
- - Menüleiste (oben): Navigation durch die IDE
- - File Explorer (links): Projektstruktur mit Verzeichnissen und Dateien
- - Editor (Mitte): Ansicht und Bearbeitung von Dateien
- - Terminal (unten): Einblendbar über View > Terminal
- - KI Chat (rechts): Steuerung der Entwicklung per KI
-
-
-
-Praxisbeispiel: Website erstellen
-
-
- Ein einfacher Prompt wie:
-
-
-
-{`Ich möchte eine Website aufbauen mit Next, React
-und TypeScript mit einer modernen Darstellung
-von Hello World.`}
-
-
-
- Die KI fängt sofort mit der Entwicklung an:
-
-
- - Projektzustand wird geprüft
- - Das gesamte Projekt wird aufgebaut
- - Nach wenigen Sekunden steht die Website
-
-
-
-Website starten
-
-
- Nach der Codegenerierung:
-
-
-
-{`# Abhängigkeiten installieren
-npm install
-
-# Entwicklungsserver starten
-npm run dev`}
-
-
-
- Besonders praktisch: Ein integrierter Browser in der Editor-Ansicht ermöglicht die direkte Vorschau der Website während der Entwicklung.
-
-
-
-Erste Schritte
-
-
- - Cursor von cursor.com herunterladen
- - Installation für das jeweilige Betriebssystem durchführen
- - Projektordner anlegen und in Cursor öffnen
- - Im KI-Chat beschreiben, was gebaut werden soll
- - Die KI arbeiten lassen und bei Bedarf anpassen
-
-
-
-Fazit
-
-
- Cursor revolutioniert die Softwareentwicklung, indem es die Barriere zwischen Idee und Umsetzung dramatisch senkt. Statt Programmiersprachen zu lernen, beschreibst du einfach in natürlicher Sprache, was du haben möchtest - und die KI erledigt den Rest.
-
-
diff --git a/src/pages-vite/portfolio/projects/kamenpro.mdx b/src/pages-vite/portfolio/projects/kamenpro.mdx
deleted file mode 100644
index 28799d1..0000000
--- a/src/pages-vite/portfolio/projects/kamenpro.mdx
+++ /dev/null
@@ -1,229 +0,0 @@
----
-slug: "kamenpro"
-title: "KamenPro: Digitale Transformation für dekorative Steinverkleidungen"
-description: "Entwicklung einer modernen Multi-Location PWA für einen Hersteller dekorativer Steinverkleidungen aus Bijeljina mit React 18.3 und Supabase"
-excerpt: "Von Stein zu Pixel: Wie ein traditioneller Handwerksbetrieb durch moderne Webtechnologie 300% mehr Anfragen generiert."
-date: "2024-11-15"
-category: "Web Development"
-coverImage: "/images/projects/kamenpro/cover.jpg"
-client: "KamenPro - Željko"
-duration: "3 Monate"
-url: "https://kamenpro.net"
-repository: ""
-documentation: ""
-published: true
-featured: true
-technologies: ["React 18.3", "TypeScript", "Vite", "Supabase", "Framer Motion", "TailwindCSS", "PWA", "Schema.org"]
-tags: ["Multi-Location SEO", "E-Commerce", "PWA", "Local Business", "Performance"]
----
-
-
-
- KamenPro, ein etablierter Hersteller dekorativer Steinverkleidungen aus Bijeljina, stand vor der Herausforderung, sein traditionelles Handwerk in die digitale Welt zu übertragen. Diese Case Study zeigt, wie moderne Webtechnologie einem lokalen Handwerksbetrieb zu überregionalem Erfolg verhalf.
-
-
-
-
-
-Die Herausforderung
-
-
- Als Hersteller hochwertiger Steinverkleidungen aus Weißzement fehlte KamenPro die digitale Präsenz:
-
-
- - Keine digitale Präsenz trotz hoher Online-Nachfrage
- - Kunden suchten online nach 'dekorativni kamen' ohne KamenPro zu finden
- - Fehlende Produktpräsentation für 3 verschiedene Steinstrukturen
- - Keine lokale SEO-Präsenz in Bijeljina, Brčko und Tuzla
- - Verlust potenzieller Kunden an digital präsente Konkurrenz
-
-
-
-Der Lösungsansatz
-
-
- Eine moderne PWA mit Multi-Location SEO-Strategie für maximale lokale Sichtbarkeit:
-
-
- - Multi-Location SEO für Bijeljina, Brčko und Tuzla
- - Produktkatalog mit HD-Bildern der Steinstrukturen
- - PWA für Offline-Verfügbarkeit auf Baustellen
- - Supabase-Backend für Produktverwaltung und Anfragen
- - Schema.org LocalBusiness für optimale Google-Präsenz
-
-
-
-Technische Implementierung
-
-
Multi-Location SEO mit Schema.org
-
-
-{`// Standort-spezifische Landing Pages
-const LocationPage: React.FC<{city: string}> = ({ city }) => {
- const structuredData = {
- "@context": "https://schema.org",
- "@type": "LocalBusiness",
- "name": \`KamenPro \${city}\`,
- "description": \`Dekorativni kamen i fasadne obloge u \${city}\`,
- "telephone": "+387 65 678 634",
- "address": {
- "@type": "PostalAddress",
- "addressLocality": city,
- "addressCountry": "BA"
- }
- };
-
- return (
- <>
-
- Dekorativni kamen {city} | KamenPro
-
-
-
-
- >
- );
-};`}
-
-
-
-
-Produktverwaltung mit Supabase
-
-
-
-{`interface StoneProduct {
- id: string;
- name: string;
- type: 'decorative_stone' | 'rustic_brick';
- dimensions: {
- length: 44, // cm
- width: 8.5, // cm
- thickness: 15 // mm
- };
- price_per_m2: number; // 33-40 BAM
- weight_per_m2: 32; // kg
- textures: string[]; // 3 verschiedene
- available_colors: string[];
-}
-
-// Produkt-Service für Katalog
-class ProductService {
- async getProducts(filters?: {
- type?: string;
- priceRange?: [number, number];
- texture?: string;
- }) {
- let query = supabase
- .from('products')
- .select('*, product_images (*)')
- .eq('is_active', true);
-
- if (filters?.type) {
- query = query.eq('type', filters.type);
- }
-
- const { data, error } = await query;
- return this.transformProducts(data);
- }
-}`}
-
-
-
-
-PWA für Offline-Verfügbarkeit
-
-
-
-{`// Service Worker für Offline-Katalog
-self.addEventListener('install', event => {
- event.waitUntil(
- caches.open('kamenpro-v1').then(cache => {
- return cache.addAll([
- '/',
- '/offline.html',
- '/katalog',
- // Kritische Produktbilder
- '/images/products/dekorativni-kamen-preview.webp',
- '/images/products/rustik-cigla-preview.webp'
- ]);
- })
- );
-});
-
-// Network-first für Produktdaten
-self.addEventListener('fetch', event => {
- if (event.request.url.includes('/api/products')) {
- event.respondWith(
- fetch(event.request)
- .then(response => {
- // Cache aktualisieren
- const responseClone = response.clone();
- caches.open('kamenpro-api').then(cache => {
- cache.put(event.request, responseClone);
- });
- return response;
- })
- .catch(() => caches.match(event.request))
- );
- }
-});`}
-
-
-
-
-Lokale SEO-Erfolge
-
-
- Durch gezielte Multi-Location-Optimierung erreichten wir Top-Rankings:
-
-
- - Rank #1 für 'dekorativni kamen bijeljina'
- - Rank #2 für 'fasadne obloge brčko'
- - Rank #1 für 'rustik cigla tuzla'
- - Google My Business Integration für alle Standorte
- - Lokale Backlinks von Bauunternehmen
-
-
-
-Messbare Geschäftsergebnisse
-
-
- Die digitale Transformation brachte beeindruckende Ergebnisse:
-
-
- - 300% mehr Anfragen in den ersten 3 Monaten
- - Von 0 auf Platz 1-3 bei lokalen Suchanfragen
- - 45 Anfragen/Monat statt vorher ~12
- - 25-30 Projekte/Quartal statt 8-10
- - ROI von 275% in 6 Monaten
- - Expansion in neue Märkte durch Online-Präsenz
-
-
-
-Produktspezifikationen
-
-
- KamenPro's Produktpalette umfasst:
-
-
- - Dekorativer Stein: 44cm x 8.5cm, 15-20mm dick
- - Rustikale Ziegel: 5mm dick, wetterbeständig
- - Gewicht: 30-35 kg/m²
- - Material: Weißzement-Basis mit Additiven
- - Preise: 33-40 BAM (Stein), 25-30 BAM (Ziegel)
- - Varianten: 3 verschiedene Texturen, Farben nach Wunsch
-
-
-
-Fazit
-
-
- Die digitale Transformation von KamenPro zeigt eindrucksvoll, wie ein traditioneller Handwerksbetrieb durch moderne Webtechnologie neue Märkte erschließen kann.
- Die Kombination aus Multi-Location SEO, optimierter Produktpräsentation und technischer Exzellenz führte zu einer Verdreifachung der Kundenanfragen.
- Besonders bemerkenswert: Ein lokaler Steinverkleidungs-Hersteller aus Bijeljina erreicht nun Kunden in der gesamten Region -
- ein Beweis dafür, dass durchdachte Digitalisierung auch im traditionellen Handwerk messbare Erfolge bringt.
-
-
\ No newline at end of file
diff --git a/src/pages-vite/portfolio/projects/power-platform-governance.mdx b/src/pages-vite/portfolio/projects/power-platform-governance.mdx
deleted file mode 100644
index 23c7321..0000000
--- a/src/pages-vite/portfolio/projects/power-platform-governance.mdx
+++ /dev/null
@@ -1,320 +0,0 @@
----
-slug: "power-platform-governance"
-title: "Power Platform Governance & Automation Suite"
-description: "Enterprise-Governance-Plattform für Microsoft Power Platform und SharePoint Online"
-excerpt: "Zentrale Verwaltung, Überwachung und Automatisierung der gesamten M365-Umgebung - von Tenant-Provisionierung bis Compliance-Monitoring."
-date: "2024-09-15"
-category: "Enterprise Software"
-coverImage: "/images/projects/power-platform-governance/cover.jpg"
-client: "Enterprise CoE Teams"
-duration: "6 Monate"
-url: "https://governance-demo.azurewebsites.net"
-repository: ""
-documentation: "/docs/power-platform-governance"
-published: true
-featured: true
-technologies: ["React", "TypeScript", "Node.js", "Microsoft Graph", "PowerShell", "Azure Functions", "GraphQL", "Fluent UI"]
-tags: ["Microsoft 365", "Power Platform", "SharePoint", "Governance", "Automation", "Enterprise"]
----
-
-
-
- Eine umfassende Enterprise-Governance-Plattform, die IT-Administratoren und Power Platform CoE Teams ermöglicht, ihre gesamte M365-Umgebung zentral zu verwalten. Von automatisierter Provisionierung über Compliance-Monitoring bis zur intelligenten Ressourcenverwaltung - alles in einer einzigen, leistungsstarken Lösung.
-
-
-
-
-
-Die Herausforderung
-
-
- Unkontrolliertes Wachstum von Power Apps, Flows und SharePoint-Sites führte zu kritischen Problemen:
-
-
- - Shadow IT durch ungovernte Citizen Development
- - Compliance-Risiken und Datenschutzverletzungen
- - Explodierende Lizenzkosten durch ungenutzte Ressourcen
- - Fehlende Übersicht über die Power Platform Landschaft
- - Manuelle, fehleranfällige Provisionierungsprozesse
- - Inkonsistente Governance-Policies über Teams hinweg
-
-
-
-Die Lösung
-
-
- Eine zentrale Governance-Plattform mit folgenden Kernbausteinen:
-
-
- - Automated Provisioning Hub für Sites, Teams und Environments
- - Real-Time Monitoring Dashboard mit Live Activity Streams
- - Power Platform Governance Center mit DLP Policy Enforcement
- - Compliance & Security Suite mit Permission Analyzer
- - PowerShell Automation Framework für Custom Operations
- - Intelligent Resource Optimization mit ML-based Predictions
-
-
-
-Technische Architektur
-
-
-
-{`┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
-│ Web Portal │────▶│ GraphQL API │────▶│ Auth Service │
-└─────────────────┘ └──────────────────┘ └─────────────────┘
- │
- ┌────────────┴────────────┐
- ▼ ▼
- ┌─────────────────┐ ┌─────────────────┐
- │ Provisioning │ │ Monitoring │
- │ Service │ │ Service │
- └─────────────────┘ └─────────────────┘
- │ │
- └────────────┬────────────┘
- ▼
- ┌─────────────────┐
- │ Message Queue │
- │ (Service Bus) │
- └─────────────────┘
- │
- ┌───────────────────┼───────────────────┐
- ▼ ▼ ▼
-┌───────────────┐ ┌───────────────┐ ┌───────────────┐
-│ PowerShell │ │ Graph API │ │ Analytics │
-│ Executor │ │ Gateway │ │ Engine │
-└───────────────┘ └───────────────┘ └───────────────┘`}
-
-
-
-
-Automated Provisioning Hub
-
-
Site Provisioning Engine
-
-
-{`class SiteProvisioningEngine {
- async provisionSite(template: SiteTemplate, params: ProvisioningParams) {
- // Validate naming conventions
- const siteName = this.validateNaming(params.name);
-
- // Create site from template
- const site = await this.graph.sites.create({
- displayName: siteName,
- template: template.id,
- owner: params.owner,
- classification: params.classification
- });
-
- // Apply custom configurations
- await this.applyCustomizations(site, template.customizations);
-
- // Setup permissions
- await this.configurePermissions(site, params.permissions);
-
- // Execute post-provisioning workflows
- await this.runPostProvisioningWorkflows(site, template.workflows);
-
- return site;
- }
-}`}
-
-
-
-
-Real-Time Monitoring
-
-
-
-{`const ActivityMonitor: React.FC = () => {
- const { data: activities } = useSubscription(ACTIVITY_SUBSCRIPTION);
-
- return (
-
- Platform Activity
-
-
- );
-};`}
-
-
-
-
-PowerShell Automation Framework
-
-
-
-{`function New-GovernanceReport {
- [CmdletBinding()]
- param (
- [Parameter(Mandatory)]
- [string]$TenantId,
-
- [ValidateSet('Sites','Apps','Flows','All')]
- [string]$Scope = 'All',
-
- [DateTime]$StartDate = (Get-Date).AddDays(-30)
- )
-
- # Complex governance analysis logic
- $results = @{
- ComplianceScore = Get-ComplianceScore -TenantId $TenantId
- UnusedResources = Find-UnusedResources -Scope $Scope
- SecurityRisks = Analyze-SecurityPosture -StartDate $StartDate
- CostOptimization = Calculate-OptimizationPotential
- }
-
- Export-GovernanceReport -Results $results
-}`}
-
-
-
-
-Compliance & Security Suite
-
-
Permission Analyzer
-
- - Cross-tenant Permission Reports mit visueller Darstellung
- - Overprivileged Users Detection durch ML-Algorithmen
- - External Sharing Audit mit Risikobewertung
- - Inheritance Breaking Analysis für SharePoint Sites
- - Automated Access Reviews mit Approval Workflows
-
-
-
Data Loss Prevention
-
- - Custom DLP Policy Builder mit Drag-and-Drop Interface
- - Sensitive Data Discovery durch Pattern Matching
- - Real-time Policy Violation Alerts
- - Automated Remediation Actions
- - Compliance Dashboards mit Trend Analysis
-
-
-
-Policy as Code
-
-
-
-{`# Governance Policy Definition
-apiVersion: governance/v1
-kind: SitePolicy
-metadata:
- name: external-sharing-policy
-spec:
- rules:
- - effect: Deny
- resource: "sites/*"
- action: "share.external"
- condition:
- classification: "Confidential"
- - effect: Audit
- resource: "sites/*"
- action: "permissions.break"
- enforcement:
- mode: "preventive"
- notifications:
- - channel: "teams"
- webhook: "${TEAMS_WEBHOOK_URL}"`}
-
-
-
-
-Intelligent Resource Optimization
-
-
-
-{`class ResourceOptimizer {
- async analyzeUsage() {
- const resources = await this.getAllResources();
-
- for (const resource of resources) {
- const usage = await this.mlEngine.predictUsage(resource);
-
- if (usage.probability < 0.1) {
- await this.scheduleForCleanup(resource, {
- reason: 'Low usage probability',
- confidence: usage.confidence,
- suggestedAction: 'archive',
- scheduledDate: this.calculateCleanupDate(usage)
- });
- }
- }
-
- return this.generateOptimizationReport();
- }
-}`}
-
-
-
-
-Projekt-Metriken & Ergebnisse
-
-
Quantitative Ergebnisse
-
- - 95% schneller: Provisioning Zeit von 5 Min auf 15 Sek reduziert
- - 94% Compliance: Rate von 67% gesteigert
- - 40% Kostenreduktion: Durch Bereinigung ungenutzter Ressourcen
- - 3x Produktivität: Admin-Effizienz verdreifacht
- - 80% schneller: Incident Response Time verbessert
-
-
-
Technische Achievements
-
- - 35.000+ Lines of Code
- - 45+ React Components mit Fluent UI
- - 120+ Custom PowerShell Cmdlets
- - 80+ GraphQL Endpoints
- - 92% Test Coverage
- - 25+ aktive Enterprise-Tenants
- - 50.000+ verwaltete Ressourcen
-
-
-
-UI/UX Features
-
-
- - Customizable Dashboards mit Drag-and-Drop Widgets
- - Dark Mode mit vollem Theme Support
- - Command Palette für Quick Actions (Ctrl+K)
- - Bulk Operations mit Multi-Select
- - Export nach Excel, PDF und Power BI
- - Mobile Responsive für Tablet-Nutzung
-
-
-
-Zukunftsperspektiven
-
-
Roadmap 2025
-
- - AI-Powered Insights mit Anomalie-Erkennung
- - Copilot Integration für Natural Language Governance
- - Microsoft Fabric Analytics Integration
- - Container Apps für serverlose PowerShell-Execution
- - Zero Trust Security Model Implementation
-
-
-
Vision 2026
-
- - Autonomous Governance mit selbstheilender Umgebung
- - Cross-Cloud Support für AWS/GCP
- - Blockchain-basierte Audit Logs
- - Quantum-Safe Encryption
-
-
-
-Fazit
-
-
- Diese Plattform setzt neue Standards für Power Platform Governance und demonstriert, wie moderne Cloud-Technologien mit Microsoft 365 kombiniert werden können, um Enterprise-Grade Automatisierung und Compliance zu erreichen.
- Die Lösung ermöglicht es Unternehmen, die Balance zwischen Innovation und Kontrolle zu finden, während sie gleichzeitig Kosten senkt und die Sicherheit erhöht.
-
-
\ No newline at end of file
diff --git a/src/pages-vite/portfolio/projects/smart-warehouse.mdx b/src/pages-vite/portfolio/projects/smart-warehouse.mdx
deleted file mode 100644
index 7545e3d..0000000
--- a/src/pages-vite/portfolio/projects/smart-warehouse.mdx
+++ /dev/null
@@ -1,270 +0,0 @@
----
-slug: "smart-warehouse"
-title: "Intelligente Lagerverwaltung mit Computer Vision"
-description: "KI-gestütztes System zur Echtzeit-Bestandsführung und Optimierung von Lagerprozessen"
-excerpt: "Entwicklung eines autonomen Lagerverwaltungssystems mit Computer Vision, IoT-Sensorik und Machine Learning für präzise Bestandsführung."
-date: "2024-06-15"
-category: "AI & Automation"
-coverImage: "/images/projects/smart-warehouse/cover.jpg"
-client: "LogiTech Solutions GmbH"
-duration: "4 Monate"
-url: "https://warehouse-demo.example.com"
-repository: ""
-documentation: "/case-studies/smart-warehouse"
-published: true
-featured: true
-technologies: ["YOLOv8", "Python", "FastAPI", "React", "PostgreSQL", "Docker", "Kubernetes", "IoT", "MQTT"]
-tags: ["Computer Vision", "Machine Learning", "IoT", "Edge Computing", "Automation"]
----
-
-
-
- Ein mittelständischer Logistikdienstleister transformierte seine manuelle Lagerverwaltung durch ein vollautomatisiertes System, das Computer Vision, IoT-Sensorik und Machine Learning kombiniert. Diese Case Study zeigt, wie KI-gestützte Bilderkennung zu 98,7% Genauigkeit bei der Bestandserfassung führte.
-
-
-
-
-
-Die Herausforderung
-
-
- Die manuelle Bestandsführung führte zu erheblichen operativen Problemen:
-
-
- - Diskrepanzen zwischen tatsächlichem und gebuchtem Bestand von bis zu 15%
- - Zeitintensive manuelle Inventuren (3-4 Tage pro Quartal)
- - Fehlende Echtzeit-Transparenz über Lagerbestände
- - Ineffiziente Lagerplatznutzung durch fehlende Optimierung
- - Hohe Personalkosten durch manuelle Prozesse
-
-
-
-Die Lösung
-
-
- Entwicklung eines vollautomatisierten Lagerverwaltungssystems mit folgenden Kernkomponenten:
-
-
- - KI-gestützte Objekterkennung mittels hochauflösender Kameras
- - IoT-Gewichtssensoren zur Validierung
- - Predictive Analytics für Bestandsoptimierung
- - Echtzeit-Dashboard mit mobilem Zugriff
- - Automatische Nachbestellungstrigger
-
-
-
-Systemarchitektur
-
-
-
-{`┌─────────────┐ ┌──────────────┐ ┌────────────┐
-│ Kameras │────▶│ Edge Server │────▶│ ML Cloud │
-└─────────────┘ └──────────────┘ └────────────┘
- │ │ │
- ▼ ▼ ▼
-┌─────────────┐ ┌──────────────┐ ┌────────────┐
-│ IoT Sensoren│────▶│ MQTT Broker │────▶│ API GW │
-└─────────────┘ └──────────────┘ └────────────┘
- │ │
- ▼ ▼
- ┌──────────────┐ ┌────────────┐
- │ PostgreSQL │◀────│ Dashboard │
- └──────────────┘ └────────────┘`}
-
-
-
-
-Computer Vision Pipeline
-
-
-
-{`# Objekterkennung mit YOLOv8
-model = YOLO('yolov8x-custom.pt')
-results = model.predict(
- source=camera_feed,
- conf=0.85,
- save=False,
- stream=True
-)
-
-# Bestandszählung und Klassifizierung
-inventory_count = process_detections(results)
-validate_with_sensors(inventory_count, weight_data)
-
-# Echtzeit-Update der Datenbank
-async def update_inventory(items: List[DetectedItem]):
- async with db.transaction():
- for item in items:
- await db.execute(
- """UPDATE inventory
- SET quantity = $1,
- last_seen = $2,
- confidence = $3
- WHERE sku = $4""",
- item.quantity,
- datetime.now(),
- item.confidence,
- item.sku
- )`}
-
-
-
-
-IoT-Integration
-
-
-
-{`class IoTSensorManager:
- def __init__(self):
- self.mqtt_client = mqtt.Client()
- self.sensors = {}
-
- async def process_weight_data(self, sensor_id: str, weight: float):
- """Validiert CV-Ergebnisse mit Gewichtsdaten"""
- location = self.sensors[sensor_id].location
- expected_items = await self.get_cv_prediction(location)
-
- # Gewichtsvalidierung
- expected_weight = sum(item.weight for item in expected_items)
- variance = abs(weight - expected_weight) / expected_weight
-
- if variance > 0.1: # 10% Toleranz
- await self.trigger_recount(location)
-
- return {
- 'sensor_id': sensor_id,
- 'measured_weight': weight,
- 'expected_weight': expected_weight,
- 'variance': variance,
- 'status': 'valid' if variance <= 0.1 else 'recount_needed'
- }`}
-
-
-
-
-Implementierungsphasen
-
-
Phase 1: Proof of Concept (4 Wochen)
-
- - Entwicklung eines Prototyps für einen Lagerbereich
- - Training des ML-Modells mit 10.000+ annotierten Bildern
- - Integration von 5 Testkameras und 20 IoT-Sensoren
- - Validierung der Erkennungsgenauigkeit
-
-
-
Phase 2: Skalierung (8 Wochen)
-
- - Rollout auf gesamtes Lager (5.000m²)
- - Installation von 45 Kameras und 200+ Sensoren
- - Entwicklung des Echtzeit-Dashboards
- - Integration in bestehendes ERP-System
-
-
-
Phase 3: Optimierung (4 Wochen)
-
- - Fine-Tuning der ML-Modelle
- - Implementierung von Predictive Analytics
- - Mobile App Entwicklung
- - Schulung der Mitarbeiter
-
-
-
-Herausforderungen & Lösungen
-
-
Variierende Lichtverhältnisse
-
- Problem: Schatten und Reflexionen beeinträchtigten die Erkennung
- Lösung: HDR-Kameras + adaptive Bildvorverarbeitung + Augmentierung der Trainingsdaten
-
-
-
Echtzeitverarbeitung großer Datenmengen
-
- Problem: 45 Kameras generierten 2TB Daten/Tag
- Lösung: Edge Computing für Vorverarbeitung + Stream Processing mit Apache Kafka
-
-
-
Integration in Legacy-Systeme
-
- Problem: 20 Jahre altes ERP ohne moderne APIs
- Lösung: Entwicklung eines Adapter-Layers mit bidirektionaler Synchronisation
-
-
-
-Performance Metriken
-
-
-
-{`Performance:
- - Bildverarbeitung: <50ms pro Frame
- - API Response Time: p95 < 100ms
- - System Uptime: 99.95%
- - Datenverarbeitung: 500 Events/Sekunde
-
-Skalierung:
- - Kameras: 45 aktiv, bis 200 möglich
- - IoT Sensoren: 200+
- - Concurrent Users: 50+
- - Datenspeicher: 50TB (3 Jahre Historie)
-
-ML Performance:
- - mAP@50: 0.92
- - Inference Time: 23ms
- - False Positive Rate: <2%
- - Model Size: 138MB`}
-
-
-
-
-Ergebnisse und Auswirkungen
-
-
Quantitative Ergebnisse
-
- - 98,7% Genauigkeit bei der Bestandserfassung
- - 85% Reduktion der Inventurzeit
- - 60% weniger Fehlbestände
- - ROI nach 14 Monaten erreicht
- - 35% Steigerung der Lagerplatznutzung
-
-
-
Qualitative Verbesserungen
-
- - Echtzeit-Transparenz über alle Lagerbestände
- - Proaktive Nachbestellung verhindert Lieferengpässe
- - Mitarbeiter fokussieren sich auf wertschöpfende Tätigkeiten
- - Deutlich reduzierte Fehlerquote
- - Verbesserte Kundenzufriedenheit durch höhere Liefertreue
-
-
-
-Lessons Learned
-
-
- - Edge Computing ist essentiell: Die Vorverarbeitung direkt an der Kamera reduzierte die Netzwerklast um 70%
- - Datenqualität vor Quantität: 1.000 hochqualitative Annotationen waren wertvoller als 10.000 automatisch generierte
- - Iterative Entwicklung: Frühe Pilotierung in einem Bereich ermöglichte schnelle Anpassungen
- - Change Management: Frühzeitige Einbindung der Mitarbeiter war entscheidend für die Akzeptanz
-
-
-
-Zukunftsperspektiven
-
-
- Nächste Entwicklungsstufen:
-
-
- - Integration von Robotik für automatisierte Kommissionierung
- - Erweiterung auf Außenlager und mobile Einheiten
- - KI-basierte Vorhersage von Wartungsbedarf
- - Blockchain-Integration für Supply Chain Transparenz
- - AR-Brillen für Lagermitarbeiter mit visuellen Hinweisen
-
-
-
-Fazit
-
-
- Dieses Projekt demonstriert die erfolgreiche Transformation traditioneller Lagerprozesse durch modernste Computer Vision und IoT-Technologien.
- Die Kombination aus technischer Innovation und praxisorientierter Umsetzung schafft messbaren Mehrwert und ebnet den Weg für die Logistik 4.0.
- Das System wurde als White-Label-Lösung konzipiert und kann mit minimalen Anpassungen in anderen Lagern eingesetzt werden.
-
-
\ No newline at end of file
diff --git a/src/pages-vite/portfolio/projects/website-mit-ki.mdx b/src/pages-vite/portfolio/projects/website-mit-ki.mdx
deleted file mode 100644
index 27e3ad0..0000000
--- a/src/pages-vite/portfolio/projects/website-mit-ki.mdx
+++ /dev/null
@@ -1,168 +0,0 @@
----
-slug: "website-mit-ki"
-title: "Eigene Website mit KI bauen: Von Null zur fertigen Seite"
-description: "Schritt-für-Schritt Anleitung zum Aufbau einer professionellen Website mit Claude Code - DSGVO-konform, mehrsprachig, mit Portfolio und Blog"
-excerpt: "Wie du in unter einer Stunde eine komplette Website erstellst - ohne eine einzige Zeile Code selbst zu schreiben."
-date: "2024-12-13"
-category: "KI & Automatisierung"
-coverImage: "/images/projects/website-mit-ki/cover.jpg"
-client: "Eigenprojekt"
-duration: "30 Minuten"
-url: ""
-repository: ""
-documentation: ""
-published: true
-featured: true
-technologies: ["Claude Code", "React", "TypeScript", "Vercel", "GitHub", "Vite"]
-tags: ["KI", "Website", "No-Code", "Tutorial", "Claude Code", "Vercel"]
-videoUrl: "https://www.tella.tv/video/baue-deine-website-noch-heute-mit-ki-8f7v"
----
-
-
-
-
-
-
-
- Eine professionelle Website ohne Programmierkenntnisse? Mit KI ist das heute möglich. Dieses Tutorial zeigt den kompletten Prozess - von der leeren Projektmappe zur fertigen, gehosteten Website.
-
-
-
-
-
-Was wird gebaut?
-
-
- Die fertige Website enthält alle wichtigen Features einer modernen Webpräsenz:
-
-
- - DSGVO-konforme Implementierung mit Cookie-Banner
- - Mehrsprachigkeit (Deutsch, Englisch, weitere möglich)
- - Über-mich-Seite für persönliche Informationen
- - Portfolio-Seite mit interaktiven Projekten
- - Blog-Funktionalität für Blogposts
- - SEO-Optimierung für bessere Google-Rankings
- - Kontaktformular für potenzielle Kunden
- - Mobile-optimiertes, responsives Design
-
-
-
-Die Vorteile
-
-
Bessere Sichtbarkeit
-
- Durch SEO-Optimierung erscheint die Website direkt unter den ersten Google-Treffern für relevante Suchbegriffe - etwa bei der Suche nach dem eigenen Namen.
-
-
-
Minimale Kosten
-
- Im Gegensatz zu Baukastensystemen wie Wix, WordPress oder Webflow fallen keine monatlichen Gebühren an. Die einzigen Kosten: Ein KI-Abonnement und optional eine eigene Domain.
-
-
-
Volle Kontrolle
-
- Das Projekt liegt versioniert auf GitHub und wird über Vercel kostenlos gehostet. Keine Abhängigkeit von Plattformen, die ihre Preise ändern oder Features einschränken können.
-
-
-
-Die Werkzeuge
-
-
-
-
- | Tool |
- Zweck |
- Kosten |
-
-
-
-
- | Claude Code |
- KI für die komplette Entwicklung |
- Pro-Plan empfohlen |
-
-
- | GitHub |
- Versionierung des Codes |
- Kostenlos |
-
-
- | Vercel |
- Hosting und Deployment |
- Kostenlos |
-
-
- | Domain |
- Eigene URL |
- Optional (~10-15EUR/Jahr) |
-
-
-
-
-
-Der Workflow
-
-
1. Vorbereitung
-
- - GitHub-Account erstellen und neue Repository anlegen
- - Vercel-Account erstellen für späteres Hosting
- - Lokalen Projektordner auf dem Rechner anlegen
- - Relevante Dokumente bereitstellen (Lebenslauf, Anschreiben, etc.)
-
-
-
2. Entwicklung mit Claude Code
-
- - Claude Code öffnen und Projektordner auswählen
- - Prompt mit gewünschten Features eingeben
- - KI arbeitet autonom durch alle Dateien und erstellt die Website
- - Berechtigungen bei Nachfrage erteilen
-
-
-
3. Deployment
-
- - Projekt auf GitHub pushen
- - In Vercel das GitHub-Repository importieren
- - Website wird automatisch gebaut und deployed
- - Bei Build-Fehlern: Logs kopieren und Claude zur Fehlerbehebung nutzen
-
-
-
-Design-Inspiration
-
-
- Für individuelle Designs gibt es mehrere Ansätze:
-
-
- - Awwwards: Preisgekrönte Websites als Inspiration
- - Envato Elements: Website-Templates als Designvorlage - Screenshot anfertigen und Claude zeigen
- - Individuelle Anpassungen: Farben, Bilder und Elemente per Chat-Anweisung ändern
-
-
-
-Änderungen vornehmen
-
-
- Nach dem initialen Setup können jederzeit Änderungen vorgenommen werden:
-
-
- - Bilder austauschen durch Anhängen in Claude Code
- - Texte und Inhalte per Chat-Anweisung ändern
- - Neue Seiten oder Funktionen hinzufügen
- - Nach jeder Änderung: Push zu GitHub, automatisches Redeployment auf Vercel
-
-
-
-Fazit
-
-
- Eine komplette, professionelle Website lässt sich heute in unter einer Stunde aufbauen - ohne eine einzige Zeile Code selbst zu schreiben. Die Kombination aus KI-Entwicklung, kostenlosem Hosting und Versionierung macht diese Lösung nicht nur für Entwickler interessant, sondern für alle, die eine professionelle Webpräsenz benötigen: Selbstständige, Unternehmen oder Projekte.
-
-
- Der Kostenfaktor ist minimal, die Kontrolle maximal - und das Ergebnis kann sich mit jeder professionell entwickelten Website messen.
-
-
diff --git a/src/pages-vite/portfolio/types.ts b/src/pages-vite/portfolio/types.ts
deleted file mode 100644
index dcbbf2d..0000000
--- a/src/pages-vite/portfolio/types.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-// pages/portfolio/types.ts
-import { ReactNode } from 'react';
-
-export interface Project {
- title: string;
- description: string;
- image: string;
- tags: string[];
- slug: string;
- date: string;
- content: ReactNode;
- client?: string;
- technologies?: string[];
- links?: {
- github?: string;
- live?: string;
- };
-}
\ No newline at end of file
diff --git a/src/pages-vite/portfolio/utils.ts b/src/pages-vite/portfolio/utils.ts
deleted file mode 100644
index 1da3e71..0000000
--- a/src/pages-vite/portfolio/utils.ts
+++ /dev/null
@@ -1,205 +0,0 @@
-// src/pages/portfolio/utils.ts
-import { type ComponentType } from 'react';
-
-export interface Project {
- slug: string;
- title: string;
- description: string;
- excerpt: string;
- date: string;
- category: string;
- coverImage: string;
- client: string;
- duration: string;
- url?: string;
- repository?: string;
- documentation?: string;
- published: boolean;
- featured: boolean;
- technologies: string[];
- tags: string[];
-}
-
-export type ProjectWithContent = Project & {
- content: ComponentType;
-};
-
-interface ProjectModule {
- frontmatter: unknown;
- default: ComponentType;
-}
-
-/**
- * Validiert, ob das übergebene Frontmatter den Anforderungen eines Project entspricht.
- */
-const validateProjectFrontmatter = (frontmatter: unknown): frontmatter is Project => {
- if (!frontmatter || typeof frontmatter !== 'object') return false;
- const fm = frontmatter as Record;
-
- const requiredFields: (keyof Project)[] = [
- 'slug',
- 'title',
- 'description',
- 'excerpt',
- 'date',
- 'category',
- 'coverImage',
- 'client',
- 'duration',
- 'published',
- 'featured',
- 'technologies',
- 'tags'
- ];
-
- const missingFields = requiredFields.filter(field => !(field in fm));
- if (missingFields.length > 0) {
- console.warn(`Missing required fields in project frontmatter: ${missingFields.join(', ')}`);
- return false;
- }
-
- // Validierungen: Bei Feldern, die Arrays sein sollen, prüfen wir, ob jedes Element vom erwarteten Typ ist.
- const validations = [
- { field: 'slug', type: 'string' },
- { field: 'title', type: 'string' },
- { field: 'description', type: 'string' },
- { field: 'excerpt', type: 'string' },
- { field: 'date', type: 'string' },
- { field: 'category', type: 'string' },
- { field: 'coverImage', type: 'string' },
- { field: 'client', type: 'string' },
- { field: 'duration', type: 'string' },
- { field: 'published', type: 'boolean' },
- { field: 'featured', type: 'boolean' },
- { field: 'technologies', type: 'object', arrayOf: 'string' },
- { field: 'tags', type: 'object', arrayOf: 'string' }
- ];
-
- for (const validation of validations) {
- const value = fm[validation.field];
- if (validation.type === 'object' && validation.arrayOf) {
- if (!Array.isArray(value) || !value.every(item => typeof item === validation.arrayOf)) {
- console.warn(`Invalid type for ${validation.field}. Expected array of ${validation.arrayOf}`);
- return false;
- }
- } else if (typeof value !== validation.type) {
- console.warn(`Invalid type for ${validation.field}. Expected ${validation.type}`);
- return false;
- }
- }
-
- return true;
-};
-
-export const getAllProjects = async (): Promise => {
- try {
- const modules = import.meta.glob('./projects/*.mdx', { eager: true }) as Record;
- console.log('Found project files:', Object.keys(modules));
-
- const projects = Object.entries(modules)
- .map(([path, module]: [string, ProjectModule]) => {
- console.log('Processing:', path);
- const frontmatter = module.frontmatter;
- if (!validateProjectFrontmatter(frontmatter)) {
- console.warn(`Invalid or missing frontmatter in project: ${path}`);
- return null;
- }
- const fm = frontmatter as Project;
-
- // Cast fm zuerst zu unknown und dann zu Record
- const fmAny = fm as unknown as Record;
- if ('image' in fmAny && !fm.coverImage) {
- fm.coverImage = fmAny['image'] as string;
- delete fmAny['image'];
- }
- return fm;
- })
- .filter((project): project is Project => project !== null && project.published);
-
- // Sortiere nach Datum absteigend (neueste zuerst)
- return projects.sort((a, b) =>
- new Date(b.date).getTime() - new Date(a.date).getTime()
- );
- } catch (error) {
- console.error('Error loading projects:', error);
- return [];
- }
-};
-
-export const getFeaturedProjects = async (): Promise => {
- const projects = await getAllProjects();
- return projects.filter(project => project.featured);
-};
-
-export const getProjectsByCategory = async (category: string): Promise => {
- const projects = await getAllProjects();
- return projects.filter(project => project.category === category);
-};
-
-export const getProjectBySlug = async (slug: string): Promise => {
- if (!slug) return null;
-
- try {
- const modules = import.meta.glob('./projects/*.mdx', { eager: true }) as Record;
- const projectEntry = Object.entries(modules).find(([path]) =>
- path.includes(`${slug}.mdx`)
- );
-
- if (!projectEntry) {
- console.warn(`Project not found: ${slug}`);
- return null;
- }
-
- const module = projectEntry[1];
- const frontmatter = module.frontmatter;
- if (!validateProjectFrontmatter(frontmatter)) {
- console.warn(`Invalid frontmatter in project: ${slug}`);
- return null;
- }
- const fm = frontmatter as Project;
- if (!fm.published) {
- console.warn(`Attempting to access unpublished project: ${slug}`);
- return null;
- }
-
- const fmAny = fm as unknown as Record;
- if ('image' in fmAny && !fm.coverImage) {
- fm.coverImage = fmAny['image'] as string;
- delete fmAny['image'];
- }
-
- return {
- ...fm,
- content: module.default
- };
- } catch (error) {
- console.error(`Error loading project: ${slug}`, error);
- return null;
- }
-};
-
-export const getRelatedProjects = async (
- currentSlug: string,
- tags: string[],
- limit: number = 3
-): Promise => {
- const allProjects = await getAllProjects();
-
- return allProjects
- .filter(project =>
- project.slug !== currentSlug &&
- project.tags.some(tag => tags.includes(tag))
- )
- .sort((a, b) => {
- const aMatches = a.tags.filter(tag => tags.includes(tag)).length;
- const bMatches = b.tags.filter(tag => tags.includes(tag)).length;
- return bMatches - aMatches;
- })
- .slice(0, limit);
-};
-
-export const getAllProjectCategories = async (): Promise => {
- const projects = await getAllProjects();
- const categories = new Set(projects.map(project => project.category));
- return Array.from(categories).sort();
-};
diff --git a/src/utils/analytics.ts b/src/utils/analytics.ts
index 6fa8535..4f268d8 100644
--- a/src/utils/analytics.ts
+++ b/src/utils/analytics.ts
@@ -1,9 +1,16 @@
+/**
+ * Analytics Service
+ * Zentrale Verwaltung von Google Analytics, Facebook Pixel und Cookie-basiertem Tracking
+ */
+
import ReactGA from 'react-ga4';
const TRACKING_ID = 'G-N0PZBL7X18';
const FB_PIXEL_ID = 'XXXXXXXXXXXXXXXXX'; // Ersetze mit deiner Facebook Pixel ID
-// Prüft, ob eine bestimmte Cookie-Kategorie aktiviert ist
+/**
+ * Prüft, ob eine bestimmte Cookie-Kategorie aktiviert ist
+ */
export const isCookieCategoryEnabled = (category: string): boolean => {
try {
const cookieSettings = localStorage.getItem('cookieSettings');
@@ -17,7 +24,9 @@ export const isCookieCategoryEnabled = (category: string): boolean => {
}
};
-// Entfernt alle Google Analytics Cookies
+/**
+ * Entfernt alle Google Analytics Cookies
+ */
const removeGACookies = (): void => {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
@@ -33,7 +42,9 @@ const removeGACookies = (): void => {
}
};
-// Google Analytics Initialisierung - mit strikter Zustimmungsprüfung
+/**
+ * Initialisiert Google Analytics mit strikter Zustimmungsprüfung
+ */
export const initGA = (): void => {
if (!isCookieCategoryEnabled('analytics')) {
// Entferne bestehende GA-Cookies, falls keine Zustimmung
@@ -49,6 +60,9 @@ export const initGA = (): void => {
});
};
+/**
+ * Loggt einen Seitenaufruf in Google Analytics
+ */
export const logPageView = (): void => {
if (!isCookieCategoryEnabled('analytics')) {
return;
@@ -61,6 +75,9 @@ export const logPageView = (): void => {
});
};
+/**
+ * Trackt ein benutzerdefiniertes Event
+ */
export const trackEvent = (category: string, action: string, label?: string): void => {
if (!isCookieCategoryEnabled('analytics')) {
return;
@@ -73,6 +90,9 @@ export const trackEvent = (category: string, action: string, label?: string): vo
});
};
+/**
+ * Trackt eine Conversion
+ */
export const trackConversion = (value?: number): void => {
if (!isCookieCategoryEnabled('analytics')) {
return;
@@ -85,6 +105,9 @@ export const trackConversion = (value?: number): void => {
});
};
+/**
+ * Trackt Performance-Timing
+ */
export const trackTiming = (category: string, variable: string, value: number): void => {
if (!isCookieCategoryEnabled('analytics')) {
return;
@@ -98,7 +121,9 @@ export const trackTiming = (category: string, variable: string, value: number):
});
};
-// Facebook Pixel Initialisierung - nur mit Zustimmung
+/**
+ * Initialisiert Facebook Pixel nur mit Zustimmung
+ */
export const initFBPixel = (): void => {
if (!isCookieCategoryEnabled('marketing')) {
return;
@@ -122,7 +147,9 @@ export const initFBPixel = (): void => {
}
};
-// Facebook Event tracking
+/**
+ * Trackt Facebook Pixel Events
+ */
export const trackFBEvent = (event: string, params?: Record): void => {
if (!isCookieCategoryEnabled('marketing') || typeof window === 'undefined' || !window.fbq) {
return;
@@ -131,7 +158,9 @@ export const trackFBEvent = (event: string, params?: Record): v
window.fbq('track', event, params);
};
-// Google Fonts Handling - nur mit funktionaler Cookie-Zustimmung
+/**
+ * Initialisiert Google Fonts nur mit funktionaler Cookie-Zustimmung
+ */
export const initGoogleFonts = (): void => {
if (!isCookieCategoryEnabled('functional')) {
// Entferne bestehende Google Fonts
@@ -151,7 +180,9 @@ export const initGoogleFonts = (): void => {
// Code hier zur lokalen Einbindung der Schriftarten
};
-// Funktionale Cookies initialisieren
+/**
+ * Initialisiert funktionale Cookies und Services
+ */
export const initFunctionalCookies = (): void => {
if (!isCookieCategoryEnabled('functional')) {
return;
diff --git a/src/utils/api.ts b/src/utils/api.ts
index 53326d6..b244e06 100644
--- a/src/utils/api.ts
+++ b/src/utils/api.ts
@@ -1,9 +1,25 @@
+/**
+ * API Utility Functions
+ * Provides enhanced fetch functionality with timeout support
+ */
+
import { handleError, createAPIError } from './errorHandling';
+/**
+ * Extended request options with timeout support
+ */
interface RequestOptions extends RequestInit {
+ /** Request timeout in milliseconds (default: 8000ms) */
timeout?: number;
}
+/**
+ * Fetch with automatic timeout and error handling
+ * @param resource - URL or resource to fetch
+ * @param options - Request options including optional timeout
+ * @returns Promise resolving to the fetch Response
+ * @throws {Error} On timeout, network error, or HTTP error status
+ */
export async function fetchWithTimeout(
resource: string,
options: RequestOptions = {}
diff --git a/src/utils/auth.ts b/src/utils/auth.ts
index c57b754..3af825e 100644
--- a/src/utils/auth.ts
+++ b/src/utils/auth.ts
@@ -1,17 +1,33 @@
+/**
+ * Authentication Utility Functions
+ * Handles user authentication checks and sign out operations
+ */
+
import { supabase } from './supabaseClient';
import { NavigateFunction } from 'react-router-dom';
+/**
+ * Check if user is authenticated
+ * Verifies the current session and redirects to login if not authenticated
+ * @param {NavigateFunction} navigate - React Router navigate function for redirects
+ * @returns {Promise} True if authenticated, false otherwise
+ */
export const checkAuth = async (navigate: NavigateFunction) => {
const { data: { session } } = await supabase.auth.getSession();
-
+
if (!session) {
navigate('/login');
return false;
}
-
+
return true;
};
+/**
+ * Sign out current user
+ * Terminates the user session and redirects to login page
+ * @param {NavigateFunction} navigate - React Router navigate function for redirects
+ */
export const signOut = async (navigate: NavigateFunction) => {
await supabase.auth.signOut();
navigate('/login');
diff --git a/src/utils/cache.test.ts b/src/utils/cache.test.ts
new file mode 100644
index 0000000..230b0ac
--- /dev/null
+++ b/src/utils/cache.test.ts
@@ -0,0 +1,282 @@
+import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
+import { cache } from './cache';
+
+describe('Cache', () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ cache.clear(); // Clear cache before each test
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ cache.clear(); // Clean up after each test
+ });
+
+ describe('set and get', () => {
+ it('should set and retrieve a value', () => {
+ cache.set('key', 'value');
+ expect(cache.get('key')).toBe('value');
+ });
+
+ it('should set multiple values with different keys', () => {
+ cache.set('key1', 'value1');
+ cache.set('key2', 'value2');
+ cache.set('key3', 'value3');
+
+ expect(cache.get('key1')).toBe('value1');
+ expect(cache.get('key2')).toBe('value2');
+ expect(cache.get('key3')).toBe('value3');
+ });
+
+ it('should overwrite existing value for same key', () => {
+ cache.set('key', 'value1');
+ expect(cache.get('key')).toBe('value1');
+
+ cache.set('key', 'value2');
+ expect(cache.get('key')).toBe('value2');
+ });
+
+ it('should return null for non-existing key', () => {
+ expect(cache.get('nonexistent')).toBeNull();
+ });
+
+ it('should store different types of values', () => {
+ cache.set('string', 'text');
+ cache.set('number', 42);
+ cache.set('boolean', true);
+ cache.set('object', { name: 'test' });
+ cache.set('array', [1, 2, 3]);
+ cache.set('null', null);
+
+ expect(cache.get('string')).toBe('text');
+ expect(cache.get('number')).toBe(42);
+ expect(cache.get('boolean')).toBe(true);
+ expect(cache.get('object')).toEqual({ name: 'test' });
+ expect(cache.get('array')).toEqual([1, 2, 3]);
+ expect(cache.get('null')).toBeNull();
+ });
+ });
+
+ describe('TTL expiration', () => {
+ it('should use default TTL of 5 minutes', () => {
+ cache.set('test', 'value');
+
+ // Should be available before TTL expires
+ expect(cache.get('test')).toBe('value');
+
+ // Advance time by 4 minutes 59 seconds
+ vi.advanceTimersByTime(4 * 60 * 1000 + 59 * 1000);
+ expect(cache.get('test')).toBe('value');
+
+ // Advance time by 2 more seconds (total 5 minutes 1 second)
+ vi.advanceTimersByTime(2 * 1000);
+ expect(cache.get('test')).toBeNull();
+ });
+
+ it('should accept custom TTL per item', () => {
+ cache.set('short', 'value', 1000); // 1 second TTL
+ cache.set('long', 'value', 10000); // 10 seconds TTL
+
+ // Both should be available initially
+ expect(cache.get('short')).toBe('value');
+ expect(cache.get('long')).toBe('value');
+
+ // After 1.5 seconds, short should expire but long should remain
+ vi.advanceTimersByTime(1500);
+ expect(cache.get('short')).toBeNull();
+ expect(cache.get('long')).toBe('value');
+
+ // After 9 more seconds (total 10.5), long should expire
+ vi.advanceTimersByTime(9000);
+ expect(cache.get('long')).toBeNull();
+ });
+
+ it('should return null for expired item', () => {
+ cache.set('key', 'value', 1000);
+
+ // Should be available initially
+ expect(cache.get('key')).toBe('value');
+
+ // Should be null after expiration
+ vi.advanceTimersByTime(1001);
+ expect(cache.get('key')).toBeNull();
+ });
+
+ it('should delete expired item from storage', () => {
+ cache.set('key', 'value', 1000);
+
+ // Item should exist
+ expect(cache.get('key')).toBe('value');
+
+ // After expiration, get should delete it
+ vi.advanceTimersByTime(1001);
+ expect(cache.get('key')).toBeNull();
+
+ // Subsequent get should also return null (item was deleted)
+ expect(cache.get('key')).toBeNull();
+ });
+
+ it('should return value at exact TTL boundary', () => {
+ cache.set('key', 'value', 1000);
+
+ // At exactly TTL time, should still be valid
+ vi.advanceTimersByTime(1000);
+ expect(cache.get('key')).toBe('value');
+
+ // One millisecond later, should expire
+ vi.advanceTimersByTime(1);
+ expect(cache.get('key')).toBeNull();
+ });
+
+ it('should respect different TTLs for different items', () => {
+ cache.set('item1', 'value1', 1000);
+ cache.set('item2', 'value2', 2000);
+ cache.set('item3', 'value3', 3000);
+
+ // All items should be available initially
+ expect(cache.get('item1')).toBe('value1');
+ expect(cache.get('item2')).toBe('value2');
+ expect(cache.get('item3')).toBe('value3');
+
+ // After 1.5 seconds, item1 should expire
+ vi.advanceTimersByTime(1500);
+ expect(cache.get('item1')).toBeNull();
+ expect(cache.get('item2')).toBe('value2');
+ expect(cache.get('item3')).toBe('value3');
+
+ // After 1 more second (total 2.5), item2 should expire
+ vi.advanceTimersByTime(1000);
+ expect(cache.get('item1')).toBeNull();
+ expect(cache.get('item2')).toBeNull();
+ expect(cache.get('item3')).toBe('value3');
+
+ // After 1 more second (total 3.5), item3 should expire
+ vi.advanceTimersByTime(1000);
+ expect(cache.get('item1')).toBeNull();
+ expect(cache.get('item2')).toBeNull();
+ expect(cache.get('item3')).toBeNull();
+ });
+
+ it('should reset TTL when item is overwritten', () => {
+ cache.set('key', 'value1', 1000);
+
+ // Advance time by 500ms
+ vi.advanceTimersByTime(500);
+
+ // Overwrite with new value and new TTL
+ cache.set('key', 'value2', 2000);
+
+ // After 1500ms more (total 2000ms from first set), should still be available
+ // because TTL was reset
+ vi.advanceTimersByTime(1500);
+ expect(cache.get('key')).toBe('value2');
+
+ // After 600ms more (2100ms from second set), should expire
+ vi.advanceTimersByTime(600);
+ expect(cache.get('key')).toBeNull();
+ });
+
+ it('should handle zero TTL', () => {
+ cache.set('key', 'value', 0);
+
+ // Should expire immediately
+ vi.advanceTimersByTime(1);
+ expect(cache.get('key')).toBeNull();
+ });
+
+ it('should handle very long TTL', () => {
+ const oneDayInMs = 24 * 60 * 60 * 1000;
+ cache.set('key', 'value', oneDayInMs);
+
+ // Should be available after 23 hours
+ vi.advanceTimersByTime(23 * 60 * 60 * 1000);
+ expect(cache.get('key')).toBe('value');
+
+ // Should still be available at exactly 24 hours
+ vi.advanceTimersByTime(60 * 60 * 1000);
+ expect(cache.get('key')).toBe('value');
+
+ // Should expire after 24 hours + 1ms
+ vi.advanceTimersByTime(1);
+ expect(cache.get('key')).toBeNull();
+ });
+ });
+
+ describe('has', () => {
+ it('should return true for existing key', () => {
+ cache.set('key', 'value');
+ expect(cache.has('key')).toBe(true);
+ });
+
+ it('should return false for non-existing key', () => {
+ expect(cache.has('nonexistent')).toBe(false);
+ });
+
+ it('should return false for expired key', () => {
+ cache.set('key', 'value', 1000);
+
+ expect(cache.has('key')).toBe(true);
+
+ vi.advanceTimersByTime(1001);
+ expect(cache.has('key')).toBe(false);
+ });
+
+ it('should return false for null values', () => {
+ cache.set('key', null);
+ expect(cache.has('key')).toBe(false);
+ });
+ });
+
+ describe('delete', () => {
+ it('should delete existing key', () => {
+ cache.set('key', 'value');
+ expect(cache.get('key')).toBe('value');
+
+ cache.delete('key');
+ expect(cache.get('key')).toBeNull();
+ });
+
+ it('should handle deleting non-existing key', () => {
+ expect(() => cache.delete('nonexistent')).not.toThrow();
+ expect(cache.get('nonexistent')).toBeNull();
+ });
+
+ it('should only delete specified key', () => {
+ cache.set('key1', 'value1');
+ cache.set('key2', 'value2');
+ cache.set('key3', 'value3');
+
+ cache.delete('key2');
+
+ expect(cache.get('key1')).toBe('value1');
+ expect(cache.get('key2')).toBeNull();
+ expect(cache.get('key3')).toBe('value3');
+ });
+ });
+
+ describe('clear', () => {
+ it('should clear all items from cache', () => {
+ cache.set('key1', 'value1');
+ cache.set('key2', 'value2');
+ cache.set('key3', 'value3');
+
+ cache.clear();
+
+ expect(cache.get('key1')).toBeNull();
+ expect(cache.get('key2')).toBeNull();
+ expect(cache.get('key3')).toBeNull();
+ });
+
+ it('should handle clearing empty cache', () => {
+ expect(() => cache.clear()).not.toThrow();
+ });
+
+ it('should allow setting items after clear', () => {
+ cache.set('key', 'value1');
+ cache.clear();
+ cache.set('key', 'value2');
+
+ expect(cache.get('key')).toBe('value2');
+ });
+ });
+});
diff --git a/src/utils/cache.ts b/src/utils/cache.ts
index d04a660..a742d6e 100644
--- a/src/utils/cache.ts
+++ b/src/utils/cache.ts
@@ -1,50 +1,132 @@
+/**
+ * In-Memory Cache Utility
+ * Provides simple key-value caching with TTL (Time To Live) support
+ */
+
+/**
+ * Cache item structure with value, timestamp, and TTL
+ */
type CacheItem = {
value: T;
timestamp: number;
ttl: number;
+ lastAccessed: number;
};
+/**
+ * In-memory cache implementation with automatic expiration
+ */
class Cache {
private storage: Map>;
private readonly defaultTTL: number;
+ private readonly maxSize?: number;
+<<<<<<< HEAD
+ /**
+ * Create a new cache instance
+ * @param defaultTTL - Default time-to-live in milliseconds (default: 5 minutes)
+ */
constructor(defaultTTL = 5 * 60 * 1000) { // 5 minutes default TTL
+=======
+ constructor(defaultTTL = 5 * 60 * 1000, maxSize?: number) { // 5 minutes default TTL
+>>>>>>> origin/master
this.storage = new Map();
this.defaultTTL = defaultTTL;
+ this.maxSize = maxSize;
}
+ /**
+ * Store a value in cache with optional TTL
+ * @param key - Cache key
+ * @param value - Value to cache
+ * @param ttl - Time-to-live in milliseconds (uses default if not specified)
+ */
set(key: string, value: T, ttl = this.defaultTTL): void {
+ const now = Date.now();
+
+ // If maxSize is set and cache is full, evict least recently used item
+ if (this.maxSize && this.storage.size >= this.maxSize && !this.storage.has(key)) {
+ this.evictLRU();
+ }
+
this.storage.set(key, {
value,
- timestamp: Date.now(),
- ttl
+ timestamp: now,
+ ttl,
+ lastAccessed: now
});
}
+<<<<<<< HEAD
+ /**
+ * Retrieve a value from cache
+ * Returns null if key doesn't exist or has expired
+ * @param key - Cache key
+ * @returns Cached value or null
+ */
+=======
+ private evictLRU(): void {
+ let lruKey: string | null = null;
+ let lruTime = Infinity;
+
+ for (const [key, item] of this.storage.entries()) {
+ if (item.lastAccessed < lruTime) {
+ lruTime = item.lastAccessed;
+ lruKey = key;
+ }
+ }
+
+ if (lruKey) {
+ this.storage.delete(lruKey);
+ }
+ }
+
+>>>>>>> origin/master
get(key: string): T | null {
const item = this.storage.get(key);
-
+
if (!item) return null;
-
+
if (Date.now() > item.timestamp + item.ttl) {
this.storage.delete(key);
return null;
}
-
+
+<<<<<<< HEAD
+=======
+ // Update last accessed time for LRU tracking
+ item.lastAccessed = Date.now();
+
+>>>>>>> origin/master
return item.value;
}
+ /**
+ * Check if a key exists and is not expired
+ * @param key - Cache key
+ * @returns True if key exists and is valid
+ */
has(key: string): boolean {
return this.get(key) !== null;
}
+ /**
+ * Remove a specific key from cache
+ * @param key - Cache key to delete
+ */
delete(key: string): void {
this.storage.delete(key);
}
+ /**
+ * Clear all cached items
+ */
clear(): void {
this.storage.clear();
}
}
-export const cache = new Cache();
\ No newline at end of file
+/**
+ * Shared cache instance with 5-minute default TTL
+ */
+export const cache = new Cache();
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index c82ad9b..bf34e12 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -1,6 +1,12 @@
-// src/utils/constants.ts
-// Zentrale Konstanten für die gesamte Website
+/**
+ * Application Constants
+ * Zentrale Konstanten für die gesamte Website
+ */
+/**
+ * Contact information
+ * Kontaktinformationen für alle Kontaktpunkte auf der Website
+ */
export const CONTACT = {
email: "info@damjan-savic.com",
phone: "+49 175 695 0979",
@@ -14,12 +20,20 @@ export const CONTACT = {
}
} as const;
+/**
+ * Current professional role
+ * Aktuelle berufliche Position und Unternehmen
+ */
export const CURRENT_ROLE = {
company: "Everlast Consulting GmbH",
position: "Process Automation Specialist",
since: "2024-12"
} as const;
+/**
+ * Profile information
+ * Profil- und persönliche Informationen für die gesamte Website
+ */
export const PROFILE = {
name: "Damjan Savić",
title: "Fullstack Developer",
diff --git a/src/utils/csrf.ts b/src/utils/csrf.ts
index 98f24f0..7430a6d 100644
--- a/src/utils/csrf.ts
+++ b/src/utils/csrf.ts
@@ -1,20 +1,31 @@
+/**
+ * CSRF Token Utility
+ * Verwaltung von Cross-Site Request Forgery Schutz-Tokens
+ */
+
import { v4 as uuidv4 } from 'uuid';
// CSRF token storage
let csrfToken: string | null = null;
-// Generate a new CSRF token
+/**
+ * Generate a new CSRF token
+ */
export const generateCsrfToken = (): string => {
csrfToken = uuidv4();
return csrfToken;
};
-// Validate a CSRF token
+/**
+ * Validate a CSRF token
+ */
export const validateCsrfToken = (token: string): boolean => {
return token === csrfToken;
};
-// Get the current CSRF token
+/**
+ * Get the current CSRF token
+ */
export const getCsrfToken = (): string => {
if (!csrfToken) {
return generateCsrfToken();
diff --git a/src/utils/errorHandling.test.ts b/src/utils/errorHandling.test.ts
new file mode 100644
index 0000000..e79e349
--- /dev/null
+++ b/src/utils/errorHandling.test.ts
@@ -0,0 +1,165 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { AppError, errorCodes, handleError, createAPIError } from './errorHandling';
+import * as analytics from './analytics';
+
+// Mock the analytics module
+vi.mock('./analytics', () => ({
+ trackEvent: vi.fn()
+}));
+
+describe('errorHandling', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe('AppError', () => {
+ it('should create an AppError with all properties', () => {
+ const error = new AppError('Test error', 'TEST_CODE', 'error', { key: 'value' });
+
+ expect(error).toBeInstanceOf(Error);
+ expect(error).toBeInstanceOf(AppError);
+ expect(error.message).toBe('Test error');
+ expect(error.code).toBe('TEST_CODE');
+ expect(error.severity).toBe('error');
+ expect(error.metadata).toEqual({ key: 'value' });
+ expect(error.name).toBe('AppError');
+ });
+
+ it('should create an AppError with default severity', () => {
+ const error = new AppError('Test error', 'TEST_CODE');
+
+ expect(error.severity).toBe('error');
+ expect(error.metadata).toBeUndefined();
+ });
+
+ it('should create an AppError with warning severity', () => {
+ const error = new AppError('Test warning', 'TEST_CODE', 'warning');
+
+ expect(error.severity).toBe('warning');
+ });
+
+ it('should create an AppError with info severity', () => {
+ const error = new AppError('Test info', 'TEST_CODE', 'info');
+
+ expect(error.severity).toBe('info');
+ });
+ });
+
+ describe('errorCodes', () => {
+ it('should have all error codes defined', () => {
+ expect(errorCodes.NETWORK_ERROR).toBe('ERR_NETWORK');
+ expect(errorCodes.API_ERROR).toBe('ERR_API');
+ expect(errorCodes.AUTH_ERROR).toBe('ERR_AUTH');
+ expect(errorCodes.VALIDATION_ERROR).toBe('ERR_VALIDATION');
+ expect(errorCodes.UNKNOWN_ERROR).toBe('ERR_UNKNOWN');
+ });
+ });
+
+ describe('handleError', () => {
+ it('should handle AppError and return it unchanged', () => {
+ const originalError = new AppError('Test error', 'TEST_CODE', 'error', { foo: 'bar' });
+ const result = handleError(originalError, 'test-context');
+
+ expect(result).toBe(originalError);
+ expect(result.message).toBe('Test error');
+ expect(result.code).toBe('TEST_CODE');
+ expect(result.severity).toBe('error');
+ expect(result.metadata).toEqual({ foo: 'bar' });
+ expect(analytics.trackEvent).toHaveBeenCalledWith(
+ 'Error',
+ 'TEST_CODE',
+ 'test-context: Test error'
+ );
+ });
+
+ it('should convert regular Error to AppError', () => {
+ const originalError = new Error('Regular error');
+ const result = handleError(originalError, 'test-context');
+
+ expect(result).toBeInstanceOf(AppError);
+ expect(result.message).toBe('Regular error');
+ expect(result.code).toBe(errorCodes.UNKNOWN_ERROR);
+ expect(result.severity).toBe('error');
+ expect(result.metadata).toEqual({ originalError });
+ expect(analytics.trackEvent).toHaveBeenCalledWith(
+ 'Error',
+ errorCodes.UNKNOWN_ERROR,
+ 'test-context: Regular error'
+ );
+ });
+
+ it('should handle unknown error types', () => {
+ const result = handleError('string error', 'test-context');
+
+ expect(result).toBeInstanceOf(AppError);
+ expect(result.message).toBe('An unexpected error occurred');
+ expect(result.code).toBe(errorCodes.UNKNOWN_ERROR);
+ expect(result.severity).toBe('error');
+ expect(result.metadata).toEqual({ originalError: 'string error' });
+ expect(analytics.trackEvent).toHaveBeenCalledWith(
+ 'Error',
+ errorCodes.UNKNOWN_ERROR,
+ 'test-context: An unexpected error occurred'
+ );
+ });
+
+ it('should handle null error', () => {
+ const result = handleError(null, 'test-context');
+
+ expect(result).toBeInstanceOf(AppError);
+ expect(result.message).toBe('An unexpected error occurred');
+ expect(result.code).toBe(errorCodes.UNKNOWN_ERROR);
+ expect(result.metadata).toEqual({ originalError: null });
+ });
+
+ it('should handle undefined error', () => {
+ const result = handleError(undefined, 'test-context');
+
+ expect(result).toBeInstanceOf(AppError);
+ expect(result.message).toBe('An unexpected error occurred');
+ expect(result.code).toBe(errorCodes.UNKNOWN_ERROR);
+ expect(result.metadata).toEqual({ originalError: undefined });
+ });
+
+ it('should track error in analytics', () => {
+ const error = new AppError('Analytics test', 'ANALYTICS_CODE');
+ handleError(error, 'analytics-context');
+
+ expect(analytics.trackEvent).toHaveBeenCalledTimes(1);
+ expect(analytics.trackEvent).toHaveBeenCalledWith(
+ 'Error',
+ 'ANALYTICS_CODE',
+ 'analytics-context: Analytics test'
+ );
+ });
+ });
+
+ describe('createAPIError', () => {
+ it('should create an API error with status code', () => {
+ const error = createAPIError(404, 'Not found');
+
+ expect(error).toBeInstanceOf(AppError);
+ expect(error.message).toBe('Not found');
+ expect(error.code).toBe(errorCodes.API_ERROR);
+ expect(error.severity).toBe('error');
+ expect(error.metadata).toEqual({ status: 404 });
+ });
+
+ it('should create an API error for 500 status', () => {
+ const error = createAPIError(500, 'Internal server error');
+
+ expect(error).toBeInstanceOf(AppError);
+ expect(error.message).toBe('Internal server error');
+ expect(error.code).toBe(errorCodes.API_ERROR);
+ expect(error.metadata).toEqual({ status: 500 });
+ });
+
+ it('should create an API error for 401 status', () => {
+ const error = createAPIError(401, 'Unauthorized');
+
+ expect(error).toBeInstanceOf(AppError);
+ expect(error.message).toBe('Unauthorized');
+ expect(error.metadata).toEqual({ status: 401 });
+ });
+ });
+});
diff --git a/src/utils/errorHandling.ts b/src/utils/errorHandling.ts
index 3a6e561..4a8689f 100644
--- a/src/utils/errorHandling.ts
+++ b/src/utils/errorHandling.ts
@@ -1,5 +1,13 @@
+/**
+ * Error Handling Utilities
+ * Centralized error handling and custom error types
+ */
+
import { trackEvent } from './analytics';
+/**
+ * Custom application error class with error codes, severity levels, and metadata
+ */
export class AppError extends Error {
constructor(
message: string,
@@ -12,6 +20,9 @@ export class AppError extends Error {
}
}
+/**
+ * Standard error codes used throughout the application
+ */
export const errorCodes = {
NETWORK_ERROR: 'ERR_NETWORK',
API_ERROR: 'ERR_API',
@@ -20,6 +31,9 @@ export const errorCodes = {
UNKNOWN_ERROR: 'ERR_UNKNOWN'
} as const;
+/**
+ * Handle and normalize errors with tracking and logging
+ */
export const handleError = (error: unknown, context: string) => {
let appError: AppError;
@@ -52,6 +66,9 @@ export const handleError = (error: unknown, context: string) => {
return appError;
};
+/**
+ * Create an API error with HTTP status code
+ */
export const createAPIError = (status: number, message: string) => {
return new AppError(
message,
diff --git a/src/utils/fontLoader.ts b/src/utils/fontLoader.ts
index 3aab35f..f04ec64 100644
--- a/src/utils/fontLoader.ts
+++ b/src/utils/fontLoader.ts
@@ -1,4 +1,13 @@
-// Font loading utility to ensure fonts are loaded before showing content
+/**
+ * Font Loading Utility
+ * Ensures fonts are loaded before showing content to prevent FOUT/FOIT
+ */
+
+/**
+ * Preload fonts asynchronously
+ * Uses the Font Loading API to ensure Inter font is loaded
+ * Falls back to system fonts on error
+ */
export const preloadFonts = async () => {
if ('fonts' in document) {
try {
@@ -12,14 +21,18 @@ export const preloadFonts = async () => {
}
};
-// Call this function as early as possible
+/**
+ * Initialize font loading process
+ * Should be called as early as possible in the application lifecycle
+ * Adds loading state and fallback timeout to prevent indefinite loading
+ */
export const initializeFonts = () => {
// Add loading class immediately
document.documentElement.classList.add('fonts-loading');
-
+
// Start font loading
preloadFonts();
-
+
// Remove loading class after a timeout to prevent indefinite loading state
setTimeout(() => {
document.documentElement.classList.remove('fonts-loading');
@@ -27,4 +40,4 @@ export const initializeFonts = () => {
document.documentElement.classList.add('fonts-fallback');
}
}, 3000);
-};
\ No newline at end of file
+};
diff --git a/src/utils/getClientIp.ts b/src/utils/getClientIp.ts
new file mode 100644
index 0000000..b6d0f7e
--- /dev/null
+++ b/src/utils/getClientIp.ts
@@ -0,0 +1,33 @@
+import { NextRequest } from 'next/server';
+
+/**
+ * Extract client IP address from Next.js request headers
+ *
+ * This function handles various proxy and forwarding scenarios:
+ * - Vercel's x-forwarded-for header (may contain multiple IPs)
+ * - x-real-ip header (direct client IP)
+ * - Fallback for development environments
+ *
+ * @param request - Next.js request object
+ * @returns Client IP address as a string, or 'unknown-ip' as fallback
+ */
+export function getClientIp(request: NextRequest): string {
+ // Check Vercel's forwarded IP header first
+ // In production, this is the most reliable source
+ const forwardedFor = request.headers.get('x-forwarded-for');
+ if (forwardedFor) {
+ // x-forwarded-for may contain multiple IPs in format: "client, proxy1, proxy2"
+ // The first IP is the original client IP
+ return forwardedFor.split(',')[0].trim();
+ }
+
+ // Check for real IP header (used by some proxies and load balancers)
+ const realIp = request.headers.get('x-real-ip');
+ if (realIp) {
+ return realIp;
+ }
+
+ // Fallback to a default identifier for development environments
+ // or when headers are not available
+ return 'unknown-ip';
+}
diff --git a/src/utils/rateLimitSupabase.ts b/src/utils/rateLimitSupabase.ts
new file mode 100644
index 0000000..b0a7abe
--- /dev/null
+++ b/src/utils/rateLimitSupabase.ts
@@ -0,0 +1,160 @@
+import { createClient } from '@/lib/supabase/server';
+
+interface RateLimitRecord {
+ identifier: string;
+ count: number;
+ window_start: string;
+ created_at?: string;
+ updated_at?: string;
+}
+
+const WINDOW_SIZE_MS = 3600000; // 1 hour
+const MAX_REQUESTS = 5; // Maximum requests per hour
+
+class SupabaseRateLimiter {
+ /**
+ * Check if an identifier (IP address) is rate limited
+ * @param identifier - The IP address or unique identifier to check
+ * @returns true if rate limited, false otherwise
+ */
+ async isRateLimited(identifier: string): Promise {
+ try {
+ const supabase = await createClient();
+ const now = Date.now();
+ const windowStart = new Date(now - WINDOW_SIZE_MS).toISOString();
+ // Get the current rate limit record for this identifier
+ const { data: existingRecord, error: fetchError } = await supabase
+ .from('rate_limits')
+ .select('*')
+ .eq('identifier', identifier)
+ .gte('window_start', windowStart)
+ .order('window_start', { ascending: false })
+ .limit(1)
+ .single();
+
+ if (fetchError && fetchError.code !== 'PGRST116') {
+ // PGRST116 is "no rows returned" which is fine
+ console.error('Error fetching rate limit:', fetchError);
+ return false; // Fail open - don't block on errors
+ }
+
+ // No existing record or window expired - create new record
+ if (!existingRecord || new Date(existingRecord.window_start).getTime() < now - WINDOW_SIZE_MS) {
+ const { error: insertError } = await supabase
+ .from('rate_limits')
+ .insert({
+ identifier,
+ count: 1,
+ window_start: new Date(now).toISOString()
+ });
+
+ if (insertError) {
+ console.error('Error inserting rate limit:', insertError);
+ return false; // Fail open
+ }
+
+ return false; // First request in new window
+ }
+
+ // Check if limit exceeded
+ if (existingRecord.count >= MAX_REQUESTS) {
+ return true; // Rate limited
+ }
+
+ // Increment counter
+ const { error: updateError } = await supabase
+ .from('rate_limits')
+ .update({
+ count: existingRecord.count + 1,
+ updated_at: new Date().toISOString()
+ })
+ .eq('identifier', identifier)
+ .eq('window_start', existingRecord.window_start);
+
+ if (updateError) {
+ console.error('Error updating rate limit:', updateError);
+ return false; // Fail open
+ }
+
+ return false; // Not rate limited yet
+ } catch (error) {
+ console.error('[RateLimiter] Unexpected error in isRateLimited - FAILING OPEN:', error);
+ return false; // Fail open on ALL errors
+ }
+ }
+
+ /**
+ * Get the number of remaining attempts for an identifier
+ * @param identifier - The IP address or unique identifier to check
+ * @returns Number of remaining attempts (0 if rate limited)
+ */
+ async getRemainingAttempts(identifier: string): Promise {
+ try {
+ const supabase = await createClient();
+ const now = Date.now();
+ const windowStart = new Date(now - WINDOW_SIZE_MS).toISOString();
+ const { data: record, error } = await supabase
+ .from('rate_limits')
+ .select('count')
+ .eq('identifier', identifier)
+ .gte('window_start', windowStart)
+ .order('window_start', { ascending: false })
+ .limit(1)
+ .single();
+
+ if (error && error.code !== 'PGRST116') {
+ console.error('Error fetching remaining attempts:', error);
+ return MAX_REQUESTS; // Fail open
+ }
+
+ if (!record) {
+ return MAX_REQUESTS; // No record yet
+ }
+
+ return Math.max(0, MAX_REQUESTS - record.count);
+ } catch (error) {
+ console.error('[RateLimiter] Unexpected error in getRemainingAttempts - FAILING OPEN:', error);
+ return MAX_REQUESTS; // Fail open on ALL errors
+ }
+ }
+
+ /**
+ * Get the time in milliseconds until the rate limit resets
+ * @param identifier - The IP address or unique identifier to check
+ * @returns Milliseconds until reset (0 if no active limit)
+ */
+ async getTimeToReset(identifier: string): Promise {
+ try {
+ const supabase = await createClient();
+ const now = Date.now();
+ const windowStart = new Date(now - WINDOW_SIZE_MS).toISOString();
+ const { data: record, error } = await supabase
+ .from('rate_limits')
+ .select('window_start')
+ .eq('identifier', identifier)
+ .gte('window_start', windowStart)
+ .order('window_start', { ascending: false })
+ .limit(1)
+ .single();
+
+ if (error && error.code !== 'PGRST116') {
+ console.error('Error fetching time to reset:', error);
+ return 0;
+ }
+
+ if (!record) {
+ return 0; // No active window
+ }
+
+ const windowStartTime = new Date(record.window_start).getTime();
+ const resetTime = windowStartTime + WINDOW_SIZE_MS;
+ return Math.max(0, resetTime - now);
+ } catch (error) {
+ console.error('[RateLimiter] Unexpected error in getTimeToReset - FAILING OPEN:', error);
+ return 0; // Fail open on ALL errors
+ }
+ }
+}
+
+export const supabaseRateLimiter = new SupabaseRateLimiter();
+export { WINDOW_SIZE_MS, MAX_REQUESTS };
diff --git a/src/utils/rateLimiting.ts b/src/utils/rateLimiting.ts
index f1a77b7..7ce2248 100644
--- a/src/utils/rateLimiting.ts
+++ b/src/utils/rateLimiting.ts
@@ -1,3 +1,6 @@
+/**
+ * Rate limit entry tracking request count and timestamp
+ */
interface RateLimitEntry {
count: number;
timestamp: number;
@@ -6,6 +9,10 @@ interface RateLimitEntry {
const WINDOW_SIZE_MS = 3600000; // 1 hour
const MAX_REQUESTS = 5; // Maximum requests per hour
+/**
+ * Rate Limiter for API request throttling
+ * Manages rate limiting based on IP addresses with sliding window
+ */
class RateLimiter {
private cache: Map;
@@ -13,6 +20,9 @@ class RateLimiter {
this.cache = new Map();
}
+ /**
+ * Check if an IP address is currently rate limited
+ */
isRateLimited(ip: string): boolean {
const now = Date.now();
const entry = this.cache.get(ip);
@@ -35,6 +45,9 @@ class RateLimiter {
return false;
}
+ /**
+ * Get remaining attempts for an IP address
+ */
getRemainingAttempts(ip: string): number {
const entry = this.cache.get(ip);
if (!entry) {
@@ -43,6 +56,9 @@ class RateLimiter {
return Math.max(0, MAX_REQUESTS - entry.count);
}
+ /**
+ * Get time in milliseconds until rate limit resets for an IP
+ */
getTimeToReset(ip: string): number {
const entry = this.cache.get(ip);
if (!entry) {
diff --git a/src/utils/serviceWorkerRegistration.ts b/src/utils/serviceWorkerRegistration.ts
index 1246246..72d05f3 100644
--- a/src/utils/serviceWorkerRegistration.ts
+++ b/src/utils/serviceWorkerRegistration.ts
@@ -1,3 +1,12 @@
+/**
+ * Service Worker Registration Utility
+ * Handles registration and unregistration of service workers for PWA functionality
+ */
+
+/**
+ * Registers the service worker
+ * Automatically registers the service worker after the window load event
+ */
export function register() {
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
@@ -14,6 +23,10 @@ export function register() {
}
}
+/**
+ * Unregisters the service worker
+ * Removes the currently registered service worker
+ */
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
diff --git a/src/utils/supabaseClient.ts b/src/utils/supabaseClient.ts
index 3e9dcc1..566d67e 100644
--- a/src/utils/supabaseClient.ts
+++ b/src/utils/supabaseClient.ts
@@ -1,6 +1,30 @@
+/**
+ * Supabase Client Configuration
+ * Zentrale Konfiguration für die Supabase-Verbindung
+ */
+
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = 'https://mxadgucxhmstlzsbgmoz.supabase.co';
const supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im14YWRndWN4aG1zdGx6c2JnbW96Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzkwMDgwNjIsImV4cCI6MjA1NDU4NDA2Mn0.MmFqEm7OfwzOlSegLYl9YWLCmIp8YajzK3Aozubn66Q';
+/**
+ * Supabase Client Instanz
+ *
+ * Vorkonfigurierter Supabase-Client für den Zugriff auf die Datenbank.
+ * Verwendet die anonyme (anon) Rolle für öffentliche Zugriffe.
+ *
+ * @example
+ * // Daten aus einer Tabelle abrufen
+ * const { data, error } = await supabase
+ * .from('table_name')
+ * .select('*');
+ *
+ * @example
+ * // Authentifizierung
+ * const { user, error } = await supabase.auth.signIn({
+ * email: 'user@example.com',
+ * password: 'password'
+ * });
+ */
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
\ No newline at end of file
diff --git a/src/utils/throttle.ts b/src/utils/throttle.ts
new file mode 100644
index 0000000..c28bf2e
--- /dev/null
+++ b/src/utils/throttle.ts
@@ -0,0 +1,40 @@
+/**
+ * Throttles a function to execute at most once per specified delay period.
+ * The first call executes immediately, then subsequent calls are throttled.
+ *
+ * @param func - The function to throttle
+ * @param delay - Minimum time in milliseconds between function executions
+ * @returns Throttled version of the function
+ */
+export function throttle any>(
+ func: T,
+ delay: number
+): (...args: Parameters) => void {
+ let lastCall = 0;
+ let timeout: NodeJS.Timeout | null = null;
+
+ return function (...args: Parameters) {
+ const now = Date.now();
+ const timeSinceLastCall = now - lastCall;
+
+ const execute = () => {
+ lastCall = Date.now();
+ func(...args);
+ };
+
+ if (timeSinceLastCall >= delay) {
+ // Enough time has passed, execute immediately
+ if (timeout) {
+ clearTimeout(timeout);
+ timeout = null;
+ }
+ execute();
+ } else if (!timeout) {
+ // Schedule execution after remaining delay
+ timeout = setTimeout(() => {
+ timeout = null;
+ execute();
+ }, delay - timeSinceLastCall);
+ }
+ };
+}
diff --git a/src/utils/webVitals.ts b/src/utils/webVitals.ts
index 8fb3ba1..8de9a4c 100644
--- a/src/utils/webVitals.ts
+++ b/src/utils/webVitals.ts
@@ -1,5 +1,13 @@
+/**
+ * Web Vitals Reporting Service
+ * Tracks and reports Core Web Vitals metrics to Google Analytics
+ */
+
import { onCLS, onINP, onLCP, onFCP, onTTFB } from 'web-vitals';
+/**
+ * Web Vitals metric data structure
+ */
interface Metric {
name: string;
value: number;
@@ -8,6 +16,9 @@ interface Metric {
id: string;
}
+/**
+ * Send Web Vitals metric to Google Analytics and dispatch custom event
+ */
function sendToAnalytics(metric: Metric) {
const body = JSON.stringify({
name: metric.name,
@@ -45,6 +56,9 @@ function sendToAnalytics(metric: Metric) {
// }
}
+/**
+ * Initialize Web Vitals reporting for all Core Web Vitals metrics
+ */
export function reportWebVitals() {
onCLS(sendToAnalytics);
onINP(sendToAnalytics);
@@ -53,7 +67,9 @@ export function reportWebVitals() {
onTTFB(sendToAnalytics);
}
-// Helper function to get Web Vitals score
+/**
+ * Calculate Web Vitals rating based on metric thresholds
+ */
export function getWebVitalsScore(metric: Metric): string {
const thresholds = {
LCP: { good: 2500, poor: 4000 },
diff --git a/supabase/APPLY_MIGRATION.md b/supabase/APPLY_MIGRATION.md
new file mode 100644
index 0000000..cf408af
--- /dev/null
+++ b/supabase/APPLY_MIGRATION.md
@@ -0,0 +1,119 @@
+# How to Apply the Rate Limits Migration
+
+## Quick Start (Manual Application)
+
+Since the Supabase CLI is not configured for this project, please follow these steps to apply the migration manually through the Supabase dashboard:
+
+### Step 1: Access the SQL Editor
+
+Open the Supabase SQL Editor in your browser:
+```
+https://app.supabase.com/project/mxadgucxhmstlzsbgmoz/sql
+```
+
+### Step 2: Copy the Migration SQL
+
+Open the migration file:
+```
+supabase/migrations/20260125_create_rate_limits_table.sql
+```
+
+Copy the entire contents (approximately 42 lines of SQL).
+
+### Step 3: Execute the Migration
+
+1. In the SQL Editor, paste the copied SQL
+2. Click the **"RUN"** button (or press `Ctrl+Enter` / `Cmd+Enter`)
+3. Wait for the success message: "Success. No rows returned"
+
+### Step 4: Verify the Table Was Created
+
+**Option A: Using Table Editor**
+1. Navigate to: https://app.supabase.com/project/mxadgucxhmstlzsbgmoz/editor
+2. Look for **"rate_limits"** in the table list (left sidebar)
+3. Click on it to see the table structure
+
+**Option B: Using SQL Query**
+Run this query in the SQL Editor:
+```sql
+SELECT
+ table_name,
+ column_name,
+ data_type,
+ is_nullable
+FROM information_schema.columns
+WHERE table_name = 'rate_limits'
+ORDER BY ordinal_position;
+```
+
+You should see 5 rows (one for each column):
+- identifier (text)
+- count (integer)
+- window_start (timestamp with time zone)
+- created_at (timestamp with time zone)
+- updated_at (timestamp with time zone)
+
+**Option C: Check Indexes**
+Run this query to verify indexes were created:
+```sql
+SELECT
+ indexname,
+ indexdef
+FROM pg_indexes
+WHERE tablename = 'rate_limits';
+```
+
+You should see:
+- Primary key on (identifier, window_start)
+- idx_rate_limits_identifier_window
+- idx_rate_limits_window_start
+
+## Troubleshooting
+
+### Error: "relation 'rate_limits' already exists"
+
+The table is already created! No action needed. Verify it exists using the verification steps above.
+
+### Error: "permission denied"
+
+Make sure you're:
+1. Logged into the correct Supabase account
+2. Have admin/owner permissions on the project
+3. Using the correct project URL
+
+### Table not visible in Table Editor
+
+1. Refresh the page (F5)
+2. Check the SQL output for error messages
+3. Try the SQL verification query in Option B above
+
+### Need to Revert/Drop the Table?
+
+If you need to start over, run this SQL:
+```sql
+DROP TABLE IF EXISTS rate_limits CASCADE;
+```
+
+Then re-apply the migration from Step 1.
+
+## Alternative: Automated Scripts
+
+We've provided scripts for automated migration, but they require additional setup:
+
+### Using apply-migration.js (requires service role key)
+
+1. Add `SUPABASE_SERVICE_ROLE_KEY` to `.env.local`
+2. Run: `node scripts/apply-migration.js`
+
+### Using Supabase CLI (if installed)
+
+```bash
+supabase migration up
+```
+
+## Next Steps
+
+After the migration is applied successfully:
+1. ✅ Verify the table exists (see Step 4 above)
+2. ✅ Mark this subtask as complete
+3. ✅ Proceed to Phase 2: Building the rate limiting utility
diff --git a/supabase/MIGRATION_INSTRUCTIONS.md b/supabase/MIGRATION_INSTRUCTIONS.md
new file mode 100644
index 0000000..cc11091
--- /dev/null
+++ b/supabase/MIGRATION_INSTRUCTIONS.md
@@ -0,0 +1,85 @@
+# Supabase Migration Instructions
+
+## Apply Migration: Create rate_limits Table
+
+### Quick Steps
+
+1. **Open Supabase SQL Editor**
+ - Go to: https://app.supabase.com/project/mxadgucxhmstlzsbgmoz/sql
+
+2. **Copy Migration SQL**
+ - Open: `supabase/migrations/20260125_create_rate_limits_table.sql`
+ - Copy all contents (Ctrl+A, Ctrl+C)
+
+3. **Execute Migration**
+ - Paste the SQL into the Supabase SQL editor
+ - Click the "Run" button (or press Ctrl+Enter)
+ - Wait for "Success. No rows returned" message
+
+4. **Verify Table Creation**
+ - Go to Table Editor: https://app.supabase.com/project/mxadgucxhmstlzsbgmoz/editor
+ - Look for "rate_limits" table in the list
+ - Click on it to verify the schema
+
+### Expected Table Schema
+
+The `rate_limits` table should have:
+
+| Column | Type | Description |
+|--------|------|-------------|
+| identifier | TEXT | Client identifier (IP address) |
+| count | INTEGER | Number of requests in window |
+| window_start | TIMESTAMPTZ | Start of rate limit window |
+| created_at | TIMESTAMPTZ | When record was created |
+| updated_at | TIMESTAMPTZ | When record was last updated |
+
+**Primary Key:** (identifier, window_start)
+
+**Indexes:**
+- `idx_rate_limits_identifier_window` on (identifier, window_start DESC)
+- `idx_rate_limits_window_start` on (window_start)
+
+### Alternative: Using Supabase CLI (if available)
+
+If you have Supabase CLI installed:
+
+```bash
+supabase db push
+```
+
+Or apply the specific migration:
+
+```bash
+supabase migration up
+```
+
+### Verification Query
+
+Run this query in the SQL editor to verify the table exists:
+
+```sql
+SELECT
+ table_name,
+ column_name,
+ data_type,
+ is_nullable
+FROM information_schema.columns
+WHERE table_name = 'rate_limits'
+ORDER BY ordinal_position;
+```
+
+You should see all 5 columns listed.
+
+### Troubleshooting
+
+**Error: "relation already exists"**
+- The table already exists. You can verify by checking Table Editor.
+
+**Error: "permission denied"**
+- Make sure you're logged into the correct Supabase project.
+- Verify you have admin/owner access to the project.
+
+**Table not appearing in Table Editor**
+- Refresh the page (F5)
+- Check the SQL editor for any error messages
+- Verify the SQL was executed successfully
diff --git a/supabase/migrations/20260125_create_rate_limits_table.sql b/supabase/migrations/20260125_create_rate_limits_table.sql
new file mode 100644
index 0000000..898ac6e
--- /dev/null
+++ b/supabase/migrations/20260125_create_rate_limits_table.sql
@@ -0,0 +1,41 @@
+-- Create rate_limits table for persistent rate limiting
+-- This replaces the in-memory Map-based rate limiter
+
+CREATE TABLE IF NOT EXISTS rate_limits (
+ -- Unique identifier (typically IP address)
+ identifier TEXT NOT NULL,
+
+ -- Number of requests in the current time window
+ count INTEGER NOT NULL DEFAULT 1,
+
+ -- Start of the rate limit window (for sliding window algorithm)
+ window_start TIMESTAMPTZ NOT NULL,
+
+ -- Timestamp when the record was created
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+
+ -- Timestamp when the record was last updated
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+
+ -- Primary key ensures one record per identifier per time window
+ PRIMARY KEY (identifier, window_start)
+);
+
+-- Index for fast lookups by identifier and window_start
+-- This is critical for rate limiting performance
+CREATE INDEX IF NOT EXISTS idx_rate_limits_identifier_window
+ ON rate_limits(identifier, window_start DESC);
+
+-- Index for cleanup queries (to remove old rate limit records)
+CREATE INDEX IF NOT EXISTS idx_rate_limits_window_start
+ ON rate_limits(window_start);
+
+-- Add a comment to the table
+COMMENT ON TABLE rate_limits IS 'Stores rate limiting data for API endpoints. Each record tracks request counts within a time window for a specific identifier (typically IP address).';
+
+-- Add comments to columns for documentation
+COMMENT ON COLUMN rate_limits.identifier IS 'Unique identifier for the client (typically IP address)';
+COMMENT ON COLUMN rate_limits.count IS 'Number of requests made in the current time window';
+COMMENT ON COLUMN rate_limits.window_start IS 'Start timestamp of the rate limit window';
+COMMENT ON COLUMN rate_limits.created_at IS 'Timestamp when this record was first created';
+COMMENT ON COLUMN rate_limits.updated_at IS 'Timestamp when this record was last updated';
diff --git a/test-headers-complete.sh b/test-headers-complete.sh
new file mode 100644
index 0000000..d83a2f5
--- /dev/null
+++ b/test-headers-complete.sh
@@ -0,0 +1,127 @@
+#!/bin/bash
+set -e
+
+echo "==================================="
+echo "Security Headers Verification Test"
+echo "==================================="
+echo ""
+
+# Kill any existing node processes
+echo "Stopping any existing dev servers..."
+lsof -ti:3000 | xargs kill -9 2>/dev/null || true
+sleep 2
+
+# Clear build cache
+echo "Clearing build cache..."
+rm -rf .next
+sleep 1
+
+# Start dev server
+echo "Starting Next.js dev server..."
+npm run dev > dev-server.log 2>&1 &
+SERVER_PID=$!
+echo "Server PID: $SERVER_PID"
+
+# Wait for server to be ready
+echo "Waiting for server to start..."
+MAX_RETRIES=60
+RETRY_COUNT=0
+
+while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
+ if curl -s http://localhost:3000 > /dev/null 2>&1; then
+ echo "✓ Server is ready!"
+ break
+ fi
+ RETRY_COUNT=$((RETRY_COUNT + 1))
+ sleep 1
+ echo -n "."
+done
+echo ""
+
+if [ $RETRY_COUNT -eq $MAX_RETRIES ]; then
+ echo "✗ Server failed to start within $MAX_RETRIES seconds"
+ kill $SERVER_PID 2>/dev/null || true
+ exit 1
+fi
+
+sleep 3
+
+# Test headers on /de route
+echo ""
+echo "Testing security headers on /de route..."
+echo "===================================="
+HEADERS=$(curl -sI http://localhost:3000/de 2>&1)
+
+# Check each header
+PASS_COUNT=0
+FAIL_COUNT=0
+
+echo ""
+echo "Checking Content-Security-Policy..."
+if echo "$HEADERS" | grep -qi "content-security-policy"; then
+ echo "✓ Content-Security-Policy header found"
+ echo "$HEADERS" | grep -i "content-security-policy"
+ PASS_COUNT=$((PASS_COUNT + 1))
+else
+ echo "✗ Content-Security-Policy header MISSING"
+ FAIL_COUNT=$((FAIL_COUNT + 1))
+fi
+
+echo ""
+echo "Checking Strict-Transport-Security..."
+if echo "$HEADERS" | grep -qi "strict-transport-security"; then
+ echo "✓ Strict-Transport-Security header found"
+ echo "$HEADERS" | grep -i "strict-transport-security"
+ PASS_COUNT=$((PASS_COUNT + 1))
+else
+ echo "✗ Strict-Transport-Security header MISSING"
+ FAIL_COUNT=$((FAIL_COUNT + 1))
+fi
+
+echo ""
+echo "Checking Referrer-Policy..."
+if echo "$HEADERS" | grep -qi "referrer-policy"; then
+ echo "✓ Referrer-Policy header found"
+ echo "$HEADERS" | grep -i "referrer-policy"
+ PASS_COUNT=$((PASS_COUNT + 1))
+else
+ echo "✗ Referrer-Policy header MISSING"
+ FAIL_COUNT=$((FAIL_COUNT + 1))
+fi
+
+echo ""
+echo "Checking Permissions-Policy..."
+if echo "$HEADERS" | grep -qi "permissions-policy"; then
+ echo "✓ Permissions-Policy header found"
+ echo "$HEADERS" | grep -i "permissions-policy"
+ PASS_COUNT=$((PASS_COUNT + 1))
+else
+ echo "✗ Permissions-Policy header MISSING"
+ FAIL_COUNT=$((FAIL_COUNT + 1))
+fi
+
+echo ""
+echo "===================================="
+echo "Results: $PASS_COUNT/4 headers found"
+echo "===================================="
+
+if [ $FAIL_COUNT -gt 0 ]; then
+ echo ""
+ echo "FULL HEADERS RESPONSE:"
+ echo "$HEADERS"
+fi
+
+# Cleanup
+echo ""
+echo "Stopping dev server..."
+kill $SERVER_PID 2>/dev/null || true
+wait $SERVER_PID 2>/dev/null || true
+
+# Exit with appropriate code
+if [ $PASS_COUNT -eq 4 ]; then
+ echo "✓ All security headers verified successfully!"
+ exit 0
+else
+ echo "✗ Verification failed - some headers are missing"
+ exit 1
+fi
diff --git a/test-output.txt b/test-output.txt
new file mode 100644
index 0000000..e69de29
diff --git a/test-rate-limit-api.sh b/test-rate-limit-api.sh
new file mode 100644
index 0000000..4ec9068
--- /dev/null
+++ b/test-rate-limit-api.sh
@@ -0,0 +1,107 @@
+#!/bin/bash
+
+# Test script for /api/contact rate limiting
+# Run this after fixing the middleware configuration issue
+
+echo "========================================="
+echo "API Rate Limiting Test Script"
+echo "========================================="
+echo ""
+
+# Configuration
+API_URL="http://localhost:3000/api/contact"
+TEST_IP="192.168.1.100"
+
+echo "Testing endpoint: $API_URL"
+echo "Test IP address: $TEST_IP"
+echo ""
+
+# Test function
+send_request() {
+ local request_num=$1
+ echo "--- Request #$request_num ---"
+
+ RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$API_URL" \
+ -H "Content-Type: application/json" \
+ -H "X-Forwarded-For: $TEST_IP" \
+ -d '{"name":"Test User","email":"test@example.com","message":"This is test message #'$request_num'"}')
+
+ HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
+ BODY=$(echo "$RESPONSE" | sed '$d')
+
+ echo "Status Code: $HTTP_CODE"
+ echo "Response Body: $BODY"
+
+ # Get headers in a separate request
+ HEADERS=$(curl -s -I -X POST "$API_URL" \
+ -H "Content-Type: application/json" \
+ -H "X-Forwarded-For: $TEST_IP" \
+ -d '{"name":"Test User","email":"test@example.com","message":"Test"}' 2>&1 \
+ | grep -i -E "X-RateLimit-Remaining|Retry-After" || echo "No rate limit headers")
+
+ echo "Rate Limit Headers: $HEADERS"
+ echo ""
+
+ sleep 0.5
+}
+
+# Test 1: First 5 requests should succeed
+echo "TEST 1: Sending 5 requests (should all succeed)"
+echo "================================================"
+for i in {1..5}; do
+ send_request $i
+done
+
+# Test 2: 6th request should be rate limited
+echo "TEST 2: Sending 6th request (should be rate limited - 429)"
+echo "==========================================================="
+send_request 6
+
+# Test 3: 7th request should also be rate limited
+echo "TEST 3: Sending 7th request (should still be rate limited - 429)"
+echo "================================================================"
+send_request 7
+
+echo ""
+echo "========================================="
+echo "Test Summary"
+echo "========================================="
+echo "✓ Requests 1-5 should return HTTP 200"
+echo "✓ Each successful request should decrease X-RateLimit-Remaining"
+echo "✓ Requests 6-7 should return HTTP 429"
+echo "✓ HTTP 429 responses should include Retry-After header"
+echo "✓ HTTP 429 responses should have X-RateLimit-Remaining: 0"
+echo ""
+echo "To verify persistence:"
+echo "1. Check Supabase rate_limits table for entry with identifier='$TEST_IP'"
+echo "2. Restart dev server"
+echo "3. Run this script again - should immediately return 429"
+echo "4. Wait 1 hour or manually delete DB record"
+echo "5. Run again - should allow 5 new requests"
+echo ""
+echo "========================================="
+echo "Testing invalid form data"
+echo "========================================="
+
+echo "--- Test: Empty name ---"
+curl -s -w "\nHTTP %{http_code}\n" -X POST "$API_URL" \
+ -H "Content-Type: application/json" \
+ -H "X-Forwarded-For: 192.168.1.200" \
+ -d '{"name":"","email":"test@example.com","message":"Test"}'
+echo ""
+
+echo "--- Test: Invalid email ---"
+curl -s -w "\nHTTP %{http_code}\n" -X POST "$API_URL" \
+ -H "Content-Type: application/json" \
+ -H "X-Forwarded-For: 192.168.1.200" \
+ -d '{"name":"Test","email":"notanemail","message":"Test"}'
+echo ""
+
+echo "--- Test: Empty message ---"
+curl -s -w "\nHTTP %{http_code}\n" -X POST "$API_URL" \
+ -H "Content-Type: application/json" \
+ -H "X-Forwarded-For: 192.168.1.200" \
+ -d '{"name":"Test","email":"test@example.com","message":""}'
+echo ""
+
+echo "All tests completed!"
diff --git a/test-rate-limit.sh b/test-rate-limit.sh
new file mode 100644
index 0000000..bfbaf54
--- /dev/null
+++ b/test-rate-limit.sh
@@ -0,0 +1,41 @@
+#!/bin/bash
+
+echo "=== Testing Rate Limiting on /api/contact ==="
+echo ""
+
+# Test data
+TEST_DATA='{"name":"Test User","email":"test@example.com","message":"This is a test message"}'
+
+# Send requests and capture responses
+for i in {1..7}; do
+ echo "--- Request #$i ---"
+
+ RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}\n" \
+ -X POST \
+ -H "Content-Type: application/json" \
+ -H "X-Forwarded-For: 192.168.1.100" \
+ -d "$TEST_DATA" \
+ http://localhost:3000/api/contact)
+
+ # Extract HTTP status
+ HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
+ BODY=$(echo "$RESPONSE" | grep -v "HTTP_STATUS")
+
+ echo "Status: $HTTP_STATUS"
+ echo "Response: $BODY"
+
+ # Extract headers with -i flag
+ HEADERS=$(curl -s -i -X POST \
+ -H "Content-Type: application/json" \
+ -H "X-Forwarded-For: 192.168.1.100" \
+ -d "$TEST_DATA" \
+ http://localhost:3000/api/contact | grep -E "X-RateLimit-Remaining|Retry-After")
+
+ echo "Headers: $HEADERS"
+ echo ""
+
+ # Small delay between requests
+ sleep 0.5
+done
+
+echo "=== Test Complete ==="
diff --git a/tsconfig.json b/tsconfig.json
index 4ca760c..3de2238 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -30,11 +30,8 @@
],
"exclude": [
"node_modules",
- "src/components-vite",
- "src/pages-vite",
"src/hooks",
"src/services",
- "src/utils",
"**/*.bak"
]
}
diff --git a/verify-headers.mjs b/verify-headers.mjs
new file mode 100644
index 0000000..f995a58
--- /dev/null
+++ b/verify-headers.mjs
@@ -0,0 +1,118 @@
+#!/usr/bin/env node
+/**
+ * Security Headers Verification Script
+ * Validates that all required security headers are configured in next.config.ts
+ */
+
+import { readFileSync } from 'fs';
+import { fileURLToPath } from 'url';
+import { dirname, join } from 'path';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+const REQUIRED_HEADERS = [
+ 'Content-Security-Policy',
+ 'Strict-Transport-Security',
+ 'Referrer-Policy',
+ 'Permissions-Policy'
+];
+
+const EXPECTED_VALUES = {
+ 'Content-Security-Policy': {
+ directives: [
+ "default-src 'self'",
+ "script-src",
+ "style-src",
+ "font-src",
+ "img-src",
+ "connect-src",
+ "frame-ancestors",
+ "base-uri",
+ "form-action"
+ ]
+ },
+ 'Strict-Transport-Security': {
+ includes: ['max-age=31536000', 'includeSubDomains', 'preload']
+ },
+ 'Referrer-Policy': {
+ value: 'strict-origin-when-cross-origin'
+ },
+ 'Permissions-Policy': {
+ includes: ['geolocation=()', 'microphone=()', 'camera=()', 'payment=()', 'usb=()']
+ }
+};
+
+console.log('🔒 Security Headers Verification\n');
+console.log('━'.repeat(60));
+
+try {
+ const configPath = join(__dirname, 'next.config.ts');
+ const configContent = readFileSync(configPath, 'utf-8');
+
+ let allPassed = true;
+
+ for (const header of REQUIRED_HEADERS) {
+ const headerRegex = new RegExp(`key:\\s*['"]${header}['"]`, 'i');
+ const found = headerRegex.test(configContent);
+
+ if (found) {
+ console.log(`✅ ${header}: FOUND`);
+
+ // Additional validation
+ const expected = EXPECTED_VALUES[header];
+ if (expected) {
+ if (expected.directives) {
+ // Check CSP directives
+ const allDirectivesFound = expected.directives.every(directive =>
+ configContent.includes(directive)
+ );
+ if (allDirectivesFound) {
+ console.log(` ✓ All required CSP directives present`);
+ } else {
+ console.log(` ⚠ Some CSP directives may be missing`);
+ }
+ } else if (expected.includes) {
+ // Check if all required parts are included
+ const allPartsFound = expected.includes.every(part =>
+ configContent.includes(part)
+ );
+ if (allPartsFound) {
+ console.log(` ✓ All required parts present`);
+ } else {
+ console.log(` ⚠ Some required parts may be missing`);
+ }
+ } else if (expected.value) {
+ // Check exact value
+ if (configContent.includes(expected.value)) {
+ console.log(` ✓ Correct value: ${expected.value}`);
+ } else {
+ console.log(` ⚠ Value may differ from expected`);
+ }
+ }
+ }
+ } else {
+ console.log(`❌ ${header}: NOT FOUND`);
+ allPassed = false;
+ }
+ }
+
+ console.log('━'.repeat(60));
+
+ if (allPassed) {
+ console.log('\n✅ SUCCESS: All required security headers are configured!\n');
+ console.log('Next steps for manual verification:');
+ console.log('1. Start dev server: npm run dev');
+ console.log('2. Check headers: curl -I http://localhost:3000');
+ console.log('3. Open browser DevTools > Network tab');
+ console.log('4. Verify no CSP violations in Console\n');
+ process.exit(0);
+ } else {
+ console.log('\n❌ FAILURE: Some required headers are missing!\n');
+ process.exit(1);
+ }
+
+} catch (error) {
+ console.error('❌ Error reading configuration:', error.message);
+ process.exit(1);
+}
diff --git a/verify-headers.sh b/verify-headers.sh
new file mode 100644
index 0000000..00ad8aa
--- /dev/null
+++ b/verify-headers.sh
@@ -0,0 +1,48 @@
+#!/bin/bash
+
+echo "Starting Next.js dev server..."
+npm run dev > /tmp/next-dev.log 2>&1 &
+SERVER_PID=$!
+
+echo "Waiting for server to be ready..."
+for i in {1..30}; do
+ if curl -s http://localhost:3000 > /dev/null 2>&1; then
+ echo "Server is ready!"
+ break
+ fi
+ sleep 1
+done
+
+echo ""
+echo "Testing security headers..."
+echo "================================"
+
+# Test the /de route since root redirects
+HEADERS=$(curl -s -I http://localhost:3000/de 2>&1)
+
+echo "Checking for Content-Security-Policy..."
+echo "$HEADERS" | grep -i "content-security-policy" && echo "✓ CSP header found" || echo "✗ CSP header missing"
+
+echo ""
+echo "Checking for Strict-Transport-Security..."
+echo "$HEADERS" | grep -i "strict-transport-security" && echo "✓ HSTS header found" || echo "✗ HSTS header missing"
+
+echo ""
+echo "Checking for Referrer-Policy..."
+echo "$HEADERS" | grep -i "referrer-policy" && echo "✓ Referrer-Policy header found" || echo "✗ Referrer-Policy header missing"
+
+echo ""
+echo "Checking for Permissions-Policy..."
+echo "$HEADERS" | grep -i "permissions-policy" && echo "✓ Permissions-Policy header found" || echo "✗ Permissions-Policy header missing"
+
+echo ""
+echo "================================"
+echo "Full headers response:"
+echo "$HEADERS"
+
+# Stop the server
+kill $SERVER_PID 2>/dev/null
+wait $SERVER_PID 2>/dev/null
+
+echo ""
+echo "Verification complete!"
diff --git a/verify-security-headers.js b/verify-security-headers.js
new file mode 100644
index 0000000..e954f04
--- /dev/null
+++ b/verify-security-headers.js
@@ -0,0 +1,165 @@
+const { spawn, execSync } = require('child_process');
+const http = require('http');
+
+console.log('===========================================');
+console.log('Security Headers Verification Script');
+console.log('===========================================\n');
+
+// Kill any process on port 3000
+console.log('Step 1: Stopping any processes on port 3000...');
+try {
+ if (process.platform === 'win32') {
+ execSync('for /f "tokens=5" %a in (\'netstat -aon ^| find ":3000" ^| find "LISTENING"\') do taskkill /F /PID %a', { stdio: 'ignore' });
+ } else {
+ execSync('lsof -ti:3000 | xargs kill -9', { stdio: 'ignore' });
+ }
+ console.log('✓ Stopped existing processes\n');
+} catch (e) {
+ console.log('✓ No processes to stop\n');
+}
+
+// Clear .next directory
+console.log('Step 2: Clearing build cache...');
+try {
+ execSync('rm -rf .next', { stdio: 'inherit' });
+ console.log('✓ Build cache cleared\n');
+} catch (e) {
+ console.log('! Could not clear cache (may not exist)\n');
+}
+
+// Start dev server
+console.log('Step 3: Starting Next.js dev server...');
+const server = spawn('npm', ['run', 'dev'], {
+ stdio: ['ignore', 'pipe', 'pipe'],
+ shell: true,
+ detached: false
+});
+
+let serverReady = false;
+
+server.stdout.on('data', (data) => {
+ const output = data.toString();
+ if (output.includes('Local:') || output.includes('localhost:3000')) {
+ serverReady = true;
+ console.log('✓ Server is ready!\n');
+ }
+});
+
+server.stderr.on('data', (data) => {
+ // Ignore stderr for now
+});
+
+// Wait for server to be ready, then test
+const maxWait = 60000; // 60 seconds
+const startTime = Date.now();
+const interval = setInterval(() => {
+ if (serverReady || (Date.now() - startTime > maxWait)) {
+ clearInterval(interval);
+
+ // Give it a few more seconds to fully initialize
+ setTimeout(() => {
+ testHeaders();
+ }, 5000);
+ }
+}, 1000);
+
+function testHeaders() {
+ console.log('Step 4: Testing security headers...');
+ console.log('=========================================\n');
+
+ const options = {
+ hostname: 'localhost',
+ port: 3000,
+ path: '/de',
+ method: 'HEAD'
+ };
+
+ const req = http.request(options, (res) => {
+ const headers = res.headers;
+
+ let passCount = 0;
+ let failCount = 0;
+
+ // Check Content-Security-Policy
+ console.log('Checking Content-Security-Policy...');
+ if (headers['content-security-policy']) {
+ console.log('✓ Content-Security-Policy found');
+ console.log(` Value: ${headers['content-security-policy'].substring(0, 60)}...\n`);
+ passCount++;
+ } else {
+ console.log('✗ Content-Security-Policy MISSING\n');
+ failCount++;
+ }
+
+ // Check Strict-Transport-Security
+ console.log('Checking Strict-Transport-Security...');
+ if (headers['strict-transport-security']) {
+ console.log('✓ Strict-Transport-Security found');
+ console.log(` Value: ${headers['strict-transport-security']}\n`);
+ passCount++;
+ } else {
+ console.log('✗ Strict-Transport-Security MISSING\n');
+ failCount++;
+ }
+
+ // Check Referrer-Policy
+ console.log('Checking Referrer-Policy...');
+ if (headers['referrer-policy']) {
+ console.log('✓ Referrer-Policy found');
+ console.log(` Value: ${headers['referrer-policy']}\n`);
+ passCount++;
+ } else {
+ console.log('✗ Referrer-Policy MISSING\n');
+ failCount++;
+ }
+
+ // Check Permissions-Policy
+ console.log('Checking Permissions-Policy...');
+ if (headers['permissions-policy']) {
+ console.log('✓ Permissions-Policy found');
+ console.log(` Value: ${headers['permissions-policy']}\n`);
+ passCount++;
+ } else {
+ console.log('✗ Permissions-Policy MISSING\n');
+ failCount++;
+ }
+
+ console.log('=========================================');
+ console.log(`Results: ${passCount}/4 security headers found`);
+ console.log('=========================================\n');
+
+ if (failCount > 0) {
+ console.log('All response headers:');
+ console.log(headers);
+ console.log('');
+ }
+
+ // Cleanup and exit
+ server.kill('SIGTERM');
+ setTimeout(() => {
+ server.kill('SIGKILL');
+ process.exit(passCount === 4 ? 0 : 1);
+ }, 2000);
+ });
+
+ req.on('error', (error) => {
+ console.error('✗ Error testing headers:', error.message);
+ server.kill('SIGTERM');
+ setTimeout(() => {
+ server.kill('SIGKILL');
+ process.exit(1);
+ }, 2000);
+ });
+
+ req.end();
+}
+
+// Handle script termination
+process.on('SIGINT', () => {
+ console.log('\n\nStopping server...');
+ server.kill('SIGTERM');
+ setTimeout(() => {
+ server.kill('SIGKILL');
+ process.exit(1);
+ }, 2000);
+});
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 0000000..9138fcc
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,57 @@
+import { defineConfig } from 'vitest/config';
+<<<<<<< HEAD
+import react from '@vitejs/plugin-react';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react()],
+ test: {
+ environment: 'jsdom',
+ globals: true,
+ setupFiles: ['./vitest.setup.ts'],
+ },
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
+ },
+ },
+=======
+import { resolve } from 'path';
+
+export default defineConfig({
+ plugins: [],
+ test: {
+ globals: true,
+ environment: 'jsdom',
+ setupFiles: [],
+ include: ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
+ exclude: [
+ '**/node_modules/**',
+ '**/dist/**',
+ '**/.next/**',
+ '**/coverage/**',
+ '**/*.bak/**',
+ '**/src/components-vite/**',
+ '**/src/pages-vite/**'
+ ],
+ coverage: {
+ provider: 'v8',
+ reporter: ['text', 'json', 'html'],
+ exclude: [
+ 'node_modules/',
+ '.next/',
+ 'coverage/',
+ '**/*.config.{js,ts}',
+ '**/*.bak',
+ 'src/components-vite/**',
+ 'src/pages-vite/**'
+ ]
+ }
+ },
+ resolve: {
+ alias: {
+ '@': resolve(__dirname, './src')
+ }
+ }
+>>>>>>> origin/master
+});
diff --git a/vitest.setup.ts b/vitest.setup.ts
new file mode 100644
index 0000000..7bc9877
--- /dev/null
+++ b/vitest.setup.ts
@@ -0,0 +1,20 @@
+// Vitest setup file
+// This file runs before each test file
+
+// Mock next-intl for testing
+vi.mock('next-intl', () => ({
+ useTranslations: () => (key: string) => {
+ if (key === 'title') return 'Test Title';
+ if (key === 'raw') return () => [];
+ return key;
+ },
+ useLocale: () => 'en',
+}));
+
+// Mock framer-motion to avoid animation issues in tests
+vi.mock('framer-motion', () => ({
+ motion: {
+ div: ({ children, ...props }: any) => {children}
,
+ },
+ AnimatePresence: ({ children }: any) => <>{children}>,
+}));