Files
vtuber-awards/frontend/src/components/admin/useAdminContentManager.ts
T
2026-06-26 18:09:29 +02:00

303 lines
8.7 KiB
TypeScript

import { computed, reactive, ref, watch } from 'vue'
import {
normalizeSocialIconKey,
simpleIconForKey,
socialIconOptionForKey,
} from '../../lib/socialIcons'
import { privacyContentForStorage, privacyContentToHtml } from '../../lib/privacyContent'
import { useAwardsStore } from '../../stores/awards'
import type { AdminContentForm, SocialLinkForm } from './adminContentTypes'
function createEmptyForm(): AdminContentForm {
return {
hostDisplayName: '',
hostTagline: '',
newsletterUrl: '',
privacyEmail: '',
privacyPolicyContent: '',
imprintUrl: '',
imprintContent: '',
contactUrl: '',
contactContent: '',
sponsorsUrl: '',
sponsorsContent: '',
socialLinks: [],
faq: [],
}
}
function readFileAsDataUrl(file: File) {
return new Promise<string>((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(String(reader.result ?? ''))
reader.onerror = () => reject(new Error('Icon konnte nicht gelesen werden.'))
reader.readAsDataURL(file)
})
}
async function validateRasterDimensions(file: File) {
if (file.type === 'image/svg+xml') {
return
}
const objectUrl = URL.createObjectURL(file)
try {
await new Promise<void>((resolve, reject) => {
const image = new Image()
image.onload = () => {
if (image.naturalWidth > 512 || image.naturalHeight > 512) {
reject(new Error('Icon ist zu groß. Maximal erlaubt sind 512 x 512 px.'))
return
}
resolve()
}
image.onerror = () => reject(new Error('Icon-Abmessungen konnten nicht geprüft werden.'))
image.src = objectUrl
})
} finally {
URL.revokeObjectURL(objectUrl)
}
}
export function useAdminContentManager() {
const store = useAwardsStore()
const form = reactive(createEmptyForm())
const saving = ref(false)
const saveMessage = ref('')
const saveError = ref('')
const privacyPreviewOpen = ref(false)
const iconUploadError = ref('')
watch(
() => store.adminSiteSettings,
(settings) => {
form.hostDisplayName = settings.hostDisplayName
form.hostTagline = settings.hostTagline
form.newsletterUrl = settings.newsletterUrl
form.privacyEmail = settings.privacyEmail
form.privacyPolicyContent = settings.privacyPolicyContent
form.imprintUrl = settings.imprintUrl
form.imprintContent = settings.imprintContent
form.contactUrl = settings.contactUrl
form.contactContent = settings.contactContent
form.sponsorsUrl = settings.sponsorsUrl
form.sponsorsContent = settings.sponsorsContent
form.socialLinks = settings.socialLinks.map((item) => ({
label: item.label ?? '',
platform: item.platform ?? '',
icon: item.icon ?? '',
url: item.url ?? '',
showOnHost: item.showOnHost ?? true,
showOnCommunity: item.showOnCommunity ?? true,
}))
form.faq = settings.faq.map((item) => ({
question: item.question ?? '',
answer: item.answer ?? '',
}))
},
{ immediate: true, deep: true },
)
const privacyPreviewHtml = computed(() => privacyContentToHtml(form.privacyPolicyContent))
const privacyUpdatedLabel = computed(() => {
const updatedAt = store.adminSiteSettings.privacyPolicyUpdatedAt
if (!updatedAt) {
return 'Noch keine Änderung gespeichert'
}
return new Intl.DateTimeFormat('de-DE', {
dateStyle: 'medium',
timeStyle: 'short',
}).format(new Date(updatedAt))
})
function addSocialLink() {
form.socialLinks.push({
label: 'Neu',
platform: 'website',
icon: '',
url: '',
showOnHost: true,
showOnCommunity: true,
})
}
function removeSocialLink(index: number) {
form.socialLinks.splice(index, 1)
}
function addFaqItem() {
form.faq.push({ question: '', answer: '' })
}
function removeFaqItem(index: number) {
form.faq.splice(index, 1)
}
function isUploadedIcon(icon: string) {
return icon.startsWith('data:image/')
}
function socialIconKey(social: SocialLinkForm) {
return normalizeSocialIconKey(social.icon || social.platform)
}
function selectedSocialIconValue(social: SocialLinkForm) {
return socialIconOptionForKey(social.platform)
? normalizeSocialIconKey(social.platform)
: 'custom'
}
function handleSocialIconSelection(value: string, index: number) {
const social = form.socialLinks[index]
if (!social) {
return
}
if (value === 'custom') {
if (socialIconOptionForKey(social.platform)) {
social.platform = ''
}
return
}
const option = socialIconOptionForKey(value)
social.platform = value
if (!social.label || social.label === 'Neu' || social.label === 'Website / Fallback') {
social.label = option?.label ?? value
}
if (social.icon && !isUploadedIcon(social.icon)) {
social.icon = ''
}
}
function hasSocialIconPreview(social: SocialLinkForm) {
return Boolean(social.label || social.platform || social.icon)
}
function socialIconModeLabel(social: SocialLinkForm) {
if (isUploadedIcon(social.icon)) {
return 'hochgeladenes Custom Icon'
}
const icon = simpleIconForKey(socialIconKey(social))
if (icon) {
return `eingebautes ${icon.title} Icon`
}
return 'generisches Fallback Icon'
}
function socialSimpleIconPath(social: SocialLinkForm) {
return simpleIconForKey(socialIconKey(social))?.path ?? ''
}
function socialSimpleIconColor(social: SocialLinkForm) {
return `#${simpleIconForKey(socialIconKey(social))?.hex ?? '8b5cf6'}`
}
async function handleSocialIconUpload(event: Event, index: number) {
iconUploadError.value = ''
const input = event.target as HTMLInputElement
const file = input.files?.[0]
input.value = ''
if (!file) {
return
}
const allowedTypes = ['image/png', 'image/jpeg', 'image/webp', 'image/svg+xml']
if (!allowedTypes.includes(file.type)) {
iconUploadError.value = 'Bitte PNG, JPG, WebP oder SVG hochladen.'
return
}
const maxBytes = 256 * 1024
if (file.size > maxBytes) {
iconUploadError.value = 'Icon ist zu groß. Maximal erlaubt sind 256 KB.'
return
}
try {
await validateRasterDimensions(file)
form.socialLinks[index].icon = await readFileAsDataUrl(file)
} catch (error) {
iconUploadError.value = error instanceof Error ? error.message : 'Icon konnte nicht hochgeladen werden.'
}
}
function clearSocialIcon(index: number) {
form.socialLinks[index].icon = ''
}
async function saveSiteSettings(sectionLabel = 'Landingpage-Inhalte') {
saving.value = true
saveMessage.value = ''
saveError.value = ''
try {
await store.updateAdminSiteSettings({
hostDisplayName: form.hostDisplayName,
hostTagline: form.hostTagline,
newsletterUrl: form.newsletterUrl,
privacyEmail: form.privacyEmail,
privacyPolicyContent: privacyContentForStorage(form.privacyPolicyContent),
imprintUrl: form.imprintUrl,
imprintContent: privacyContentForStorage(form.imprintContent),
contactUrl: form.contactUrl,
contactContent: privacyContentForStorage(form.contactContent),
sponsorsUrl: form.sponsorsUrl,
sponsorsContent: privacyContentForStorage(form.sponsorsContent),
socialLinks: form.socialLinks
.map((item) => ({
label: item.label.trim(),
platform: item.platform.trim(),
icon: item.icon.trim(),
url: item.url.trim(),
showOnHost: item.showOnHost,
showOnCommunity: item.showOnCommunity,
}))
.filter((item) => item.label && item.platform && item.url),
faq: form.faq
.map((item) => ({
question: item.question.trim(),
answer: item.answer.trim(),
}))
.filter((item) => item.question && item.answer),
})
saveMessage.value = `${sectionLabel} gespeichert und auf der Landingpage aktualisiert.`
} catch (error) {
saveError.value = error instanceof Error ? error.message : 'Inhalte konnten nicht gespeichert werden.'
} finally {
saving.value = false
}
}
return {
form,
saving,
saveMessage,
saveError,
privacyPreviewOpen,
iconUploadError,
privacyPreviewHtml,
privacyUpdatedLabel,
addSocialLink,
removeSocialLink,
addFaqItem,
removeFaqItem,
isUploadedIcon,
selectedSocialIconValue,
handleSocialIconSelection,
hasSocialIconPreview,
socialIconModeLabel,
socialSimpleIconPath,
socialSimpleIconColor,
handleSocialIconUpload,
clearSocialIcon,
saveSiteSettings,
adminSiteSettings: computed(() => store.adminSiteSettings),
}
}