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:
@@ -1,21 +1,33 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import api from '../api';
|
||||
import { useLocaleStore } from '../stores/locale';
|
||||
import UserSearch from './UserSearch.vue';
|
||||
|
||||
const props = defineProps({ workspaceId: String });
|
||||
const emit = defineEmits(['invited']);
|
||||
const L = useLocaleStore();
|
||||
const email = ref('');
|
||||
const picked = ref(null);
|
||||
const role = ref('member');
|
||||
const msg = ref('');
|
||||
const err = ref('');
|
||||
|
||||
function onPick(u) {
|
||||
picked.value = u;
|
||||
email.value = u.email;
|
||||
}
|
||||
|
||||
async function invite() {
|
||||
msg.value = '';
|
||||
err.value = '';
|
||||
const target = email.value.trim();
|
||||
if (!target) return;
|
||||
try {
|
||||
await api.post(`/workspaces/${props.workspaceId}/members`, { email: email.value, role: role.value });
|
||||
msg.value = `Invited ${email.value} as ${role.value}`;
|
||||
await api.post(`/workspaces/${props.workspaceId}/members`, { email: target, role: role.value });
|
||||
msg.value = `🎉 ${target} → ${role.value}`;
|
||||
email.value = '';
|
||||
picked.value = null;
|
||||
emit('invited');
|
||||
} catch (e) {
|
||||
err.value = e.response?.data?.error || 'Invite failed';
|
||||
@@ -25,17 +37,19 @@ async function invite() {
|
||||
|
||||
<template>
|
||||
<div class="card">
|
||||
<h4>Invite collaborator</h4>
|
||||
<p class="muted">They must register first with the same email.</p>
|
||||
<div class="row">
|
||||
<h4>{{ L.t('inv.title') }}</h4>
|
||||
<p class="muted">{{ L.t('inv.hint') }}</p>
|
||||
<UserSearch @pick="onPick" />
|
||||
<div class="row" style="margin-top: 8px">
|
||||
<input v-model="email" placeholder="teammate@example.com" />
|
||||
<select v-model="role" style="max-width: 130px">
|
||||
<option value="viewer">viewer</option>
|
||||
<option value="member">member</option>
|
||||
<option value="admin">admin</option>
|
||||
</select>
|
||||
<button @click="invite">Invite</button>
|
||||
<button @click="invite">✉️ {{ L.t('inv.btn') }}</button>
|
||||
</div>
|
||||
<p v-if="picked" class="muted">🔎 {{ picked.name }} <{{ picked.email }}></p>
|
||||
<p v-if="msg" style="color: green">{{ msg }}</p>
|
||||
<p v-if="err" style="color: red">{{ err }}</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user