@@ -189,7 +190,7 @@ onMounted(refresh);
diff --git a/frontend/src/components/TimeTrackerButton.vue b/frontend/src/components/TimeTrackerButton.vue
index b6e6ea3..6f4cc76 100644
--- a/frontend/src/components/TimeTrackerButton.vue
+++ b/frontend/src/components/TimeTrackerButton.vue
@@ -4,6 +4,7 @@ import { useRouter } from 'vue-router';
import { useTimeStore, formatDuration } from '../stores/time';
import { useWorkspaceStore } from '../stores/workspace';
import { useLocaleStore } from '../stores/locale';
+import Icon from './Icon.vue';
const time = useTimeStore();
const ws = useWorkspaceStore();
@@ -49,7 +50,7 @@ async function start() {
@click="toggle"
:title="L.t('time.title')"
>
- ⏱️ {{ time.open ? elapsed : L.t('time.start') }}
+
diff --git a/frontend/src/i18n/de.js b/frontend/src/i18n/de.js
index 6c638f9..e52ed6c 100644
--- a/frontend/src/i18n/de.js
+++ b/frontend/src/i18n/de.js
@@ -11,7 +11,7 @@ export default {
auth: {
title: 'Willkommen zurück', sub: 'Melde dich an.',
email: 'E-Mail', password: 'Passwort', btn: 'Anmelden',
- locked: '🔒 Accounts werden vom Admin erstellt.',
+ locked: 'Accounts werden vom Admin erstellt.',
},
bell: {
title: 'Mitteilungen', invites: 'Einladungen', notifs: 'Neuigkeiten',
@@ -38,17 +38,17 @@ export default {
evening: 'Guten Abend', night: 'Gute Nacht',
},
set: {
- title: 'Einstellungen', profile: '👤 Profil', appearance: '🎨 Darstellung',
- language: '🌍 Sprache', background: '🖼️ Hintergrund',
+ title: 'Einstellungen', profile: 'Profil', appearance: 'Darstellung',
+ language: 'Sprache', background: 'Hintergrund',
name: 'Anzeigename', email: 'E-Mail', avatar: 'Profilbild (Emoji oder Bild-URL)',
avatarHint: 'Z. B. 🦊 oder https://…/foto.png',
curPw: 'Aktuelles Passwort', newPw: 'Neues Passwort (min. 8 Zeichen)',
changePw: 'Passwort ändern', pwChanged: 'Passwort geändert ✅',
save: 'Speichern', saved: 'Gespeichert ✅', theme: 'Farbschema',
- light: '☀️ Hell', dark: '🌙 Dunkel', system: '🖥️ System',
+ light: 'Hell', dark: 'Dunkel', system: 'System',
bgUrl: 'Hintergrundbild-URL', bgUrlHint: 'Leer lassen = kein Bild. Oder Preset wählen:',
bgBlur: 'Hintergrund weichzeichnen (Blur)', bgGlass: 'Widgets als Glas darstellen',
- upload: 'Bild hochladen …', close: 'Schließen',
+ upload: 'Bild hochladen …', autoSaved: 'Wird sofort übernommen ✓', close: 'Schließen',
presetNone: 'Kein Bild', presetCustom: 'Eigene URL',
},
task: {
@@ -81,9 +81,22 @@ export default {
back: 'Zurück', loading: 'Lädt …',
},
inv: {
- title: '✉️ Mitarbeiter einladen', hint: 'Tippen zum Suchen – oder volle E-Mail eingeben.',
+ title: 'Mitarbeiter einladen', hint: 'Tippen zum Suchen – oder volle E-Mail eingeben.',
ph: 'Name oder E-Mail suchen …', btn: 'Einladen', done: 'Eingeladen',
pick: 'Person wählen …',
},
+ chat: {
+ title: 'Team-Chat', ph: 'Nachricht schreiben …', send: 'Senden',
+ empty: 'Noch keine Nachrichten. Sag Hallo! 👋',
+ tabs: 'Chats', teamTabs: 'Team-Chats', search: 'Suchen …',
+ type: 'Nachricht …', edit: 'Bearbeiten', edited: '(bearbeitet)',
+ attach: 'Bild anhängen', typing: 'schreibt …', online: 'online',
+ noConv: 'Wähle links einen Chat oder starte einen neuen.',
+ pickUser: 'User suchen für neuen Chat …', unread: 'neu',
+ },
+ perm: {
+ title: 'Rechte pro Benutzer', allow: 'Erlauben', deny: 'Verbieten',
+ inherit: 'Vererbt', manage: 'Rechte',
+ },
com: { title: 'Kommentare', ph: 'Kommentar schreiben …', post: 'Posten' },
};
diff --git a/frontend/src/i18n/en.js b/frontend/src/i18n/en.js
index 91f3900..c8fe9dd 100644
--- a/frontend/src/i18n/en.js
+++ b/frontend/src/i18n/en.js
@@ -11,7 +11,7 @@ export default {
auth: {
title: 'Welcome back', sub: 'Log in to your workspace.',
email: 'Email', password: 'Password', btn: 'Log in',
- locked: '🔒 Accounts are created by an admin.',
+ locked: 'Accounts are created by an admin.',
},
bell: {
title: 'Notifications', invites: 'Invites', notifs: 'Updates',
@@ -38,17 +38,17 @@ export default {
evening: 'Good evening', night: 'Good night',
},
set: {
- title: 'Settings', profile: '👤 Profile', appearance: '🎨 Appearance',
- language: '🌍 Language', background: '🖼️ Background',
+ title: 'Settings', profile: 'Profile', appearance: 'Appearance',
+ language: 'Language', background: 'Background',
name: 'Display name', email: 'Email', avatar: 'Avatar (emoji or image URL)',
avatarHint: 'E.g. 🦊 or https://…/photo.png',
curPw: 'Current password', newPw: 'New password (min. 8 chars)',
changePw: 'Change password', pwChanged: 'Password changed ✅',
save: 'Save', saved: 'Saved ✅', theme: 'Color scheme',
- light: '☀️ Light', dark: '🌙 Dark', system: '🖥️ System',
+ light: 'Light', dark: 'Dark', system: 'System',
bgUrl: 'Background image URL', bgUrlHint: 'Empty = no image. Or pick a preset:',
bgBlur: 'Blur background', bgGlass: 'Glassmorphism widgets',
- upload: 'Upload image …', close: 'Close',
+ upload: 'Upload image …', autoSaved: 'Applied instantly ✓', close: 'Close',
presetNone: 'No image', presetCustom: 'Custom URL',
},
task: {
@@ -81,9 +81,22 @@ export default {
back: 'Back', loading: 'Loading …',
},
inv: {
- title: '✉️ Invite collaborator', hint: 'Type to search – or enter a full email.',
+ title: 'Invite collaborator', hint: 'Type to search – or enter a full email.',
ph: 'Search name or email …', btn: 'Invite', done: 'Invited',
pick: 'Pick a person …',
},
+ chat: {
+ title: 'Team chat', ph: 'Write a message …', send: 'Send',
+ empty: 'No messages yet. Say hi! 👋',
+ tabs: 'Chats', teamTabs: 'Team chats', search: 'Search …',
+ type: 'Message …', edit: 'Edit', edited: '(edited)',
+ attach: 'Attach image', typing: 'is typing …', online: 'online',
+ noConv: 'Pick a chat on the left or start a new one.',
+ pickUser: 'Search user for new chat …', unread: 'new',
+ },
+ perm: {
+ title: 'Per-user permissions', allow: 'Allow', deny: 'Deny',
+ inherit: 'Inherited', manage: 'Rights',
+ },
com: { title: 'Comments', ph: 'Write a comment …', post: 'Post' },
};
diff --git a/frontend/src/i18n/zh.js b/frontend/src/i18n/zh.js
index e0f7960..9492bf6 100644
--- a/frontend/src/i18n/zh.js
+++ b/frontend/src/i18n/zh.js
@@ -11,7 +11,7 @@ export default {
auth: {
title: '欢迎回来', sub: '登录到你的工作区。',
email: '电子邮件', password: '密码', btn: '登录',
- locked: '🔒 账号由管理员创建。',
+ locked: '账号由管理员创建。',
},
bell: {
title: '通知', invites: '邀请', notifs: '动态',
@@ -38,17 +38,17 @@ export default {
evening: '晚上好', night: '晚安',
},
set: {
- title: '设置', profile: '👤 个人资料', appearance: '🎨 外观',
- language: '🌍 语言', background: '🖼️ 背景',
+ title: '设置', profile: '个人资料', appearance: '外观',
+ language: '语言', background: '背景',
name: '显示名称', email: '电子邮件', avatar: '头像(表情或图片链接)',
avatarHint: '例如 🦊 或 https://…/photo.png',
curPw: '当前密码', newPw: '新密码(至少 8 位)',
changePw: '修改密码', pwChanged: '密码已修改 ✅',
save: '保存', saved: '已保存 ✅', theme: '配色方案',
- light: '☀️ 浅色', dark: '🌙 深色', system: '🖥️ 跟随系统',
+ light: '浅色', dark: '深色', system: '跟随系统',
bgUrl: '背景图片链接', bgUrlHint: '留空 = 无背景,或选择预设:',
bgBlur: '模糊背景', bgGlass: '玻璃拟态组件',
- upload: '上传图片 …', close: '关闭',
+ upload: '上传图片 …', autoSaved: '即时生效 ✓', close: '关闭',
presetNone: '无背景', presetCustom: '自定义链接',
},
task: {
@@ -81,9 +81,22 @@ export default {
back: '返回', loading: '加载中 …',
},
inv: {
- title: '✉️ 邀请协作者', hint: '输入搜索,或填写完整邮箱。',
+ title: '邀请协作者', hint: '输入搜索,或填写完整邮箱。',
ph: '搜索姓名或邮箱 …', btn: '邀请', done: '已邀请',
pick: '选择人员 …',
},
+ chat: {
+ title: '团队聊天', ph: '写消息 …', send: '发送',
+ empty: '还没有消息,打个招呼吧!👋',
+ tabs: '私聊', teamTabs: '团队聊天', search: '搜索 …',
+ type: '消息 …', edit: '编辑', edited: '(已编辑)',
+ attach: '添加图片', typing: '正在输入 …', online: '在线',
+ noConv: '在左侧选择聊天或开始新聊天。',
+ pickUser: '搜索用户开始新聊天 …', unread: '新',
+ },
+ perm: {
+ title: '用户权限', allow: '允许', deny: '拒绝',
+ inherit: '继承', manage: '权限',
+ },
com: { title: '评论', ph: '写评论 …', post: '发送' },
};
diff --git a/frontend/src/socket.js b/frontend/src/socket.js
index 821f19f..149351e 100644
--- a/frontend/src/socket.js
+++ b/frontend/src/socket.js
@@ -1,4 +1,5 @@
import { io } from 'socket.io-client';
+import { usePresenceStore } from './stores/presence';
let socket = null;
@@ -9,6 +10,11 @@ export function connectSocket() {
socket?.disconnect();
const url = import.meta.env.VITE_SOCKET_URL || 'http://localhost:3000';
socket = io(url || undefined, { auth: { token } });
+ socket.on('presence:online', (ids) => {
+ try {
+ usePresenceStore().online = ids || [];
+ } catch { /* store not ready */ }
+ });
return socket;
}
diff --git a/frontend/src/stores/presence.js b/frontend/src/stores/presence.js
new file mode 100644
index 0000000..e64bfff
--- /dev/null
+++ b/frontend/src/stores/presence.js
@@ -0,0 +1,12 @@
+import { defineStore } from 'pinia';
+
+export const usePresenceStore = defineStore('presence', {
+ state: () => ({
+ online: [],
+ }),
+ actions: {
+ isOnline(id) {
+ return (this.online || []).includes(id);
+ },
+ },
+});
diff --git a/frontend/src/views/Admin.vue b/frontend/src/views/Admin.vue
index 992e080..6cbc11e 100644
--- a/frontend/src/views/Admin.vue
+++ b/frontend/src/views/Admin.vue
@@ -5,6 +5,7 @@ import { useWorkspaceStore } from '../stores/workspace';
import { useLocaleStore } from '../stores/locale';
import Sidebar from '../components/Sidebar.vue';
import Avatar from '../components/Avatar.vue';
+import Icon from '../components/Icon.vue';
const L = useLocaleStore();
const ws = useWorkspaceStore();
@@ -73,7 +74,7 @@ async function createTeam() {
newTeam.value = '';
}
async function removeTeam(t) {
- if (!confirm('🗑️?')) return;
+ if (!confirm('✕?')) return;
await api.delete(`/teams/${t.id}`);
teams.value = teams.value.filter((x) => x.id !== t.id);
}
@@ -122,7 +123,7 @@ async function togglePerm(g, perm) {
g.permissions = data.group.permissions;
}
async function removeGroup(g) {
- if (!confirm('🗑️?')) return;
+ if (!confirm('✕?')) return;
await api.delete(`/groups/${g.id}`);
groups.value = groups.value.filter((x) => x.id !== g.id);
}
@@ -153,12 +154,12 @@ onMounted(async () => {
- 🛡️ {{ L.t('admin.title') }}
+ {{ L.t('admin.title') }}
-
-
+
+
@@ -204,7 +205,7 @@ onMounted(async () => {
-
+
|
@@ -224,7 +225,7 @@ onMounted(async () => {
{{ t.icon }} {{ t.name }} ({{ t.members }})
-
+
@@ -263,8 +264,8 @@ onMounted(async () => {
- 🔑 {{ g.name }} ({{ g.members }})
-
+ {{ g.name }} ({{ g.members }})
+
{{ L.t('groups.perms') }}:
diff --git a/frontend/src/views/Calendar.vue b/frontend/src/views/Calendar.vue
index 6e7c54c..be243d5 100644
--- a/frontend/src/views/Calendar.vue
+++ b/frontend/src/views/Calendar.vue
@@ -5,6 +5,7 @@ import api from '../api';
import { useWorkspaceStore } from '../stores/workspace';
import { useLocaleStore } from '../stores/locale';
import Sidebar from '../components/Sidebar.vue';
+import Icon from '../components/Icon.vue';
const ws = useWorkspaceStore();
const L = useLocaleStore();
@@ -77,7 +78,7 @@ onMounted(async () => {
-
🗓️ {{ L.t('cal.title') }} — {{ monthName }}
+
{{ L.t('cal.title') }} — {{ monthName }}
@@ -95,12 +96,12 @@ onMounted(async () => {
:title="t.title"
@click="router.push('/tasks')"
>
- {{ t.status === 'done' ? '✅' : '📌' }} {{ t.title }}
+ {{ t.status === 'done' ? '✅' : '▸' }} {{ t.title }}
-
📌 {{ L.t('task.due') }} → {{ L.t('cal.title') }} ✨
+
▸ {{ L.t('task.due') }} → {{ L.t('cal.title') }} ✨
diff --git a/frontend/src/views/Chat.vue b/frontend/src/views/Chat.vue
index 42cd049..53cca58 100644
--- a/frontend/src/views/Chat.vue
+++ b/frontend/src/views/Chat.vue
@@ -1,19 +1,45 @@
-
- 💬 {{ L.t('chat.title') }}
-
-
-
-
-
diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue
index 68716d3..e3ec051 100644
--- a/frontend/src/views/Dashboard.vue
+++ b/frontend/src/views/Dashboard.vue
@@ -1,5 +1,5 @@
@@ -40,12 +75,44 @@ onMounted(async () => {
🏠 {{ currentName }} TEAM_{{ currentTeam }} · {{ L.t('side.role') }}: {{ ws.role }}
-
👥 {{ L.t('side.members') }} ({{ members.length }})
+
+
{{ L.t('side.members') }} ({{ members.length }})
+
+
{{ m.name }} <{{ m.email }}>
{{ m.role }}
+
diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue
index 9c67587..a6c0f43 100644
--- a/frontend/src/views/Login.vue
+++ b/frontend/src/views/Login.vue
@@ -3,6 +3,7 @@ import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useAuthStore } from '../stores/auth';
import { useLocaleStore } from '../stores/locale';
+import Icon from '../components/Icon.vue';
const L = useLocaleStore();
@@ -33,6 +34,6 @@ async function submit() {
{{ err }}
-
{{ L.t('auth.locked') }}
+
{{ L.t('auth.locked') }}
diff --git a/frontend/src/views/PageEditor.vue b/frontend/src/views/PageEditor.vue
index 5a1a760..333032f 100644
--- a/frontend/src/views/PageEditor.vue
+++ b/frontend/src/views/PageEditor.vue
@@ -113,7 +113,7 @@ async function addComment() {
}
async function removePage() {
- if (!confirm('🗑️?')) return;
+ if (!confirm('✕?')) return;
await api.delete(`/pages/${page.value.id}`);
await ws.fetchPages();
router.push('/');
@@ -144,10 +144,10 @@ onUnmounted(() => {
-
+
-
🟢 ({{ presence.length }}):
+
● ({{ presence.length }}):
{{ typingUser }}
@@ -160,11 +160,11 @@ onUnmounted(() => {
-
+
-
💬 {{ L.t('com.title') }} ({{ comments.length }})
+
{{ L.t('com.title') }} ({{ comments.length }})
diff --git a/frontend/src/views/Time.vue b/frontend/src/views/Time.vue
index 622896e..e258794 100644
--- a/frontend/src/views/Time.vue
+++ b/frontend/src/views/Time.vue
@@ -5,6 +5,7 @@ import { useWorkspaceStore } from '../stores/workspace';
import { useLocaleStore } from '../stores/locale';
import Sidebar from '../components/Sidebar.vue';
import Avatar from '../components/Avatar.vue';
+import Icon from '../components/Icon.vue';
const time = useTimeStore();
const ws = useWorkspaceStore();
@@ -19,7 +20,7 @@ function fmt(s) {
}
async function remove(id) {
- if (!confirm('🗑️?')) return;
+ if (!confirm('✕?')) return;
await time.remove(id);
}
@@ -33,7 +34,7 @@ onMounted(async () => {
- ⏱️ {{ L.t('time.title') }}
+ {{ L.t('time.title') }}
🔴 {{ L.t('time.running') }}
{{ time.open.description || '…' }}
📚
@@ -59,7 +60,7 @@ onMounted(async () => {
{{ e.duration_seconds != null ? formatDuration(e.duration_seconds) : '…' }} |
{{ e.book_title ? `📚 ${e.book_title}` : L.t('time.none') }}{{ e.description ? ` — ${e.description}` : '' }} |
|
-
|
+
|
✅ {{ L.t('task.title') }}
+