docs: add JSDoc documentation to core modules (directus, i18n, router, helpers)
This commit is contained in:
72
js/i18n.js
72
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 {
|
class I18n {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
/** @type {Object<Locale, Object>} */
|
||||||
this.translations = {}
|
this.translations = {}
|
||||||
|
/** @type {Locale} */
|
||||||
this.currentLocale = 'de'
|
this.currentLocale = 'de'
|
||||||
|
/** @type {Locale} */
|
||||||
this.fallbackLocale = 'de'
|
this.fallbackLocale = 'de'
|
||||||
|
/** @type {Locale[]} */
|
||||||
this.supportedLocales = ['de', 'en', 'fr']
|
this.supportedLocales = ['de', 'en', 'fr']
|
||||||
|
/** @type {Set<Function>} */
|
||||||
this.subscribers = new Set()
|
this.subscribers = new Set()
|
||||||
|
/** @type {boolean} */
|
||||||
this.loaded = false
|
this.loaded = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize i18n - load translations for detected locale
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
async init() {
|
async init() {
|
||||||
const savedLocale = localStorage.getItem('locale')
|
const savedLocale = localStorage.getItem('locale')
|
||||||
const browserLocale = navigator.language.split('-')[0]
|
const browserLocale = navigator.language.split('-')[0]
|
||||||
@@ -20,6 +44,11 @@ class I18n {
|
|||||||
this.updateDOM()
|
this.updateDOM()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load translations for a locale
|
||||||
|
* @param {Locale} locale - Locale to load
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
async loadTranslations(locale) {
|
async loadTranslations(locale) {
|
||||||
if (this.translations[locale]) return
|
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) {
|
async setLocale(locale) {
|
||||||
if (!this.supportedLocales.includes(locale)) {
|
if (!this.supportedLocales.includes(locale)) {
|
||||||
console.warn(`Locale ${locale} is not supported`)
|
console.warn(`Locale ${locale} is not supported`)
|
||||||
@@ -50,10 +84,22 @@ class I18n {
|
|||||||
this.notifySubscribers()
|
this.notifySubscribers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current locale
|
||||||
|
* @returns {Locale}
|
||||||
|
*/
|
||||||
getLocale() {
|
getLocale() {
|
||||||
return this.currentLocale
|
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 = {}) {
|
t(key, params = {}) {
|
||||||
const translations = this.translations[this.currentLocale]
|
const translations = this.translations[this.currentLocale]
|
||||||
|| this.translations[this.fallbackLocale]
|
|| this.translations[this.fallbackLocale]
|
||||||
@@ -73,12 +119,23 @@ class I18n {
|
|||||||
return text
|
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) {
|
getNestedValue(obj, path) {
|
||||||
return path.split('.').reduce((current, key) => {
|
return path.split('.').reduce((current, key) => {
|
||||||
return current && current[key] !== undefined ? current[key] : undefined
|
return current && current[key] !== undefined ? current[key] : undefined
|
||||||
}, obj)
|
}, obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update all DOM elements with data-i18n attributes
|
||||||
|
* Called automatically on locale change
|
||||||
|
*/
|
||||||
updateDOM() {
|
updateDOM() {
|
||||||
document.querySelectorAll('[data-i18n]').forEach((el) => {
|
document.querySelectorAll('[data-i18n]').forEach((el) => {
|
||||||
const key = el.getAttribute('data-i18n')
|
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) {
|
subscribe(callback) {
|
||||||
this.subscribers.add(callback)
|
this.subscribers.add(callback)
|
||||||
return () => this.subscribers.delete(callback)
|
return () => this.subscribers.delete(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @private */
|
||||||
notifySubscribers() {
|
notifySubscribers() {
|
||||||
this.subscribers.forEach(callback => callback(this.currentLocale))
|
this.subscribers.forEach(callback => callback(this.currentLocale))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of supported locales
|
||||||
|
* @returns {Locale[]}
|
||||||
|
*/
|
||||||
getSupportedLocales() {
|
getSupportedLocales() {
|
||||||
return this.supportedLocales
|
return this.supportedLocales
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get human-readable display name for a locale
|
||||||
|
* @param {Locale} locale
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
getLocaleDisplayName(locale) {
|
getLocaleDisplayName(locale) {
|
||||||
const names = {
|
const names = {
|
||||||
de: 'Deutsch',
|
de: 'Deutsch',
|
||||||
|
|||||||
62
js/router.js
62
js/router.js
@@ -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 {
|
class Router {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
/** @type {Map<string, string>} */
|
||||||
this.routes = new Map()
|
this.routes = new Map()
|
||||||
|
/** @type {Route|null} */
|
||||||
this.currentRoute = null
|
this.currentRoute = null
|
||||||
|
/** @type {HTMLElement|null} */
|
||||||
this.outlet = null
|
this.outlet = null
|
||||||
|
/** @type {Function|null} */
|
||||||
this.beforeNavigate = null
|
this.beforeNavigate = null
|
||||||
|
/** @type {Function|null} */
|
||||||
this.afterNavigate = null
|
this.afterNavigate = null
|
||||||
|
|
||||||
window.addEventListener('hashchange', () => this.handleRouteChange())
|
window.addEventListener('hashchange', () => this.handleRouteChange())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the outlet element where components will be rendered
|
||||||
|
* @param {HTMLElement} element - Container element
|
||||||
|
*/
|
||||||
setOutlet(element) {
|
setOutlet(element) {
|
||||||
this.outlet = 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) {
|
register(path, componentTag) {
|
||||||
this.routes.set(path, componentTag)
|
this.routes.set(path, componentTag)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse current hash into path and query params
|
||||||
|
* @private
|
||||||
|
* @returns {{path: string, params: Object}}
|
||||||
|
*/
|
||||||
parseHash() {
|
parseHash() {
|
||||||
const hash = window.location.hash.slice(1) || '/'
|
const hash = window.location.hash.slice(1) || '/'
|
||||||
const [path, queryString] = hash.split('?')
|
const [path, queryString] = hash.split('?')
|
||||||
@@ -26,6 +62,12 @@ class Router {
|
|||||||
return { path, params: Object.fromEntries(params) }
|
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) {
|
matchRoute(path) {
|
||||||
if (this.routes.has(path)) {
|
if (this.routes.has(path)) {
|
||||||
return { componentTag: this.routes.get(path), params: {} }
|
return { componentTag: this.routes.get(path), params: {} }
|
||||||
@@ -57,6 +99,10 @@ class Router {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle hash change event
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
async handleRouteChange() {
|
async handleRouteChange() {
|
||||||
const { path, params: queryParams } = this.parseHash()
|
const { path, params: queryParams } = this.parseHash()
|
||||||
const match = this.matchRoute(path)
|
const match = this.matchRoute(path)
|
||||||
@@ -86,6 +132,10 @@ class Router {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render current route's component into outlet
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
render() {
|
render() {
|
||||||
if (!this.outlet || !this.currentRoute) return
|
if (!this.outlet || !this.currentRoute) return
|
||||||
|
|
||||||
@@ -120,6 +170,7 @@ class Router {
|
|||||||
}, { once: true })
|
}, { once: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @private */
|
||||||
renderNotFound() {
|
renderNotFound() {
|
||||||
if (!this.outlet) return
|
if (!this.outlet) return
|
||||||
|
|
||||||
@@ -128,6 +179,11 @@ class Router {
|
|||||||
this.outlet.appendChild(notFound)
|
this.outlet.appendChild(notFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to a path
|
||||||
|
* @param {string} path - Target path
|
||||||
|
* @param {Object<string, string>} [params={}] - Query parameters
|
||||||
|
*/
|
||||||
navigate(path, params = {}) {
|
navigate(path, params = {}) {
|
||||||
let url = `#${path}`
|
let url = `#${path}`
|
||||||
|
|
||||||
@@ -139,14 +195,20 @@ class Router {
|
|||||||
window.location.hash = url.slice(1)
|
window.location.hash = url.slice(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Navigate back in history */
|
||||||
back() {
|
back() {
|
||||||
window.history.back()
|
window.history.back()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Navigate forward in history */
|
||||||
forward() {
|
forward() {
|
||||||
window.history.forward()
|
window.history.forward()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current route info
|
||||||
|
* @returns {Route|null}
|
||||||
|
*/
|
||||||
getCurrentRoute() {
|
getCurrentRoute() {
|
||||||
return this.currentRoute
|
return this.currentRoute
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,43 @@
|
|||||||
/**
|
/**
|
||||||
* Directus API Service for dgray.io
|
* Directus API Service for dgray.io
|
||||||
* Connects to https://api.dgray.io/
|
* Connects to https://api.dgray.io/
|
||||||
|
* @module services/directus
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/** @type {string} */
|
||||||
const DIRECTUS_URL = 'https://api.dgray.io'
|
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 {
|
class DirectusService {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.baseUrl = DIRECTUS_URL
|
this.baseUrl = DIRECTUS_URL
|
||||||
@@ -82,6 +115,13 @@ class DirectusService {
|
|||||||
|
|
||||||
// ==================== HTTP Methods ====================
|
// ==================== 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 = {}) {
|
async request(endpoint, options = {}) {
|
||||||
const url = `${this.baseUrl}${endpoint}`
|
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 = {}) {
|
async get(endpoint, params = {}) {
|
||||||
const queryString = this.buildQueryString(params)
|
const queryString = this.buildQueryString(params)
|
||||||
const url = queryString ? `${endpoint}?${queryString}` : endpoint
|
const url = queryString ? `${endpoint}?${queryString}` : endpoint
|
||||||
return this.request(url, { method: 'GET' })
|
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) {
|
async post(endpoint, data) {
|
||||||
return this.request(endpoint, {
|
return this.request(endpoint, {
|
||||||
method: 'POST',
|
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) {
|
async patch(endpoint, data) {
|
||||||
return this.request(endpoint, {
|
return this.request(endpoint, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
@@ -250,6 +308,15 @@ class DirectusService {
|
|||||||
|
|
||||||
// ==================== Listings (Anzeigen) ====================
|
// ==================== 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 = {}) {
|
async getListings(options = {}) {
|
||||||
const params = {
|
const params = {
|
||||||
fields: options.fields || [
|
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) {
|
async getListing(id) {
|
||||||
const response = await this.get(`/items/listings/${id}`, {
|
const response = await this.get(`/items/listings/${id}`, {
|
||||||
fields: [
|
fields: [
|
||||||
@@ -324,6 +396,11 @@ class DirectusService {
|
|||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new listing
|
||||||
|
* @param {Object} data - Listing data
|
||||||
|
* @returns {Promise<Listing>} Created listing
|
||||||
|
*/
|
||||||
async createListing(data) {
|
async createListing(data) {
|
||||||
const response = await this.post('/items/listings', data)
|
const response = await this.post('/items/listings', data)
|
||||||
return response?.data || response
|
return response?.data || response
|
||||||
@@ -656,6 +733,13 @@ class DirectusService {
|
|||||||
|
|
||||||
// ==================== Files (Dateien/Bilder) ====================
|
// ==================== 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 = {}) {
|
async uploadFile(file, options = {}) {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
@@ -684,6 +768,15 @@ class DirectusService {
|
|||||||
return Promise.all(uploads)
|
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 = {}) {
|
getFileUrl(fileId, options = {}) {
|
||||||
if (!fileId) return null
|
if (!fileId) return null
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Utility helper functions
|
||||||
|
* @module utils/helpers
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Escape HTML special characters to prevent XSS
|
* Escape HTML special characters to prevent XSS
|
||||||
* Use for any user-generated content rendered via innerHTML
|
* Use for any user-generated content rendered via innerHTML
|
||||||
* @param {string} str - Untrusted string
|
* @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>') // '<script>...'
|
||||||
*/
|
*/
|
||||||
export function escapeHTML(str) {
|
export function escapeHTML(str) {
|
||||||
if (str === null || str === undefined) return '';
|
if (str === null || str === undefined) return '';
|
||||||
@@ -17,8 +24,11 @@ export function escapeHTML(str) {
|
|||||||
/**
|
/**
|
||||||
* Format price with currency symbol
|
* Format price with currency symbol
|
||||||
* @param {number} price - Price value
|
* @param {number} price - Price value
|
||||||
* @param {string} currency - Currency code (EUR, USD, CHF, XMR)
|
* @param {string} [currency='EUR'] - Currency code (EUR, USD, CHF, XMR)
|
||||||
* @returns {string} - Formatted price string
|
* @returns {string} Formatted price string
|
||||||
|
* @example
|
||||||
|
* formatPrice(99.5, 'EUR') // '€ 99.50'
|
||||||
|
* formatPrice(0.5, 'XMR') // '0.5000 ɱ'
|
||||||
*/
|
*/
|
||||||
export function formatPrice(price, currency = 'EUR') {
|
export function formatPrice(price, currency = 'EUR') {
|
||||||
if (price === null || price === undefined) return '–';
|
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 {Date|string} date - Date to format
|
||||||
* @param {string} locale - Locale code
|
* @param {string} [locale='de'] - Locale code
|
||||||
* @returns {string} - Relative time string
|
* @returns {string} Relative time string
|
||||||
|
* @example
|
||||||
|
* formatRelativeTime(new Date(Date.now() - 3600000), 'de') // 'vor 1 Stunde'
|
||||||
*/
|
*/
|
||||||
export function formatRelativeTime(date, locale = 'de') {
|
export function formatRelativeTime(date, locale = 'de') {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -65,8 +78,10 @@ export function formatRelativeTime(date, locale = 'de') {
|
|||||||
/**
|
/**
|
||||||
* Debounce function calls
|
* Debounce function calls
|
||||||
* @param {Function} fn - Function to debounce
|
* @param {Function} fn - Function to debounce
|
||||||
* @param {number} delay - Delay in ms
|
* @param {number} [delay=300] - Delay in ms
|
||||||
* @returns {Function} - Debounced function
|
* @returns {Function} Debounced function
|
||||||
|
* @example
|
||||||
|
* const debouncedSearch = debounce((q) => search(q), 500)
|
||||||
*/
|
*/
|
||||||
export function debounce(fn, delay = 300) {
|
export function debounce(fn, delay = 300) {
|
||||||
let timeoutId;
|
let timeoutId;
|
||||||
@@ -79,8 +94,10 @@ export function debounce(fn, delay = 300) {
|
|||||||
/**
|
/**
|
||||||
* Truncate string with ellipsis
|
* Truncate string with ellipsis
|
||||||
* @param {string} str - String to truncate
|
* @param {string} str - String to truncate
|
||||||
* @param {number} maxLength - Maximum length
|
* @param {number} [maxLength=100] - Maximum length
|
||||||
* @returns {string} - Truncated string
|
* @returns {string} Truncated string
|
||||||
|
* @example
|
||||||
|
* truncate('Hello World', 5) // 'Hell…'
|
||||||
*/
|
*/
|
||||||
export function truncate(str, maxLength = 100) {
|
export function truncate(str, maxLength = 100) {
|
||||||
if (!str || str.length <= maxLength) return str;
|
if (!str || str.length <= maxLength) return str;
|
||||||
|
|||||||
Reference in New Issue
Block a user