diff --git a/src/hooks/useAsync.ts b/src/hooks/useAsync.ts index 23499ef..73f0d0c 100644 --- a/src/hooks/useAsync.ts +++ b/src/hooks/useAsync.ts @@ -1,12 +1,44 @@ +/** + * Custom Hook for Async Operations + * Provides a reusable pattern for handling asynchronous operations with loading, error, and data states + */ + import { useState, useCallback } from 'react'; import { handleError } from '../utils/errorHandling'; +/** + * State interface for async operations + * @template T - The type of data returned by the async operation + */ interface AsyncState { + /** The data returned from the async operation, null if not yet loaded or error occurred */ data: T | null; + /** Error object if the operation failed, null otherwise */ error: Error | null; + /** Loading state indicator, true while operation is in progress */ loading: boolean; } +/** + * Custom hook for managing asynchronous operations + * Handles loading states, error handling, and data management for async functions + * + * @template T - The type of data returned by the async operation + * @returns Object containing: + * - data: The result of the async operation or null + * - error: Any error that occurred or null + * - loading: Boolean indicating if operation is in progress + * - execute: Function to trigger the async operation + * + * @example + * ```tsx + * const { data, error, loading, execute } = useAsync(); + * + * useEffect(() => { + * execute(() => fetchUser(userId)); + * }, [userId]); + * ``` + */ export function useAsync() { const [state, setState] = useState>({ data: null, diff --git a/src/hooks/useOnClickOutside.ts b/src/hooks/useOnClickOutside.ts index dbd277b..555a81d 100644 --- a/src/hooks/useOnClickOutside.ts +++ b/src/hooks/useOnClickOutside.ts @@ -1,5 +1,21 @@ +/** + * Click Outside Detection Hook + * Detects clicks outside of a referenced element and triggers a handler + */ + import { RefObject, useEffect } from 'react'; +/** + * Hook that triggers a handler when user clicks outside the referenced element + * Useful for closing dropdowns, modals, or menus when clicking outside + * + * @param ref - React ref object pointing to the element to monitor + * @param handler - Callback function to execute when click occurs outside the element + * + * @example + * const menuRef = useRef(null); + * useOnClickOutside(menuRef, () => setMenuOpen(false)); + */ export function useOnClickOutside(ref: RefObject, handler: (event: MouseEvent | TouchEvent) => void) { useEffect(() => { const listener = (event: MouseEvent | TouchEvent) => { diff --git a/src/hooks/useProjectData.ts b/src/hooks/useProjectData.ts index f1849ef..3bb704e 100644 --- a/src/hooks/useProjectData.ts +++ b/src/hooks/useProjectData.ts @@ -1,7 +1,18 @@ -// src/hooks/useProjectData.ts +/** + * Project Data Hook + * Custom hook for dynamically loading project data based on current language + */ + import { useTranslation } from 'react-i18next'; import { useState, useEffect } from 'react'; +/** + * Loads project data dynamically based on project ID and current language + * + * @template T - The type of the project data object + * @param {string} projectId - The unique identifier of the project + * @returns {T | null} The project data object or null if loading fails + */ export const useProjectData = (projectId: string): T | null => { const { i18n } = useTranslation(); const [projectData, setProjectData] = useState(null); diff --git a/src/hooks/useScrollLock.ts b/src/hooks/useScrollLock.ts index d790af5..4ff48ca 100644 --- a/src/hooks/useScrollLock.ts +++ b/src/hooks/useScrollLock.ts @@ -1,8 +1,18 @@ +/** + * Scroll Lock Hook + * Manages body scroll locking based on state and navigation transitions + */ + // hooks/useScrollLock.ts import { useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import { useScrollContext } from '../components/ScrollContext'; +/** + * Custom hook to lock/unlock page scrolling + * Automatically handles scroll restoration on route changes and cleanup on unmount + * @param lock - Whether to lock the page scroll + */ export const useScrollLock = (lock: boolean) => { const location = useLocation(); const { isTransitioning, lockScroll, unlockScroll } = useScrollContext(); diff --git a/src/hooks/useScrollTracking.ts b/src/hooks/useScrollTracking.ts index 8eebbc7..d024bba 100644 --- a/src/hooks/useScrollTracking.ts +++ b/src/hooks/useScrollTracking.ts @@ -1,8 +1,37 @@ +/** + * Scroll Tracking Hook + * Tracks user scroll depth and sends events to Google Tag Manager + */ + import { useEffect, useRef } from 'react'; import { trackScrollDepth } from '../services/gtm'; /** - * Hook to track scroll depth for GTM + * Custom hook to track scroll depth for GTM analytics + * + * Monitors user scroll behavior and triggers GTM events when the user reaches + * specific depth milestones (25%, 50%, 75%, 90%, 100%). Each milestone is + * tracked only once per page visit to avoid duplicate events. + * + * Features: + * - Debounced scroll event handling (100ms) for performance + * - Tracks depth milestones: 25%, 50%, 75%, 90%, 100% + * - Prevents duplicate tracking of the same milestone + * - Automatically resets tracking state on route changes + * - Checks initial scroll position on mount + * + * @example + * ```tsx + * function MyPage() { + * useScrollTracking(); + * return
...
; + * } + * ``` + * + * @remarks + * This hook should be used at the page/route level component to ensure + * accurate tracking across navigation. The tracking state is automatically + * reset when the URL pathname changes. */ export const useScrollTracking = () => { const trackedDepths = useRef(new Set()); diff --git a/src/lib/blog.ts b/src/lib/blog.ts index cafe5f9..ec45c80 100644 --- a/src/lib/blog.ts +++ b/src/lib/blog.ts @@ -1,21 +1,42 @@ +/** + * Blog Post Management Service + * Zentrale Verwaltung aller Blog-Beiträge mit automatischer Kategorie-Erkennung + */ + import fs from 'fs'; import path from 'path'; import { cache } from '@/utils/cache'; +/** + * Blog Post Interface + * Definiert die Struktur eines Blog-Beitrags + */ export interface BlogPost { + /** URL-freundlicher Slug */ slug: string; + /** Titel des Beitrags */ title: string; + /** Veröffentlichungsdatum (ISO 8601) */ date: string; + /** Kurze Zusammenfassung */ excerpt: string; + /** Pfad zum Cover-Bild */ coverImage: string; + /** Kategorie des Beitrags */ category?: string; + /** Schlagwörter/Tags */ tags?: string[]; + /** Vollständiger Markdown-Inhalt */ content?: string; } +// Verzeichnis der Blog-Beiträge const BLOG_POSTS_DIR = path.join(process.cwd(), 'blog-posts'); -// Category mapping based on filename patterns +/** + * Kategorie-Mapping basierend auf Dateinamen- und Inhaltsmustern + * Ordnet Keywords automatisch passenden Kategorien zu + */ const categoryMap: Record = { 'agentic': 'KI-Agenten', 'ai-agent': 'KI-Agenten', @@ -59,6 +80,13 @@ const categoryMap: Record = { 'home-assistant': 'Smart Home', }; +/** + * Erkennt automatisch die Kategorie eines Blog-Posts + * Analysiert Dateiname und Inhalt auf Keywords + * @param filename - Dateiname des Blog-Posts + * @param content - Inhalt des Blog-Posts + * @returns Die erkannte Kategorie oder 'Technologie' als Fallback + */ function detectCategory(filename: string, content: string): string { const lowerFilename = filename.toLowerCase(); const lowerContent = content.toLowerCase().slice(0, 500); @@ -71,6 +99,13 @@ function detectCategory(filename: string, content: string): string { return 'Technologie'; } +/** + * Parst einen Markdown-Blog-Post und extrahiert alle Metadaten + * Extrahiert Titel, Meta-Description, Keywords, Kategorie und generiert Slug + * @param filename - Dateiname des Markdown-Posts + * @param content - Vollständiger Markdown-Inhalt + * @returns BlogPost-Objekt oder null bei Fehler + */ function parseMarkdownPost(filename: string, content: string): BlogPost | null { try { // Extract title from first H1 @@ -130,6 +165,11 @@ function parseMarkdownPost(filename: string, content: string): BlogPost | null { } } +/** + * Lädt alle Blog-Posts aus dem Dateisystem + * Posts werden nach Datum absteigend sortiert (neueste zuerst) + * @returns Array aller Blog-Posts + */ export function getAllBlogPosts(): BlogPost[] { const cacheKey = 'blog:all-posts'; @@ -174,11 +214,21 @@ export function getAllBlogPosts(): BlogPost[] { return sortedPosts; } +/** + * Sucht einen Blog-Post anhand seines Slugs + * @param slug - Der URL-Slug des gesuchten Posts + * @returns Der gefundene Blog-Post oder null + */ export function getBlogPostBySlug(slug: string): BlogPost | null { const posts = getAllBlogPosts(); return posts.find(post => post.slug === slug) || null; } +/** + * Gibt alle verfügbaren Blog-Post Slugs zurück + * Nützlich für statische Seitengenerierung + * @returns Array aller Post-Slugs + */ export function getBlogPostSlugs(): string[] { return getAllBlogPosts().map(post => post.slug); } diff --git a/src/lib/markdown.tsx b/src/lib/markdown.tsx index 1ddde9d..1ff8f5c 100644 --- a/src/lib/markdown.tsx +++ b/src/lib/markdown.tsx @@ -1,5 +1,14 @@ +/** + * Markdown Rendering Utility + * Converts markdown text to styled React components + */ + import React from 'react'; +/** + * Parse inline markdown syntax (code, bold, italic, links) + * Converts inline markdown elements within a text string to React components + */ function parseInlineMarkdown(text: string): React.ReactNode { const parts: React.ReactNode[] = []; let remaining = text; @@ -63,6 +72,10 @@ function parseInlineMarkdown(text: string): React.ReactNode { return parts.length === 1 ? parts[0] : <>{parts}; } +/** + * Render markdown content to React components + * Supports headers, lists, code blocks, tables, blockquotes, and inline formatting + */ export function renderMarkdown(content: string): React.ReactNode { const lines = content.split('\n'); const elements: React.ReactNode[] = []; diff --git a/src/lib/supabase/client.ts b/src/lib/supabase/client.ts index e6db2a1..71be730 100644 --- a/src/lib/supabase/client.ts +++ b/src/lib/supabase/client.ts @@ -1,5 +1,14 @@ +/** + * Supabase Browser Client + * Client-side Supabase client for authentication and database access + */ + import { createBrowserClient } from '@supabase/ssr'; +/** + * Creates a Supabase browser client instance + * @returns Supabase client configured with environment variables + */ export function createClient() { return createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, diff --git a/src/lib/supabase/server.ts b/src/lib/supabase/server.ts index 7c29af0..6e300f9 100644 --- a/src/lib/supabase/server.ts +++ b/src/lib/supabase/server.ts @@ -1,6 +1,17 @@ +/** + * Supabase Server Client Utility + * Creates server-side Supabase clients with cookie handling for SSR + */ + import { createServerClient } from '@supabase/ssr'; import { cookies } from 'next/headers'; +/** + * Creates a Supabase client for server-side usage + * Handles cookie management for authentication in Next.js server components + * + * @returns {Promise} Supabase server client instance + */ export async function createClient() { const cookieStore = await cookies(); 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.ts b/src/utils/cache.ts index ef503e5..a742d6e 100644 --- a/src/utils/cache.ts +++ b/src/utils/cache.ts @@ -1,3 +1,11 @@ +/** + * 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; @@ -5,17 +13,34 @@ type CacheItem = { 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(); @@ -32,6 +57,14 @@ class Cache { }); } +<<<<<<< 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; @@ -48,6 +81,7 @@ class Cache { } } +>>>>>>> origin/master get(key: string): T | null { const item = this.storage.get(key); @@ -58,23 +92,41 @@ class Cache { 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.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/rateLimiting.ts b/src/utils/rateLimiting.ts new file mode 100644 index 0000000..7ce2248 --- /dev/null +++ b/src/utils/rateLimiting.ts @@ -0,0 +1,71 @@ +/** + * Rate limit entry tracking request count and timestamp + */ +interface RateLimitEntry { + count: number; + timestamp: number; +} + +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; + + constructor() { + 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); + + if (!entry) { + this.cache.set(ip, { count: 1, timestamp: now }); + return false; + } + + if (now - entry.timestamp > WINDOW_SIZE_MS) { + this.cache.set(ip, { count: 1, timestamp: now }); + return false; + } + + if (entry.count >= MAX_REQUESTS) { + return true; + } + + entry.count++; + return false; + } + + /** + * Get remaining attempts for an IP address + */ + getRemainingAttempts(ip: string): number { + const entry = this.cache.get(ip); + if (!entry) { + return MAX_REQUESTS; + } + 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) { + return 0; + } + return Math.max(0, WINDOW_SIZE_MS - (Date.now() - entry.timestamp)); + } +} + +export const rateLimiter = new RateLimiter(); \ No newline at end of file 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/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 },