From bd7a259d725909acc87ab7a981c51cb9ac870c12 Mon Sep 17 00:00:00 2001 From: Alexander Schmidt Date: Thu, 5 Feb 2026 15:25:57 +0100 Subject: [PATCH] docs: add JSDoc documentation to core modules (directus, i18n, router, helpers) --- js/i18n.js | 72 +++++++++++++++++++++++++++++++ js/router.js | 62 +++++++++++++++++++++++++++ js/services/directus.js | 93 +++++++++++++++++++++++++++++++++++++++++ js/utils/helpers.js | 37 +++++++++++----- 4 files changed, 254 insertions(+), 10 deletions(-) diff --git a/js/i18n.js b/js/i18n.js index 21028c5..ae42f79 100644 --- a/js/i18n.js +++ b/js/i18n.js @@ -1,13 +1,37 @@ +/** + * Internationalization (i18n) Service + * Handles translations and locale switching + * @module i18n + */ + +/** + * @typedef {'de' | 'en' | 'fr'} Locale + */ + +/** + * I18n Service class + * @class + */ class I18n { constructor() { + /** @type {Object} */ this.translations = {} + /** @type {Locale} */ this.currentLocale = 'de' + /** @type {Locale} */ this.fallbackLocale = 'de' + /** @type {Locale[]} */ this.supportedLocales = ['de', 'en', 'fr'] + /** @type {Set} */ this.subscribers = new Set() + /** @type {boolean} */ this.loaded = false } + /** + * Initialize i18n - load translations for detected locale + * @returns {Promise} + */ async init() { const savedLocale = localStorage.getItem('locale') const browserLocale = navigator.language.split('-')[0] @@ -20,6 +44,11 @@ class I18n { this.updateDOM() } + /** + * Load translations for a locale + * @param {Locale} locale - Locale to load + * @returns {Promise} + */ async loadTranslations(locale) { if (this.translations[locale]) return @@ -35,6 +64,11 @@ class I18n { } } + /** + * Change current locale + * @param {Locale} locale - New locale + * @returns {Promise} + */ async setLocale(locale) { if (!this.supportedLocales.includes(locale)) { console.warn(`Locale ${locale} is not supported`) @@ -50,10 +84,22 @@ class I18n { this.notifySubscribers() } + /** + * Get current locale + * @returns {Locale} + */ getLocale() { return this.currentLocale } + /** + * Translate a key with optional parameters + * @param {string} key - Translation key (e.g., 'home.title') + * @param {Object} [params={}] - Interpolation params + * @returns {string} Translated text or key if not found + * @example + * t('listing.expiresInDays', { days: 5 }) // "Noch 5 Tage" + */ t(key, params = {}) { const translations = this.translations[this.currentLocale] || this.translations[this.fallbackLocale] @@ -73,12 +119,23 @@ class I18n { return text } + /** + * Get nested value from object by dot-notation path + * @private + * @param {Object} obj - Object to search + * @param {string} path - Dot-notation path (e.g., 'home.title') + * @returns {*} Value or undefined + */ getNestedValue(obj, path) { return path.split('.').reduce((current, key) => { return current && current[key] !== undefined ? current[key] : undefined }, obj) } + /** + * Update all DOM elements with data-i18n attributes + * Called automatically on locale change + */ updateDOM() { document.querySelectorAll('[data-i18n]').forEach((el) => { const key = el.getAttribute('data-i18n') @@ -109,19 +166,34 @@ class I18n { }) } + /** + * Subscribe to locale changes + * @param {Function} callback - Called with new locale on change + * @returns {Function} Unsubscribe function + */ subscribe(callback) { this.subscribers.add(callback) return () => this.subscribers.delete(callback) } + /** @private */ notifySubscribers() { this.subscribers.forEach(callback => callback(this.currentLocale)) } + /** + * Get list of supported locales + * @returns {Locale[]} + */ getSupportedLocales() { return this.supportedLocales } + /** + * Get human-readable display name for a locale + * @param {Locale} locale + * @returns {string} + */ getLocaleDisplayName(locale) { const names = { de: 'Deutsch', diff --git a/js/router.js b/js/router.js index f74316f..c662fbe 100644 --- a/js/router.js +++ b/js/router.js @@ -1,23 +1,59 @@ +/** + * Hash-based Client-Side Router + * @module router + */ + +/** + * @typedef {Object} Route + * @property {string} path - Current path + * @property {Object} params - Route and query params + * @property {string} componentTag - Web component tag name + */ + +/** + * Hash-based router for SPA navigation + * @class + */ class Router { constructor() { + /** @type {Map} */ this.routes = new Map() + /** @type {Route|null} */ this.currentRoute = null + /** @type {HTMLElement|null} */ this.outlet = null + /** @type {Function|null} */ this.beforeNavigate = null + /** @type {Function|null} */ this.afterNavigate = null window.addEventListener('hashchange', () => this.handleRouteChange()) } + /** + * Set the outlet element where components will be rendered + * @param {HTMLElement} element - Container element + */ setOutlet(element) { this.outlet = element } + /** + * Register a route + * @param {string} path - Route path (e.g., '/listing/:id') + * @param {string} componentTag - Web component tag name + * @returns {Router} this for chaining + */ register(path, componentTag) { this.routes.set(path, componentTag) return this } + /** + * Parse current hash into path and query params + * @private + * @returns {{path: string, params: Object}} + */ parseHash() { const hash = window.location.hash.slice(1) || '/' const [path, queryString] = hash.split('?') @@ -26,6 +62,12 @@ class Router { return { path, params: Object.fromEntries(params) } } + /** + * Match a path to a registered route + * @private + * @param {string} path - Path to match + * @returns {{componentTag: string, params: Object}|null} + */ matchRoute(path) { if (this.routes.has(path)) { return { componentTag: this.routes.get(path), params: {} } @@ -57,6 +99,10 @@ class Router { return null } + /** + * Handle hash change event + * @private + */ async handleRouteChange() { const { path, params: queryParams } = this.parseHash() const match = this.matchRoute(path) @@ -86,6 +132,10 @@ class Router { } } + /** + * Render current route's component into outlet + * @private + */ render() { if (!this.outlet || !this.currentRoute) return @@ -120,6 +170,7 @@ class Router { }, { once: true }) } + /** @private */ renderNotFound() { if (!this.outlet) return @@ -128,6 +179,11 @@ class Router { this.outlet.appendChild(notFound) } + /** + * Navigate to a path + * @param {string} path - Target path + * @param {Object} [params={}] - Query parameters + */ navigate(path, params = {}) { let url = `#${path}` @@ -139,14 +195,20 @@ class Router { window.location.hash = url.slice(1) } + /** Navigate back in history */ back() { window.history.back() } + /** Navigate forward in history */ forward() { window.history.forward() } + /** + * Get current route info + * @returns {Route|null} + */ getCurrentRoute() { return this.currentRoute } diff --git a/js/services/directus.js b/js/services/directus.js index 3e36e3b..ad550c0 100644 --- a/js/services/directus.js +++ b/js/services/directus.js @@ -1,10 +1,43 @@ /** * Directus API Service for dgray.io * Connects to https://api.dgray.io/ + * @module services/directus */ +/** @type {string} */ const DIRECTUS_URL = 'https://api.dgray.io' +/** + * @typedef {Object} AuthTokens + * @property {string} access_token - JWT access token + * @property {string} refresh_token - Refresh token for session renewal + * @property {number} expires - Token expiry in seconds + */ + +/** + * @typedef {Object} Listing + * @property {string} id - UUID + * @property {string} title - Listing title + * @property {string} [description] - Listing description + * @property {number} price - Price value + * @property {string} currency - Currency code (EUR, USD, CHF, XMR) + * @property {string} status - Status (draft, published, sold, expired) + * @property {Object} [location] - Location object + * @property {Array} [images] - Array of image objects + */ + +/** + * @typedef {Object} Category + * @property {string} id - UUID + * @property {string} slug - URL-safe slug + * @property {string} [parent] - Parent category ID + * @property {Array} [translations] - Category translations + */ + +/** + * Main Directus API client + * @class + */ class DirectusService { constructor() { this.baseUrl = DIRECTUS_URL @@ -82,6 +115,13 @@ class DirectusService { // ==================== HTTP Methods ==================== + /** + * Make an HTTP request to the Directus API + * @param {string} endpoint - API endpoint path + * @param {RequestInit} [options={}] - Fetch options + * @returns {Promise} Response data or null for 204 + * @throws {DirectusError} On request failure + */ async request(endpoint, options = {}) { const url = `${this.baseUrl}${endpoint}` @@ -128,12 +168,24 @@ class DirectusService { } } + /** + * GET request with query parameters + * @param {string} endpoint - API endpoint + * @param {Object} [params={}] - Query parameters + * @returns {Promise} Response data + */ async get(endpoint, params = {}) { const queryString = this.buildQueryString(params) const url = queryString ? `${endpoint}?${queryString}` : endpoint return this.request(url, { method: 'GET' }) } + /** + * POST request with JSON body + * @param {string} endpoint - API endpoint + * @param {Object} data - Request body + * @returns {Promise} Response data + */ async post(endpoint, data) { return this.request(endpoint, { method: 'POST', @@ -141,6 +193,12 @@ class DirectusService { }) } + /** + * PATCH request with JSON body + * @param {string} endpoint - API endpoint + * @param {Object} data - Request body + * @returns {Promise} Response data + */ async patch(endpoint, data) { return this.request(endpoint, { method: 'PATCH', @@ -250,6 +308,15 @@ class DirectusService { // ==================== Listings (Anzeigen) ==================== + /** + * Get listings with filters and pagination + * @param {Object} [options={}] - Query options + * @param {number} [options.limit=20] - Max items to return + * @param {number} [options.page=1] - Page number + * @param {string} [options.category] - Filter by category slug + * @param {string} [options.search] - Search query + * @returns {Promise<{items: Listing[], total: number}>} Paginated listings + */ async getListings(options = {}) { const params = { fields: options.fields || [ @@ -291,6 +358,11 @@ class DirectusService { } } + /** + * Get a single listing by ID + * @param {string} id - Listing UUID + * @returns {Promise} Listing object or null + */ async getListing(id) { const response = await this.get(`/items/listings/${id}`, { fields: [ @@ -324,6 +396,11 @@ class DirectusService { return response.data } + /** + * Create a new listing + * @param {Object} data - Listing data + * @returns {Promise} Created listing + */ async createListing(data) { const response = await this.post('/items/listings', data) return response?.data || response @@ -656,6 +733,13 @@ class DirectusService { // ==================== Files (Dateien/Bilder) ==================== + /** + * Upload a file to Directus + * @param {File} file - File to upload + * @param {Object} [options={}] - Upload options + * @param {string} [options.folder] - Target folder ID + * @returns {Promise} Uploaded file data + */ async uploadFile(file, options = {}) { const formData = new FormData() formData.append('file', file) @@ -684,6 +768,15 @@ class DirectusService { return Promise.all(uploads) } + /** + * Get URL for a file with optional transformations + * @param {string} fileId - File UUID + * @param {Object} [options={}] - Transform options + * @param {number} [options.width] - Resize width + * @param {number} [options.height] - Resize height + * @param {string} [options.fit] - Fit mode (cover, contain, etc.) + * @returns {string|null} File URL or null + */ getFileUrl(fileId, options = {}) { if (!fileId) return null diff --git a/js/utils/helpers.js b/js/utils/helpers.js index 693a99d..9e361dc 100644 --- a/js/utils/helpers.js +++ b/js/utils/helpers.js @@ -1,8 +1,15 @@ +/** + * Utility helper functions + * @module utils/helpers + */ + /** * Escape HTML special characters to prevent XSS * Use for any user-generated content rendered via innerHTML * @param {string} str - Untrusted string - * @returns {string} - Escaped string safe for innerHTML + * @returns {string} Escaped string safe for innerHTML + * @example + * escapeHTML('') // '<script>...' */ export function escapeHTML(str) { if (str === null || str === undefined) return ''; @@ -17,8 +24,11 @@ export function escapeHTML(str) { /** * Format price with currency symbol * @param {number} price - Price value - * @param {string} currency - Currency code (EUR, USD, CHF, XMR) - * @returns {string} - Formatted price string + * @param {string} [currency='EUR'] - Currency code (EUR, USD, CHF, XMR) + * @returns {string} Formatted price string + * @example + * formatPrice(99.5, 'EUR') // '€ 99.50' + * formatPrice(0.5, 'XMR') // '0.5000 ɱ' */ export function formatPrice(price, currency = 'EUR') { if (price === null || price === undefined) return '–'; @@ -40,10 +50,13 @@ export function formatPrice(price, currency = 'EUR') { } /** - * Format relative time (e.g., "vor 2 Stunden") + * Format relative time (e.g., "vor 2 Stunden", "2 hours ago") + * Uses Intl.RelativeTimeFormat for localization * @param {Date|string} date - Date to format - * @param {string} locale - Locale code - * @returns {string} - Relative time string + * @param {string} [locale='de'] - Locale code + * @returns {string} Relative time string + * @example + * formatRelativeTime(new Date(Date.now() - 3600000), 'de') // 'vor 1 Stunde' */ export function formatRelativeTime(date, locale = 'de') { const now = new Date(); @@ -65,8 +78,10 @@ export function formatRelativeTime(date, locale = 'de') { /** * Debounce function calls * @param {Function} fn - Function to debounce - * @param {number} delay - Delay in ms - * @returns {Function} - Debounced function + * @param {number} [delay=300] - Delay in ms + * @returns {Function} Debounced function + * @example + * const debouncedSearch = debounce((q) => search(q), 500) */ export function debounce(fn, delay = 300) { let timeoutId; @@ -79,8 +94,10 @@ export function debounce(fn, delay = 300) { /** * Truncate string with ellipsis * @param {string} str - String to truncate - * @param {number} maxLength - Maximum length - * @returns {string} - Truncated string + * @param {number} [maxLength=100] - Maximum length + * @returns {string} Truncated string + * @example + * truncate('Hello World', 5) // 'Hell…' */ export function truncate(str, maxLength = 100) { if (!str || str.length <= maxLength) return str;