feat(frontend): bell, chat, uploads, glass, teams UI, fixes
- Notification bell with invites (accept/decline refreshes workspaces) and unread badge (polling + socket push); team chat view realtime - Avatar/background uploads in settings + glassmorphism widgets toggle; background layer resolves /uploads URLs; full-bleed blurred backdrop - Sidebar: visible creation errors, working sub-pages (parent_id), TEAM badge workspaces, refresh button, chat nav; admin creates users - Tasks: per-team boards, assignee select with avatars; admin tabs for users/teams/groups (team workspaces, permission checkboxes) - Register view removed (admin-only accounts); login localized; prettier emoji set; scaling/overflow + mobile fixes
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import api from '../api';
|
||||
import { getSocket } from '../socket';
|
||||
import { useLocaleStore } from '../stores/locale';
|
||||
import { useWorkspaceStore } from '../stores/workspace';
|
||||
|
||||
const L = useLocaleStore();
|
||||
const ws = useWorkspaceStore();
|
||||
const router = useRouter();
|
||||
|
||||
const open = ref(false);
|
||||
const invites = ref([]);
|
||||
const notifs = ref([]);
|
||||
const unread = ref(0);
|
||||
let timer = null;
|
||||
|
||||
const total = computed(() => invites.value.length + unread.value);
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const { data } = await api.get('/invites/mine');
|
||||
invites.value = data.invites;
|
||||
const n = await api.get('/notifications/mine');
|
||||
notifs.value = n.data.notifications;
|
||||
unread.value = n.data.unread;
|
||||
} catch { /* offline */ }
|
||||
}
|
||||
|
||||
async function accept(inv) {
|
||||
await api.post(`/invites/${inv.id}/accept`);
|
||||
invites.value = invites.value.filter((i) => i.id !== inv.id);
|
||||
await ws.fetchWorkspaces();
|
||||
if (ws.currentId) {
|
||||
await ws.fetchBooks();
|
||||
await ws.fetchPages();
|
||||
}
|
||||
router.push('/');
|
||||
}
|
||||
|
||||
async function decline(inv) {
|
||||
await api.post(`/invites/${inv.id}/decline`);
|
||||
invites.value = invites.value.filter((i) => i.id !== inv.id);
|
||||
}
|
||||
|
||||
async function markAll() {
|
||||
await api.post('/notifications/read-all');
|
||||
notifs.value.forEach((n) => (n.is_read = true));
|
||||
unread.value = 0;
|
||||
}
|
||||
|
||||
async function markOne(n) {
|
||||
await api.patch(`/notifications/${n.id}/read`);
|
||||
n.is_read = true;
|
||||
unread.value = Math.max(0, unread.value - 1);
|
||||
}
|
||||
|
||||
function onPush() {
|
||||
load();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
load();
|
||||
timer = setInterval(load, 20000);
|
||||
const s = getSocket();
|
||||
s?.on('notification:new', onPush);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
clearInterval(timer);
|
||||
getSocket()?.off('notification:new', onPush);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bell">
|
||||
<button class="ghost icon-btn bell-btn" @click="open = !open" :title="L.t('bell.title')">
|
||||
🔔<span v-if="total" class="badge">{{ total }}</span>
|
||||
</button>
|
||||
<div v-if="open" class="card bell-pop">
|
||||
<h4 style="margin: 0 0 8px">🔔 {{ L.t('bell.title') }}</h4>
|
||||
<div v-if="invites.length">
|
||||
<div class="side-label">{{ L.t('bell.invites') }}</div>
|
||||
<div v-for="inv in invites" :key="inv.id" class="card invite-card">
|
||||
<strong>{{ inv.workspace_name }}</strong>
|
||||
<div class="muted">{{ L.t('bell.as') }} {{ inv.role }} · {{ inv.inviter_name || '?' }}</div>
|
||||
<div class="row" style="margin-top: 6px">
|
||||
<button class="mini" @click="accept(inv)">✓ {{ L.t('bell.accept') }}</button>
|
||||
<button class="ghost mini" @click="decline(inv)">✕ {{ L.t('bell.decline') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="side-label">{{ L.t('bell.notifs') }}</div>
|
||||
<div v-if="!notifs.length" class="muted">{{ L.t('bell.empty') }}</div>
|
||||
<div
|
||||
v-for="n in notifs.slice(0, 12)" :key="n.id"
|
||||
class="notif" :class="{ unread: !n.is_read }" @click="markOne(n)"
|
||||
>
|
||||
<strong>{{ n.title }}</strong>
|
||||
<div class="muted">{{ n.body }}</div>
|
||||
</div>
|
||||
<button v-if="unread" class="ghost mini" style="margin-top: 6px" @click="markAll">
|
||||
{{ L.t('bell.readAll') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user