Refactor app architecture and clean local artifacts
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
|
||||
import {
|
||||
normalizeSocialIconKey,
|
||||
simpleIconForKey,
|
||||
socialIconOptionForKey,
|
||||
} from '../../lib/socialIcons'
|
||||
import { useAwardsStore } from '../../stores/awards'
|
||||
import type { AdminContentForm, SocialLinkForm } from './adminContentTypes'
|
||||
|
||||
function createEmptyForm(): AdminContentForm {
|
||||
return {
|
||||
hostDisplayName: '',
|
||||
hostTagline: '',
|
||||
newsletterUrl: '',
|
||||
privacyEmail: '',
|
||||
privacyPolicyContent: '',
|
||||
imprintUrl: '',
|
||||
contactUrl: '',
|
||||
sponsorsUrl: '',
|
||||
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.contactUrl = settings.contactUrl
|
||||
form.sponsorsUrl = settings.sponsorsUrl
|
||||
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 privacyPreviewBlocks = computed(() =>
|
||||
form.privacyPolicyContent
|
||||
.split(/\n{2,}/)
|
||||
.map((block) => block.trim())
|
||||
.filter(Boolean),
|
||||
)
|
||||
|
||||
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(event: Event, index: number) {
|
||||
const value = (event.target as HTMLSelectElement).value
|
||||
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: form.privacyPolicyContent,
|
||||
imprintUrl: form.imprintUrl,
|
||||
contactUrl: form.contactUrl,
|
||||
sponsorsUrl: form.sponsorsUrl,
|
||||
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,
|
||||
privacyPreviewBlocks,
|
||||
privacyUpdatedLabel,
|
||||
addSocialLink,
|
||||
removeSocialLink,
|
||||
addFaqItem,
|
||||
removeFaqItem,
|
||||
isUploadedIcon,
|
||||
selectedSocialIconValue,
|
||||
handleSocialIconSelection,
|
||||
hasSocialIconPreview,
|
||||
socialIconModeLabel,
|
||||
socialSimpleIconPath,
|
||||
socialSimpleIconColor,
|
||||
handleSocialIconUpload,
|
||||
clearSocialIcon,
|
||||
saveSiteSettings,
|
||||
adminSiteSettings: computed(() => store.adminSiteSettings),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user