- Settings window with tabs: profile (name, email, avatar, password), appearance (light/dark/system), language (de/en/zh), background (gradient presets, custom URL, blur toggle); persisted server-side - i18n dictionaries (de/en/zh) applied across nav, sidebar, tasks, calendar, time tracking, admin, invites and comments - Greeting header with time-of-day message, live clock and date; cooler theme, cards, kanban, calendar grid and data-table styling - Shared sidebar: nav, collapsible books with per-book pages, top-level pages, search; user chip opens settings - Tasks kanban with priorities, overdue flags and book/page links; calendar month view fed by task deadlines; time view with entries, durations and totals; top-right live timer with optional book link - User discovery autocomplete in the invite card; editor gains tables and checklists; standalone theme buttons removed
154 lines
5.0 KiB
Vue
154 lines
5.0 KiB
Vue
<script setup>
|
||
import { onMounted, ref } from 'vue';
|
||
import { useRouter } from 'vue-router';
|
||
import api from '../api';
|
||
import { useAuthStore } from '../stores/auth';
|
||
import { useWorkspaceStore } from '../stores/workspace';
|
||
import { useLocaleStore } from '../stores/locale';
|
||
import { useUiStore } from '../stores/ui';
|
||
import PageTree from './PageTree.vue';
|
||
import Avatar from './Avatar.vue';
|
||
|
||
const props = defineProps({ activePageId: String });
|
||
const auth = useAuthStore();
|
||
const ws = useWorkspaceStore();
|
||
const L = useLocaleStore();
|
||
const ui = useUiStore();
|
||
const router = useRouter();
|
||
|
||
const newWs = ref('');
|
||
const newPage = ref('');
|
||
const newBook = ref('');
|
||
const search = ref('');
|
||
const results = ref([]);
|
||
const collapsed = ref(JSON.parse(localStorage.getItem('collapsedBooks') || '{}'));
|
||
|
||
function toggleBook(id) {
|
||
collapsed.value[id] = !collapsed.value[id];
|
||
localStorage.setItem('collapsedBooks', JSON.stringify(collapsed.value));
|
||
}
|
||
|
||
async function refresh() {
|
||
await ws.fetchBooks();
|
||
await ws.fetchPages();
|
||
}
|
||
|
||
async function switchWs(id) {
|
||
ws.setCurrent(id);
|
||
await refresh();
|
||
}
|
||
|
||
async function createWs() {
|
||
if (!newWs.value.trim()) return;
|
||
await ws.createWorkspace(newWs.value.trim());
|
||
newWs.value = '';
|
||
await refresh();
|
||
}
|
||
|
||
async function createPage(bookId = null) {
|
||
const title = newPage.value.trim() || 'Untitled';
|
||
const { data } = await api.post('/pages', {
|
||
workspace_id: ws.currentId, book_id: bookId, title,
|
||
});
|
||
newPage.value = '';
|
||
await ws.fetchPages();
|
||
router.push(`/page/${data.page.id}`);
|
||
}
|
||
|
||
async function createBook() {
|
||
if (!newBook.value.trim()) return;
|
||
await ws.createBook(newBook.value.trim(), '📚');
|
||
newBook.value = '';
|
||
}
|
||
|
||
async function deleteBook(id) {
|
||
if (!confirm('🗑️?')) return;
|
||
await api.delete(`/books/${id}`);
|
||
await refresh();
|
||
}
|
||
|
||
async function doSearch() {
|
||
if (!search.value.trim()) { results.value = []; return; }
|
||
const { data } = await api.get('/pages/search/query', {
|
||
params: { q: search.value, workspace_id: ws.currentId },
|
||
});
|
||
results.value = data.results;
|
||
}
|
||
|
||
function logout() {
|
||
auth.logout();
|
||
router.push('/login');
|
||
}
|
||
|
||
onMounted(refresh);
|
||
</script>
|
||
|
||
<template>
|
||
<aside class="sidebar">
|
||
<div class="user-chip">
|
||
<Avatar :name="auth.user?.name" :avatar="auth.user?.avatar" :size="32" />
|
||
<strong style="flex: 1">{{ auth.user?.name }}</strong>
|
||
<button class="ghost icon-btn" @click="ui.openSettings()" title="⚙️">⚙️</button>
|
||
</div>
|
||
|
||
<select :value="ws.currentId" @change="switchWs($event.target.value)">
|
||
<option v-for="w in ws.workspaces" :key="w.id" :value="w.id">{{ w.name }} ({{ w.role }})</option>
|
||
</select>
|
||
<div class="row">
|
||
<input v-model="newWs" :placeholder="L.t('side.newWorkspace')" @keyup.enter="createWs" />
|
||
<button @click="createWs">+</button>
|
||
</div>
|
||
|
||
<nav class="side-nav">
|
||
<router-link to="/">🏠 {{ L.t('nav.dashboard') }}</router-link>
|
||
<router-link to="/tasks">✅ {{ L.t('nav.tasks') }}</router-link>
|
||
<router-link to="/calendar">🗓️ {{ L.t('nav.calendar') }}</router-link>
|
||
<router-link to="/time">⏱️ {{ L.t('nav.time') }}</router-link>
|
||
<router-link v-if="auth.user?.isAdmin" to="/admin">🛡️ {{ L.t('nav.admin') }}</router-link>
|
||
</nav>
|
||
|
||
<input v-model="search" :placeholder="L.t('side.searchPh')" @input="doSearch" />
|
||
<div v-if="results.length" class="card search-results">
|
||
<div v-for="r in results" :key="r.id" class="tree-item" @click="router.push(`/page/${r.id}`)">
|
||
🔍 {{ r.title }}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="side-label">{{ L.t('side.books') }}</div>
|
||
<div class="row">
|
||
<input v-model="newBook" :placeholder="L.t('side.newBookPh')" @keyup.enter="createBook" />
|
||
<button @click="createBook">+</button>
|
||
</div>
|
||
<div v-for="b in ws.books" :key="b.id" class="book">
|
||
<div class="tree-item book-head" @click="toggleBook(b.id)">
|
||
<span>{{ collapsed[b.id] ? '▶' : '▼' }} {{ b.icon }} {{ b.title }}</span>
|
||
<span>
|
||
<button class="ghost mini" @click.stop="createPage(b.id)" title="+">+</button>
|
||
<button class="ghost mini" @click.stop="deleteBook(b.id)" title="🗑️">🗑️</button>
|
||
</span>
|
||
</div>
|
||
<div v-if="!collapsed[b.id]" class="book-pages">
|
||
<PageTree
|
||
:nodes="ws.treeFor(b.id)" :active-id="activePageId"
|
||
@open="router.push(`/page/${$event}`)" @add-child="createPage(b.id)"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="side-label">{{ L.t('side.pages') }}</div>
|
||
<div class="row">
|
||
<input v-model="newPage" :placeholder="L.t('side.newPagePh')" @keyup.enter="createPage()" />
|
||
<button @click="createPage()">+</button>
|
||
</div>
|
||
<PageTree
|
||
:nodes="ws.tree()" :active-id="activePageId"
|
||
@open="router.push(`/page/${$event}`)" @add-child="createPage()"
|
||
/>
|
||
|
||
<div class="side-footer">
|
||
<span class="muted">{{ ws.role }}</span>
|
||
<button class="ghost" @click="logout">🚪 {{ L.t('nav.logout') }}</button>
|
||
</div>
|
||
</aside>
|
||
</template>
|