import 'tippy.js/animations/shift-toward-subtle.css'
import 'tippy.js/dist/tippy.css'
import tippy from 'tippy.js' // optional
import type {ClientResponseError} from 'pocketbase'
class AppStore {
theme: string = $state('dark')
showMobileNavigation: boolean = $state(false)
seo = $state({
title: 'Title',
description: 'Description',
})
// meta: {
// customer: CustomersResponse | undefined
// site: SitesResponse | undefined
// record: RecordsResponse | undefined
// contact: ContactsResponse | undefined
// file: FilesResponse | undefined
// user: UsersResponse | undefined
// } = $state({
// customer: undefined,
// site: undefined,
// record: undefined,
// contact: undefined,
// file: undefined,
// user: undefined,
// })
showGlobalActionMenu = $state(false)
confirm: {
show: boolean
title: string
text: string
error: boolean | ClientResponseError
yes: () => void
no: () => void
request: (props: any) => void
close: () => void
} = $state({
show: false,
title: '',
text: '',
error: false,
yes: () => {
},
no: () => {
},
request: (props: any) => {
this.confirm = {...this.confirm, ...props}
this.confirm.show = true
},
close: () => {
this.confirm.show = false
this.confirm.title = ''
this.confirm.text = ''
},
})
/**
Global Hotkey handler
Ignores hotkey handling when focus of user is in generic input-tags or [contenteditable]
@example
*/
hotkey = (event: KeyboardEvent, condition: boolean, callback: Function) => {
// Sicherstellen, dass `event` und `event.target` vorhanden sind
if (!event || !event.target) {
return
}
// Liste ignorierter Tag-Namen
const ignoredTags = ['INPUT', 'TEXTAREA', 'SELECT', 'OPTION']
// Standard-TAG-Prüfung mit optionaler Erweiterung
const isIgnoredTag = ignoredTags.includes((event.target as HTMLElement).tagName?.toUpperCase())
// Fokusierbare oder editierbare Inhalte ignorieren
const isEditable =
(event.target as HTMLElement).isContentEditable || // `contenteditable=true`
(event.target as HTMLElement).hasAttribute('contenteditable') // Attribut prüfen für dynamische Inhalte
if (isIgnoredTag || isEditable) {
// Rücksprung, keine Aktion ausführen
return
}
// Bedingung und Callback ausführen
if (condition) {
event.preventDefault() // Standardaktion abbrechen
callback() // Callback-Funktion ausführen
}
}
toggleTheme = () => {
this.theme = this.theme === 'dark' ? 'light' : 'dark'
}
}
export const app = new AppStore()
export const utils = {
getFileSize: async (fileUrl: string): Promise => {
try {
const response = await fetch(fileUrl, {method: 'HEAD'})
const contentLength = response.headers.get('content-length')
const bytes = contentLength ? parseInt(contentLength, 10) : 0
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
if (bytes === 0) return '0 Bytes'
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
} catch (error) {
console.error('Error fetching file size:', error)
return 'Unbekannt'
}
},
sanitize: (input: string) => {
return input.replace(/<\/?("[^"]*"|'[^']*'|[^>])*(>|$)/g, '')
},
excerpt: (input: string, length: number = 20) => {
if (input.length <= 20) return input
return input.substring(0, length) + '…'
},
slugify: (input: string) => {
if (!input) return ''
// make lower case and trim
var slug = input.toLowerCase().trim()
// remove accents from charaters
slug = slug.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
// replace invalid chars with spaces
slug = slug.replace(/[^a-z0-9\s-]/g, ' ').trim()
// replace multiple spaces or hyphens with a single hyphen
slug = slug.replace(/[\s-]+/g, '-')
return slug
},
}
/**
* Tooltip für ein Element. Appweit die einzige Art, einen Hinweis an einen
* Knopf zu hängen — `title` überlassen wir dem Browser nicht mehr: Es
* erscheint erst nach einer Sekunde, sieht auf jedem System anders aus und
* lässt sich auf Touch-Geräten gar nicht aufrufen.
*
* Die Action beherrscht Aktualisieren und Aufräumen: Ein Text, der sich
* ändert (»Erledigt« / »Wieder öffnen«), muss auch im Tooltip wechseln, und
* eine Instanz, die ein entferntes Element überlebt, hinge als leere Blase
* im Dokument.
*/
export const tooltip = (
element: HTMLElement,
props: { active?: boolean; content?: string; showOnCreate?: boolean } = {},
) => {
const options = (p: typeof props) => ({
animation: 'shift-toward-subtle',
allowHTML: false,
content: p.content ?? '',
showOnCreate: p.showOnCreate,
})
const wanted = (p: typeof props) => p.active !== false && !!p.content?.trim()
let instance = wanted(props) ? tippy(element, options(props)) : null
return {
update(next: typeof props) {
if (!wanted(next)) {
instance?.destroy()
instance = null
return
}
if (instance) instance.setProps(options(next))
else instance = tippy(element, options(next))
},
destroy() {
instance?.destroy()
instance = null
},
}
}