docs: add JSDoc documentation to core modules (directus, i18n, router, helpers)

This commit is contained in:
2026-02-05 15:25:57 +01:00
parent 43add27732
commit bd7a259d72
4 changed files with 254 additions and 10 deletions

View File

@@ -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<Locale, Object>} */
this.translations = {}
/** @type {Locale} */
this.currentLocale = 'de'
/** @type {Locale} */
this.fallbackLocale = 'de'
/** @type {Locale[]} */
this.supportedLocales = ['de', 'en', 'fr']
/** @type {Set<Function>} */
this.subscribers = new Set()
/** @type {boolean} */
this.loaded = false
}
/**
* Initialize i18n - load translations for detected locale
* @returns {Promise<void>}
*/
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<void>}
*/
async loadTranslations(locale) {
if (this.translations[locale]) return
@@ -35,6 +64,11 @@ class I18n {
}
}
/**
* Change current locale
* @param {Locale} locale - New locale
* @returns {Promise<void>}
*/
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<string, string|number>} [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',

View File

@@ -1,23 +1,59 @@
/**
* Hash-based Client-Side Router
* @module router
*/
/**
* @typedef {Object} Route
* @property {string} path - Current path
* @property {Object<string, string>} params - Route and query params
* @property {string} componentTag - Web component tag name
*/
/**
* Hash-based router for SPA navigation
* @class
*/
class Router {
constructor() {
/** @type {Map<string, string>} */
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<string, string>} [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
}

View File

@@ -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<Object|null>} 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<Object>} 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<Object>} 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<Object>} 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|null>} 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<Listing>} 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<Object>} 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

View File

@@ -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>alert("XSS")</script>') // '&lt;script&gt;...'
*/
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;