303 lines
8.7 KiB
TypeScript
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),
|
|
}
|
|
}
|