206 lines
5.9 KiB
Vue
206 lines
5.9 KiB
Vue
<script setup>
|
|
import {
|
|
ANNOUNCEMENT_SNOOZE_DAYS,
|
|
dismissAnnouncementForDays,
|
|
dismissAnnouncementForSession,
|
|
getAnnouncementBarTextColor,
|
|
isAnnouncementDismissed,
|
|
normalizeAnnouncementUrl
|
|
} from '~/lib/announcement-bar.js'
|
|
|
|
/** @type {number} 슬라이드 애니메이션 시간(ms) */
|
|
const SLIDE_MS = 320
|
|
|
|
const { data: siteSettings } = await useFetch('/api/site-settings', {
|
|
default: () => ({
|
|
announcementEnabled: false,
|
|
announcementText: '',
|
|
announcementUrl: '',
|
|
announcementBackgroundColor: '#15171a',
|
|
announcementAlignment: 'center',
|
|
updatedAt: null
|
|
})
|
|
})
|
|
|
|
/** DOM에 바를 둘지(애니메이션 종료 전까지 유지) */
|
|
const inDom = ref(false)
|
|
/** 펼침(아래로 슬라이드) 애니메이션 상태 */
|
|
const expanded = ref(false)
|
|
/** 숨김 처리 완료 여부 */
|
|
const dismissed = ref(false)
|
|
|
|
let closeTimer = null
|
|
|
|
/**
|
|
* 설정상 어나운스 바를 켤 수 있는지
|
|
* @returns {boolean} 가능 여부
|
|
*/
|
|
const isEligible = computed(() => {
|
|
if (!siteSettings.value?.announcementEnabled) {
|
|
return false
|
|
}
|
|
|
|
return Boolean((siteSettings.value?.announcementText || '').trim())
|
|
})
|
|
|
|
const announcementText = computed(() => (siteSettings.value?.announcementText || '').trim())
|
|
|
|
const announcementLink = computed(() => normalizeAnnouncementUrl(siteSettings.value?.announcementUrl || ''))
|
|
|
|
const snoozeLabel = computed(() => `${ANNOUNCEMENT_SNOOZE_DAYS}일간 보지 않기`)
|
|
|
|
const announcementAlignment = computed(() => siteSettings.value?.announcementAlignment === 'left' ? 'left' : 'center')
|
|
|
|
const barStyle = computed(() => {
|
|
const backgroundColor = siteSettings.value?.announcementBackgroundColor || '#15171a'
|
|
return {
|
|
backgroundColor,
|
|
color: getAnnouncementBarTextColor(backgroundColor)
|
|
}
|
|
})
|
|
|
|
/**
|
|
* 어나운스 바를 펼친다.
|
|
* @returns {Promise<void>}
|
|
*/
|
|
const openBar = async () => {
|
|
inDom.value = true
|
|
expanded.value = false
|
|
await nextTick()
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
expanded.value = true
|
|
})
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 어나운스 바를 접고 DOM에서 제거한다.
|
|
* @param {() => void} persistDismiss - localStorage·sessionStorage 저장
|
|
* @returns {void}
|
|
*/
|
|
const closeBar = (persistDismiss) => {
|
|
if (!inDom.value) {
|
|
return
|
|
}
|
|
|
|
persistDismiss()
|
|
expanded.value = false
|
|
|
|
if (closeTimer) {
|
|
clearTimeout(closeTimer)
|
|
}
|
|
|
|
closeTimer = setTimeout(() => {
|
|
inDom.value = false
|
|
dismissed.value = true
|
|
closeTimer = null
|
|
}, SLIDE_MS)
|
|
}
|
|
|
|
/**
|
|
* 이번 방문(세션) 동안만 닫는다.
|
|
* @returns {void}
|
|
*/
|
|
const dismissForSession = () => {
|
|
closeBar(() => dismissAnnouncementForSession(siteSettings.value))
|
|
}
|
|
|
|
/**
|
|
* N일간 보지 않기로 닫는다.
|
|
* @returns {void}
|
|
*/
|
|
const dismissForSnooze = () => {
|
|
closeBar(() => dismissAnnouncementForDays(siteSettings.value))
|
|
}
|
|
|
|
watch(() => siteSettings.value?.updatedAt, async () => {
|
|
if (!import.meta.client) {
|
|
return
|
|
}
|
|
|
|
const hidden = isAnnouncementDismissed(siteSettings.value)
|
|
dismissed.value = hidden
|
|
|
|
if (!isEligible.value || hidden) {
|
|
expanded.value = false
|
|
inDom.value = false
|
|
return
|
|
}
|
|
|
|
await openBar()
|
|
})
|
|
|
|
onMounted(() => {
|
|
dismissed.value = isAnnouncementDismissed(siteSettings.value)
|
|
|
|
if (isEligible.value && !dismissed.value) {
|
|
openBar()
|
|
}
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
if (closeTimer) {
|
|
clearTimeout(closeTimer)
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
v-if="inDom"
|
|
class="site-announcement-bar-shell grid transition-[grid-template-rows] duration-300 ease-out motion-reduce:transition-none"
|
|
:class="expanded ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'"
|
|
:style="{ transitionDuration: `${SLIDE_MS}ms` }"
|
|
>
|
|
<div class="min-h-0 overflow-hidden">
|
|
<div
|
|
class="site-announcement-bar relative z-30 w-full text-center text-sm font-medium"
|
|
:style="barStyle"
|
|
role="region"
|
|
aria-label="사이트 공지"
|
|
:aria-hidden="(!expanded).toString()"
|
|
>
|
|
<div
|
|
class="site-announcement-bar__inner relative mx-auto flex min-h-9 max-w-[1294px] items-center gap-3 px-4 py-2.5 sm:px-6 lg:px-8"
|
|
:class="announcementAlignment === 'left' ? 'justify-start' : 'justify-center'"
|
|
>
|
|
<component
|
|
:is="announcementLink ? 'a' : 'span'"
|
|
class="site-announcement-bar__text min-w-0 line-clamp-2 px-6 sm:px-8"
|
|
:class="[
|
|
announcementLink ? 'hover:underline' : '',
|
|
announcementAlignment === 'left' ? 'flex-1 text-left' : 'flex-1 text-center'
|
|
]"
|
|
:href="announcementLink || undefined"
|
|
:target="announcementLink ? '_blank' : undefined"
|
|
:rel="announcementLink ? 'noreferrer' : undefined"
|
|
>
|
|
{{ announcementText }}
|
|
</component>
|
|
<div class="site-announcement-bar__actions absolute top-1/2 right-3 flex shrink-0 -translate-y-1/2 items-center gap-2 sm:right-4 lg:right-5">
|
|
<button
|
|
class="site-announcement-bar__snooze whitespace-nowrap text-xs font-medium underline-offset-2 opacity-90 transition hover:underline hover:opacity-100 sm:text-[13px]"
|
|
type="button"
|
|
@click="dismissForSnooze"
|
|
>
|
|
{{ snoozeLabel }}
|
|
</button>
|
|
<button
|
|
class="site-announcement-bar__close inline-flex size-7 items-center justify-center rounded-full opacity-80 transition hover:opacity-100"
|
|
type="button"
|
|
aria-label="이번 방문 동안 닫기"
|
|
@click="dismissForSession"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<path d="M18 6 6 18" />
|
|
<path d="m6 6 12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|