feat(frontend): settings window, i18n, admin UI, books, tasks, calendar, timer
- 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
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import api from '../api';
|
||||
import { useWorkspaceStore } from '../stores/workspace';
|
||||
import { useLocaleStore } from '../stores/locale';
|
||||
import Sidebar from '../components/Sidebar.vue';
|
||||
|
||||
const ws = useWorkspaceStore();
|
||||
const L = useLocaleStore();
|
||||
const router = useRouter();
|
||||
|
||||
const cursor = ref(new Date());
|
||||
cursor.value.setDate(1);
|
||||
const tasks = ref([]);
|
||||
|
||||
const year = computed(() => cursor.value.getFullYear());
|
||||
const month = computed(() => cursor.value.getMonth());
|
||||
const monthName = computed(() =>
|
||||
cursor.value.toLocaleDateString(L.locale === 'zh' ? 'zh-CN' : L.locale === 'de' ? 'de-DE' : 'en-US', {
|
||||
month: 'long', year: 'numeric',
|
||||
}),
|
||||
);
|
||||
|
||||
const cells = computed(() => {
|
||||
const first = new Date(year.value, month.value, 1);
|
||||
const startDay = (first.getDay() + 6) % 7; // Monday first
|
||||
const days = new Date(year.value, month.value + 1, 0).getDate();
|
||||
const arr = [];
|
||||
for (let i = 0; i < startDay; i++) arr.push(null);
|
||||
for (let d = 1; d <= days; d++) arr.push(new Date(year.value, month.value, d));
|
||||
return arr;
|
||||
});
|
||||
|
||||
function dayKey(d) {
|
||||
return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
|
||||
}
|
||||
const byDay = computed(() => {
|
||||
const map = {};
|
||||
for (const t of tasks.value) {
|
||||
if (!t.due_at) continue;
|
||||
const d = new Date(t.due_at);
|
||||
const k = dayKey(d);
|
||||
if (!map[k]) map[k] = [];
|
||||
map[k].push(t);
|
||||
}
|
||||
return map;
|
||||
});
|
||||
|
||||
function isToday(d) {
|
||||
const n = new Date();
|
||||
return d && d.toDateString() === n.toDateString();
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!ws.currentId) return;
|
||||
const from = new Date(year.value, month.value, 1).toISOString();
|
||||
const to = new Date(year.value, month.value + 1, 0, 23, 59).toISOString();
|
||||
const { data } = await api.get(`/tasks/by-workspace/${ws.currentId}`, { params: { from, to } });
|
||||
tasks.value = data.tasks;
|
||||
}
|
||||
|
||||
function prev() { cursor.value = new Date(year.value, month.value - 1, 1); }
|
||||
function next() { cursor.value = new Date(year.value, month.value + 1, 1); }
|
||||
function goToday() { const n = new Date(); cursor.value = new Date(n.getFullYear(), n.getMonth(), 1); }
|
||||
|
||||
watch(cursor, load);
|
||||
|
||||
onMounted(async () => {
|
||||
if (!ws.workspaces.length) await ws.fetchWorkspaces();
|
||||
await load();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout">
|
||||
<Sidebar />
|
||||
<main class="main">
|
||||
<div class="topbar">
|
||||
<h2 style="margin: 0">🗓️ {{ L.t('cal.title') }} — {{ monthName }}</h2>
|
||||
<div class="row">
|
||||
<button class="ghost" @click="prev">◀</button>
|
||||
<button class="ghost" @click="goToday">{{ L.t('cal.today') }}</button>
|
||||
<button class="ghost" @click="next">▶</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="calendar">
|
||||
<div class="cal-head" v-for="d in ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']" :key="d">{{ d }}</div>
|
||||
<div v-for="(d, i) in cells" :key="i" class="cal-cell" :class="{ today: isToday(d) }">
|
||||
<template v-if="d">
|
||||
<div class="cal-day">{{ d.getDate() }}</div>
|
||||
<div
|
||||
v-for="t in (byDay[dayKey(d)] || [])" :key="t.id"
|
||||
class="cal-task" :class="{ done: t.status === 'done' }"
|
||||
:title="t.title"
|
||||
@click="router.push('/tasks')"
|
||||
>
|
||||
{{ t.status === 'done' ? '✅' : '📌' }} {{ t.title }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<p class="muted">📌 {{ L.t('task.due') }} → {{ L.t('cal.title') }} ✨</p>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user