linguaielts-api / fronted /src /components /layout /NotificationBell.vue
AnhviNguyen
Forecast, dashboard, pronunciation, nginx fixes (exclude large ML weights)
3092643
Raw
History Blame Contribute Delete
6.92 kB
<template>
<div class="relative" ref="rootRef">
<button
type="button"
class="relative flex h-8 w-8 items-center justify-center rounded-md border border-[var(--border)] bg-[var(--bg)] text-[var(--ink2)] transition-colors hover:border-[#34d399] hover:text-[var(--ink)]"
:aria-label="`Thông báo${unread ? ` (${unread} chưa đọc)` : ''}`"
@click.stop="togglePanel"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/>
<path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/>
</svg>
<span
v-if="unread > 0"
class="absolute -right-0.5 -top-0.5 flex h-4 min-w-[16px] items-center justify-center rounded-full bg-[#ef4444] px-1 text-[9px] font-bold text-white"
>{{ unread > 9 ? '9+' : unread }}</span>
</button>
<Transition name="dropdown">
<div
v-if="open"
class="absolute right-0 top-10 z-50 w-[min(100vw-2rem,360px)] overflow-hidden rounded-2xl border border-[var(--border)] bg-[var(--bg-surface)] shadow-xl"
>
<div class="flex items-center justify-between border-b border-[var(--border)] px-4 py-3">
<span class="text-[13px] font-semibold text-[var(--ink)]">Thông báo</span>
<button
v-if="unread > 0"
type="button"
class="text-[11px] font-semibold text-[var(--spotify-green-dark)] hover:underline"
@click="markAll"
>Đánh dấu đã đọc</button>
</div>
<div v-if="store.loading" class="px-4 py-6 text-center text-[12px] text-[var(--ink3)]">Đang tải...</div>
<div v-else-if="!store.items.length" class="px-4 py-8 text-center text-[12px] text-[var(--ink3)]">
Chưa có thông báo
</div>
<ul v-else class="max-h-[320px] overflow-y-auto">
<li
v-for="n in store.items"
:key="n.id"
class="border-b border-[var(--border)] last:border-0"
>
<button
type="button"
class="flex w-full gap-3 px-4 py-3 text-left transition-colors hover:bg-[var(--bg)]"
:class="!n.is_read ? 'bg-[var(--green-bg)]' : ''"
@click="openNotification(n)"
>
<span
class="mt-1 h-2 w-2 shrink-0 rounded-full"
:class="n.is_read ? 'bg-transparent' : 'bg-[var(--spotify-green)]'"
/>
<span class="min-w-0 flex-1">
<span class="block text-[12px] font-semibold text-[var(--ink)]">{{ n.title }}</span>
<span class="mt-0.5 block text-[11px] leading-snug text-[var(--ink3)] line-clamp-2">{{ n.body }}</span>
</span>
</button>
</li>
</ul>
<div class="border-t border-[var(--border)] p-3">
<button
type="button"
class="w-full rounded-lg border border-[var(--border)] py-2 text-[12px] font-semibold text-[var(--ink2)] hover:bg-[var(--bg)]"
@click="showSettings = !showSettings"
>{{ showSettings ? 'Ẩn cài đặt' : 'Cài đặt nhắc nhở' }}</button>
<form v-if="showSettings" class="mt-3 space-y-2" @submit.prevent="saveSettings">
<label class="flex items-center gap-2 text-[12px]">
<input v-model="form.reminder_enabled" type="checkbox" class="rounded" />
Bật nhắc nhở
</label>
<label class="block text-[11px] text-[var(--ink3)]">
Giờ nhắc
<input v-model="form.reminder_time" type="time" class="mt-1 w-full rounded border border-[var(--border)] px-2 py-1.5 text-[12px]" />
</label>
<label class="block text-[11px] text-[var(--ink3)]">
Kênh
<select v-model="form.channel" class="mt-1 w-full rounded border border-[var(--border)] px-2 py-1.5 text-[12px]">
<option value="in_app">Trong app</option>
<option value="email">Email</option>
<option value="both">Cả hai</option>
</select>
</label>
<label class="flex items-center gap-2 text-[12px]">
<input v-model="form.email_daily_digest" type="checkbox" class="rounded" />
Email nhắc hàng ngày
</label>
<label class="flex items-center gap-2 text-[12px] opacity-70">
<input v-model="form.push_enabled" type="checkbox" class="rounded" disabled />
Push PWA (sắp có)
</label>
<button type="submit" class="btn btn-primary w-full text-[12px]">Lưu</button>
<p v-if="settingsSaved" class="text-center text-[11px] text-[var(--spotify-green-dark)]">Đã lưu cài đặt</p>
</form>
</div>
</div>
</Transition>
</div>
</template>
<script setup>
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useNotificationsStore } from '@/stores/notifications.js'
const store = useNotificationsStore()
const router = useRouter()
const open = ref(false)
const showSettings = ref(false)
const settingsSaved = ref(false)
const rootRef = ref(null)
const unread = computed(() => store.unreadCount)
const form = reactive({
reminder_enabled: true,
reminder_time: '09:00',
channel: 'in_app',
email_daily_digest: false,
push_enabled: false,
})
function togglePanel() {
open.value = !open.value
if (open.value) {
store.load()
store.loadSettings()
}
}
async function openNotification(n) {
if (!n.is_read) await store.markRead(n.id)
open.value = false
if (n.link_path) router.push(n.link_path)
}
async function markAll() {
await store.markAllRead()
}
async function saveSettings() {
await store.saveSettings({
reminder_enabled: form.reminder_enabled,
reminder_time: form.reminder_time.length === 5 ? `${form.reminder_time}:00` : form.reminder_time,
channel: form.channel,
email_daily_digest: form.email_daily_digest,
push_enabled: form.push_enabled,
})
settingsSaved.value = true
setTimeout(() => { settingsSaved.value = false }, 2500)
}
watch(
() => store.settings,
(s) => {
if (!s) return
form.reminder_enabled = s.reminder_enabled
form.reminder_time = (s.reminder_time || '09:00:00').slice(0, 5)
form.channel = s.channel || 'in_app'
form.email_daily_digest = s.email_daily_digest
form.push_enabled = s.push_enabled
},
{ immediate: true }
)
function onDocClick(e) {
if (rootRef.value && !rootRef.value.contains(e.target)) open.value = false
}
onMounted(() => {
store.load()
document.addEventListener('click', onDocClick)
})
onUnmounted(() => document.removeEventListener('click', onDocClick))
</script>