<template lang='pug'> q-page.admin-general .row.q-pa-md.items-center .col-auto img.admin-icon.animated.fadeInLeft(src='/_assets/icons/fluent-web.svg') .col.q-pl-md .text-h5.text-primary.animated.fadeInLeft {{ t('admin.general.title') }} .text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ t('admin.general.subtitle') }} .col-auto q-btn.q-mr-sm.acrylic-btn( icon='las la-question-circle' flat color='grey' href='https://docs.js.wiki/admin/general' target='_blank' type='a' ) q-btn.q-mr-sm.acrylic-btn( icon='las la-redo-alt' flat color='secondary' :loading='state.loading > 0' @click='load' ) q-btn( unelevated icon='fa-solid fa-check' :label='t(`common.actions.apply`)' color='secondary' @click='save' :disabled='state.loading > 0' ) q-separator(inset) .row.q-pa-md.q-col-gutter-md .col-12.col-lg-7 //- ----------------------- //- Site Info //- ----------------------- q-card.shadow-1.q-pb-sm q-card-section .text-subtitle1 {{t('admin.general.siteInfo')}} q-item blueprint-icon(icon='home') q-item-section q-item-label {{t(`admin.general.siteTitle`)}} q-item-label(caption) {{t(`admin.general.siteTitleHint`)}} q-item-section q-input( outlined v-model='state.config.title' dense :rules='rulesTitle' hide-bottom-space :aria-label='t(`admin.general.siteTitle`)' ) q-separator.q-my-sm(inset) q-item blueprint-icon(icon='select-all') q-item-section q-item-label {{t(`admin.general.siteDescription`)}} q-item-label(caption) {{t(`admin.general.siteDescriptionHint`)}} q-item-section q-input( outlined v-model='state.config.description' dense :aria-label='t(`admin.general.siteDescription`)' ) q-separator.q-my-sm(inset) q-item blueprint-icon(icon='dns') q-item-section q-item-label {{t(`admin.general.siteHostname`)}} q-item-label(caption) {{t(`admin.general.siteHostnameHint`)}} q-item-section q-input( outlined v-model='state.config.hostname' dense :rules='rulesHostname' hide-bottom-space :aria-label='t(`admin.general.siteHostname`)' ) //- ----------------------- //- Footer / Copyright //- ----------------------- q-card.shadow-1.q-pb-sm.q-mt-md q-card-section .text-subtitle1 {{t('admin.general.footerCopyright')}} q-item blueprint-icon(icon='building') q-item-section q-item-label {{t(`admin.general.companyName`)}} q-item-label(caption) {{t(`admin.general.companyNameHint`)}} q-item-section q-input( outlined v-model='state.config.company' dense :aria-label='t(`admin.general.companyName`)' ) q-separator.q-my-sm(inset) q-item blueprint-icon(icon='copyright') q-item-section q-item-label {{t(`admin.general.contentLicense`)}} q-item-label(caption) {{t(`admin.general.contentLicenseHint`)}} q-item-section q-select( outlined v-model='state.config.contentLicense' :options='contentLicenses' option-value='value' option-label='text' emit-value map-options dense :aria-label='t(`admin.general.contentLicense`)' ) q-separator.q-my-sm(inset) q-item blueprint-icon(icon='subtitles') q-item-section q-item-label {{t(`admin.general.footerExtra`)}} q-item-label(caption) {{t(`admin.general.footerExtraHint`)}} q-item-section q-input( outlined v-model='state.config.footerExtra' dense :aria-label='t(`admin.general.footerExtra`)' ) //- ----------------------- //- FEATURES //- ----------------------- q-card.shadow-1.q-pb-sm.q-mt-md q-card-section .text-subtitle1 {{t('admin.general.features')}} q-item(tag='label') blueprint-icon(icon='discussion-forum') q-item-section q-item-label {{t(`admin.general.allowComments`)}} q-item-label(caption) {{t(`admin.general.allowCommentsHint`)}} q-item-section(avatar) q-toggle( v-model='state.config.features.comments' color='primary' checked-icon='las la-check' unchecked-icon='las la-times' :aria-label='t(`admin.general.allowComments`)' ) q-separator.q-my-sm(inset) q-item(tag='label') blueprint-icon(icon='pen') q-item-section q-item-label {{t(`admin.general.allowContributions`)}} q-item-label(caption) {{t(`admin.general.allowContributionsHint`)}} q-item-section(avatar) q-toggle( v-model='state.config.features.contributions' color='primary' checked-icon='las la-check' unchecked-icon='las la-times' :aria-label='t(`admin.general.allowContributions`)' ) q-separator.q-my-sm(inset) q-item(tag='label') blueprint-icon(icon='administrator-male') q-item-section q-item-label {{t(`admin.general.allowProfile`)}} q-item-label(caption) {{t(`admin.general.allowProfileHint`)}} q-item-section(avatar) q-toggle( v-model='state.config.features.profile' color='primary' checked-icon='las la-check' unchecked-icon='las la-times' :aria-label='t(`admin.general.allowProfile`)' ) q-separator.q-my-sm(inset) q-item blueprint-icon(icon='star-half-empty') q-item-section q-item-label {{t(`admin.general.allowRatings`)}} q-item-label(caption) {{t(`admin.general.allowRatingsHint`)}} q-item-section.col-auto q-btn-toggle( v-model='state.config.features.ratingsMode' push glossy no-caps toggle-color='primary' :options='ratingsModes' ) q-separator.q-my-sm(inset) q-item(tag='label') blueprint-icon(icon='search') q-item-section q-item-label {{t(`admin.general.allowSearch`)}} q-item-label(caption) {{t(`admin.general.allowSearchHint`)}} q-item-section(avatar) q-toggle( v-model='state.config.features.search' color='primary' checked-icon='las la-check' unchecked-icon='las la-times' :aria-label='t(`admin.general.allowSearch`)' ) //- ----------------------- //- URL Handling //- ----------------------- q-card.shadow-1.q-pb-sm.q-mt-md q-card-section .text-subtitle1 {{t('admin.general.urlHandling')}} q-item blueprint-icon(icon='sort-by-follow-up-date') q-item-section q-item-label {{t(`admin.general.pageExtensions`)}} q-item-label(caption) {{t(`admin.general.pageExtensionsHint`)}} q-item-section q-input( outlined v-model='state.config.pageExtensions' dense :aria-label='t(`admin.general.pageExtensions`)' ) .col-12.col-lg-5 //- ----------------------- //- Logo //- ----------------------- q-card.shadow-1.q-pb-sm q-card-section .text-subtitle1 {{t('admin.general.logo')}} q-item blueprint-icon.self-start(icon='butterfly', indicator, :indicator-text='t(`admin.extensions.requiresSharp`)') q-item-section .flex q-item-section q-item-label {{t(`admin.general.logoUpl`)}} q-item-label(caption) {{t(`admin.general.logoUplHint`)}} q-item-section.col-auto q-btn( label='Upload' unelevated icon='las la-upload' color='primary' text-color='white' @click='uploadLogo' ) q-toolbar.bg-header.q-mt-md.rounded-borders.text-white( dark style='height: 64px;' ) q-btn(dense, flat, to='/') q-avatar( v-if='state.config.logoText' size='34px' square ) img(src='/_assets/logo-wikijs.svg') img( v-else src='https://m.media-amazon.com/images/G/01/audibleweb/arya/navigation/audible_logo._V517446980_.svg' style='height: 34px;' ) q-toolbar-title.text-h6(v-if='state.config.logoText') {{state.config.title}} q-separator.q-my-sm(inset) q-item(tag='label') blueprint-icon(icon='information') q-item-section q-item-label {{t(`admin.general.displaySiteTitle`)}} q-item-label(caption) {{t(`admin.general.displaySiteTitleHint`)}} q-item-section(avatar) q-toggle( v-model='state.config.logoText' color='primary' checked-icon='las la-check' unchecked-icon='las la-times' :aria-label='t(`admin.general.displaySiteTitle`)' ) q-separator.q-my-sm(inset) q-item blueprint-icon.self-start(icon='starfish', indicator, :indicator-text='t(`admin.extensions.requiresSharp`)') q-item-section .flex q-item-section q-item-label {{t(`admin.general.favicon`)}} q-item-label(caption) {{t(`admin.general.faviconHint`)}} q-item-section.col-auto q-btn( label='Upload' unelevated icon='las la-upload' color='primary' text-color='white' @click='uploadFavicon' ) .admin-general-favicontabs.q-mt-md div q-avatar( size='24px' square ) img(src='/_assets/logo-wikijs.svg') .text-caption.q-ml-sm {{state.config.title}} div q-icon(name='las la-otter', size='24px', color='grey') .text-caption.q-ml-sm Lorem ipsum div q-icon(name='las la-mountain', size='24px', color='grey') .text-caption.q-ml-sm Dolor sit amet... //- ----------------------- //- Defaults //- ----------------------- q-card.shadow-1.q-pb-sm.q-mt-md(v-if='state.config.defaults') q-card-section .text-subtitle1 {{t('admin.general.defaults')}} q-item blueprint-icon(icon='timezone') q-item-section q-item-label {{t(`admin.general.defaultTimezone`)}} q-item-label(caption) {{t(`admin.general.defaultTimezoneHint`)}} q-item-section q-select( outlined v-model='state.config.defaults.timezone' :options='dataStore.timezones' option-value='value' option-label='text' emit-value map-options dense options-dense :virtual-scroll-slice-size='1000' :aria-label='t(`admin.general.defaultTimezone`)' ) q-separator.q-my-sm(inset) q-item blueprint-icon(icon='calendar') q-item-section q-item-label {{t(`admin.general.defaultDateFormat`)}} q-item-label(caption) {{t(`admin.general.defaultDateFormatHint`)}} q-item-section q-select( outlined v-model='state.config.defaults.dateFormat' emit-value map-options dense :aria-label='t(`admin.general.defaultDateFormat`)' :options='dateFormats' ) q-separator.q-my-sm(inset) q-item blueprint-icon(icon='clock') q-item-section q-item-label {{t(`admin.general.defaultTimeFormat`)}} q-item-label(caption) {{t(`admin.general.defaultTimeFormatHint`)}} q-item-section.col-auto q-btn-toggle( v-model='state.config.defaults.timeFormat' push glossy no-caps toggle-color='primary' :options='timeFormats' ) //- ----------------------- //- SEO //- ----------------------- q-card.shadow-1.q-pb-sm.q-mt-md(v-if='state.config.robots') q-card-section .text-subtitle1 SEO q-item(tag='label') blueprint-icon(icon='bot') q-item-section q-item-label {{t(`admin.general.searchAllowIndexing`)}} q-item-label(caption) {{t(`admin.general.searchAllowIndexingHint`)}} q-item-section(avatar) q-toggle( v-model='state.config.robots.index' color='primary' checked-icon='las la-check' unchecked-icon='las la-times' :aria-label='t(`admin.general.searchAllowIndexing`)' ) q-separator.q-my-sm(inset) q-item(tag='label') blueprint-icon(icon='polyline') q-item-section q-item-label {{t(`admin.general.searchAllowFollow`)}} q-item-label(caption) {{t(`admin.general.searchAllowFollowHint`)}} q-item-section(avatar) q-toggle( v-model='state.config.robots.follow' color='primary' checked-icon='las la-check' unchecked-icon='las la-times' :aria-label='t(`admin.general.searchAllowFollow`)' ) q-separator.q-my-sm(inset) q-item(tag='label') blueprint-icon(icon='genealogy') q-item-section q-item-label {{t(`admin.general.sitemap`)}} q-item-label(caption) {{t(`admin.general.sitemapHint`)}} q-item-section(avatar) q-toggle( v-model='state.config.sitemap' color='primary' checked-icon='las la-check' unchecked-icon='las la-times' :aria-label='t(`admin.general.sitemap`)' ) </template> <script setup> import gql from 'graphql-tag' import { cloneDeep } from 'lodash-es' import { useI18n } from 'vue-i18n' import { useMeta, useQuasar } from 'quasar' import { onMounted, reactive, watch } from 'vue' import { useAdminStore } from 'src/stores/admin' import { useSiteStore } from 'src/stores/site' import { useDataStore } from 'src/stores/data' // QUASAR const $q = useQuasar() // STORES const adminStore = useAdminStore() const siteStore = useSiteStore() const dataStore = useDataStore() // I18N const { t } = useI18n() // META useMeta({ title: t('admin.dashboard.title') }) // DATA const state = reactive({ loading: 0, config: { hostname: '', title: '', description: '', company: '', contentLicense: '', footerExtra: '', pageExtensions: '', logoText: false, ratings: { index: false, follow: false }, features: { ratings: false, ratingsMode: 'off', comments: false, contributions: false, profile: false }, defaults: { timezone: '', dateFormat: '', timeFormat: '' }, robots: { index: false, follow: false }, sitemap: false } }) const contentLicenses = [ { value: '', text: t('common.license.none') }, { value: 'alr', text: t('common.license.alr') }, { value: 'cc0', text: t('common.license.cc0') }, { value: 'ccby', text: t('common.license.ccby') }, { value: 'ccbysa', text: t('common.license.ccbysa') }, { value: 'ccbynd', text: t('common.license.ccbynd') }, { value: 'ccbync', text: t('common.license.ccbync') }, { value: 'ccbyncsa', text: t('common.license.ccbyncsa') }, { value: 'ccbyncnd', text: t('common.license.ccbyncnd') } ] const ratingsModes = [ { value: 'off', label: t('admin.general.ratingsOff') }, { value: 'thumbs', label: t('admin.general.ratingsThumbs') }, { value: 'stars', label: t('admin.general.ratingsStars') } ] const dateFormats = [ { value: '', label: t('profile.localeDefault') }, { value: 'DD/MM/YYYY', label: 'DD/MM/YYYY' }, { value: 'DD.MM.YYYY', label: 'DD.MM.YYYY' }, { value: 'MM/DD/YYYY', label: 'MM/DD/YYYY' }, { value: 'YYYY-MM-DD', label: 'YYYY-MM-DD' }, { value: 'YYYY/MM/DD', label: 'YYYY/MM/DD' } ] const timeFormats = [ { value: '12h', label: t('admin.general.defaultTimeFormat12h') }, { value: '24h', label: t('admin.general.defaultTimeFormat24h') } ] const rulesTitle = [ val => /^[^<>"]+$/.test(val) || t('admin.general.siteTitleInvalidChars') ] const rulesHostname = [ val => /^(([a-z0-9.-]+)|([*]{1}))$/.test(val) || t('admin.general.siteHostnameInvalid') ] // WATCHERS watch(() => adminStore.currentSiteId, (newValue) => { load() }) // METHODS async function load () { state.loading++ $q.loading.show() const resp = await APOLLO_CLIENT.query({ query: gql` query getSite ( $id: UUID! ) { siteById( id: $id ) { id hostname isEnabled title description company contentLicense footerExtra pageExtensions logoText sitemap robots { index follow } features { comments ratings ratingsMode contributions profile search } defaults { timezone dateFormat timeFormat } } } `, variables: { id: adminStore.currentSiteId }, fetchPolicy: 'network-only' }) state.config = cloneDeep(resp?.data?.siteById) $q.loading.hide() state.loading-- } async function save () { state.loading++ try { await APOLLO_CLIENT.mutate({ mutation: gql` mutation saveSite ( $id: UUID! $patch: SiteUpdateInput! ) { updateSite ( id: $id patch: $patch ) { operation { succeeded slug message } } } `, variables: { id: adminStore.currentSiteId, patch: { hostname: state.config.hostname ?? '', title: state.config.title ?? '', description: state.config.description ?? '', company: state.config.company ?? '', contentLicense: state.config.contentLicense ?? '', footerExtra: state.config.footerExtra ?? '', pageExtensions: state.config.pageExtensions ?? '', logoText: state.config.logoText ?? false, sitemap: state.config.sitemap ?? false, robots: { index: state.config.robots?.index ?? false, follow: state.config.robots?.follow ?? false }, features: { comments: state.config.features?.comments ?? false, ratings: (state.config.features?.ratings || 'off') !== 'off', ratingsMode: state.config.features?.ratingsMode ?? 'off', contributions: state.config.features?.contributions ?? false, profile: state.config.features?.profile ?? false, search: state.config.features?.search ?? false }, defaults: { timezone: state.config.defaults?.timezone ?? 'America/New_York', dateFormat: state.config.defaults?.dateFormat ?? 'YYYY-MM-DD', timeFormat: state.config.defaults?.timeFormat ?? '12h' } } } }) $q.notify({ type: 'positive', message: t('admin.general.saveSuccess') }) await adminStore.fetchSites() if (adminStore.currentSiteId === siteStore.id) { siteStore.$patch({ title: state.config.title, description: state.config.description, company: state.config.company, contentLicense: state.config.contentLicense }) } } catch (err) { $q.notify({ type: 'negative', message: 'Failed to save site configuration.', caption: err.message }) } state.loading-- } async function uploadLogo () { const input = document.createElement('input') input.type = 'file' input.onchange = async e => { state.loading++ try { await APOLLO_CLIENT.mutate({ mutation: gql` mutation uploadLogo ( $id: UUID! $image: Upload! ) { uploadSiteLogo ( id: $id image: $image ) { status { succeeded slug message } } } `, variables: { id: adminStore.currentSiteId, image: e.target.files[0] } }) $q.notify({ type: 'positive', message: t('admin.general.logoUploadSuccess') }) } catch (err) { $q.notify({ type: 'negative', message: 'Failed to upload site logo.', caption: err.message }) } state.loading-- } input.click() } async function uploadFavicon () { const input = document.createElement('input') input.type = 'file' input.onchange = async e => { state.loading++ try { await APOLLO_CLIENT.mutate({ mutation: gql` mutation uploadFavicon ( $id: UUID! $image: Upload! ) { uploadSiteFavicon ( id: $id image: $image ) { status { succeeded slug message } } } `, variables: { id: adminStore.currentSiteId, image: e.target.files[0] } }) $q.notify({ type: 'positive', message: t('admin.general.faviconUploadSuccess') }) } catch (err) { $q.notify({ type: 'negative', message: 'Failed to upload site favicon.', caption: err.message }) } state.loading-- } input.click() } // MOUNTED onMounted(() => { if (adminStore.currentSiteId) { load() } }) </script> <style lang='scss'> .admin-general { &-favicontabs { overflow: hidden; border-radius: 5px; background-color: rgba(0,0,0,.1); display: flex; padding: 5px 5px 0 12px; > div { display: flex; padding: 4px 12px; position: relative; align-items: center; &:first-child { border: 1px solid #FFF; border-bottom: none; border-radius: 7px 7px 0 0; box-shadow: 0 0 5px 0 rgba(0,0,0,.2); @at-root .body--light & { background: linear-gradient(to top, #FFF, rgba(255,255,255,.75)); border-color: #FFF; } @at-root .body--dark & { background: linear-gradient(to top, $dark-6, $dark-5); border-color: $dark-6; } } } } } </style>