feat(frontend): Vue 3 workspace UI with light/dark mode

- Vue 3 + Vite + Pinia + Vue Router, Axios client with JWT interceptor
- Auth views (login/register), dashboard with workspace switcher and search
- Nested PageTree sidebar, TipTap rich-text editor with debounced save
- Live collaboration via Socket.IO (remote updates, presence avatars, typing)
- Comments thread per page, invite-members card, member list
- Theme store (light/dark/system) persisted, CSS variables, ThemeToggle
- Multi-stage Dockerfile (build + nginx) for hosting
This commit is contained in:
x3
2026-09-22 13:33:22 +02:00
parent 4ba8f8bd21
commit 5252e3ab4f
22 changed files with 3274 additions and 0 deletions
+42
View File
@@ -0,0 +1,42 @@
<script setup>
import { ref } from 'vue';
import api from '../api';
const props = defineProps({ workspaceId: String });
const emit = defineEmits(['invited']);
const email = ref('');
const role = ref('member');
const msg = ref('');
const err = ref('');
async function invite() {
msg.value = '';
err.value = '';
try {
await api.post(`/workspaces/${props.workspaceId}/members`, { email: email.value, role: role.value });
msg.value = `Invited ${email.value} as ${role.value}`;
email.value = '';
emit('invited');
} catch (e) {
err.value = e.response?.data?.error || 'Invite failed';
}
}
</script>
<template>
<div class="card">
<h4>Invite collaborator</h4>
<p class="muted">They must register first with the same email.</p>
<div class="row">
<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>
</div>
<p v-if="msg" style="color: green">{{ msg }}</p>
<p v-if="err" style="color: red">{{ err }}</p>
</div>
</template>