summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/app.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/app.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/app.js115
1 files changed, 105 insertions, 10 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js
index d36a0a1..4ef334b 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js
@@ -85,7 +85,7 @@ function useAuth() { return useContext(AuthContext); }
// ── Nav ──────────────────────────────────────────────────────────────────────
-function Nav({ user, theme, onThemeToggle, onLogout, onMenuToggle }) {
+function Nav({ user, theme, onThemeToggle, onLogout, onMenuToggle, unreadCount }) {
return html`
<nav class="nav">
<div class="nav-left">
@@ -96,6 +96,11 @@ function Nav({ user, theme, onThemeToggle, onLogout, onMenuToggle }) {
<a class="nav-brand" href="#/">MeshBay</a>
</div>
<div class="nav-right">
+ ${user && html`
+ <a class="nav-notif" href="#/" title=${t('notif.title')}>
+ ${'\u{1F514}'}${unreadCount > 0 && html`<span class="notif-badge">${unreadCount}</span>`}
+ </a>
+ `}
<button class="nav-theme" onClick=${onThemeToggle}
aria-label="${t('nav.toggle_menu')}" title=${theme === 'dark' ? t('nav.light_mode') : t('nav.dark_mode')}>
${theme === 'dark' ? '☀' : '☾'}
@@ -273,11 +278,32 @@ function RegisterPage() {
// ── Home Page ────────────────────────────────────────────────────────────────
-function HomePage({ groups }) {
+function NotificationFeed({ notifications, onMarkRead }) {
+ if (!notifications.length) return null;
+ return html`
+ <div class="notif-feed">
+ <h3>${t('notif.title')}</h3>
+ ${notifications.map(n => html`
+ <div key=${n.id} class="notif-item ${n.read ? '' : 'notif-unread'}"
+ onClick=${() => {
+ if (!n.read) onMarkRead(n.id);
+ if (n.link) navigate(n.link);
+ }}>
+ <span class="notif-kind">${n.kind}</span>
+ <span class="notif-text">${n.title}</span>
+ <span class="notif-time">${new Date(n.created_at).toLocaleDateString()}</span>
+ </div>
+ `)}
+ </div>
+ `;
+}
+
+function HomePage({ groups, notifications, onMarkRead }) {
if (groups.length === 0) {
return html`
<div>
<h2>${t('home.welcome')}</h2>
+ <${NotificationFeed} notifications=${notifications} onMarkRead=${onMarkRead} />
<p class="page-message">
${t('home.no_groups')}
${' '}${t('home.browse_prefix')}<a href="#/explore">${t('home.browse_link')}</a>${t('home.browse_suffix')}
@@ -289,6 +315,7 @@ function HomePage({ groups }) {
return html`
<div>
<h2>${t('home.my_groups')}</h2>
+ <${NotificationFeed} notifications=${notifications} onMarkRead=${onMarkRead} />
<div class="group-grid">
${groups.map(g => html`
<a key=${g.id} class="group-card" href="#/group/${g.id}">
@@ -309,17 +336,32 @@ function HomePage({ groups }) {
function ExplorePage({ token }) {
const [groups, setGroups] = useState([]);
const [loading, setLoading] = useState(true);
+ const [search, setSearch] = useState('');
- useEffect(() => {
- hubFetch('/v1/groups', { token })
+ const doSearch = useCallback((q) => {
+ setLoading(true);
+ const url = q ? `/v1/groups?q=${encodeURIComponent(q)}` : '/v1/groups';
+ hubFetch(url, { token })
.then(data => setGroups(data.groups || []))
.catch(() => {})
.finally(() => setLoading(false));
}, [token]);
+ useEffect(() => { doSearch(''); }, [token]);
+
+ const onSearch = useCallback((e) => {
+ const q = e.target.value;
+ setSearch(q);
+ doSearch(q);
+ }, [doSearch]);
+
return html`
<div>
<h2>${t('explore.title')}</h2>
+ <div class="file-toolbar" style="margin-bottom:16px">
+ <input type="text" class="admin-search" placeholder="${t('explore.search')}"
+ value=${search} onInput=${onSearch} />
+ </div>
${loading
? html`<p class="page-message">${t('explore.loading')}</p>`
: groups.length === 0
@@ -938,8 +980,12 @@ function VideoPlayer({ entry, transportRef, gekRef, onClose }) {
const THEME_OPTIONS = ['light', 'dark', 'system'];
-function SettingsPage({ user, theme, onThemeChange }) {
+function SettingsPage({ user, theme, onThemeChange, groups }) {
const [locale, setLoc] = useState(getLocale);
+ const [muted, setMuted] = useState(() => {
+ try { return JSON.parse(localStorage.getItem('mb_muted') || '{}'); }
+ catch { return {}; }
+ });
const onLocaleChange = useCallback((e) => {
const code = e.target.value;
@@ -952,6 +998,14 @@ function SettingsPage({ user, theme, onThemeChange }) {
onThemeChange(e.target.value);
}, [onThemeChange]);
+ const toggleMute = useCallback((gid) => {
+ setMuted(prev => {
+ const next = { ...prev, [gid]: !prev[gid] };
+ localStorage.setItem('mb_muted', JSON.stringify(next));
+ return next;
+ });
+ }, []);
+
return html`
<div>
<h2>${t('settings.title')}</h2>
@@ -962,6 +1016,10 @@ function SettingsPage({ user, theme, onThemeChange }) {
<span class="settings-label">${t('settings.username')}</span>
<span class="settings-value">${user.username}</span>
</div>
+ <div class="settings-row">
+ <span class="settings-label">${t('settings.role')}</span>
+ <span class="settings-value">${user.role || 'user'}</span>
+ </div>
</div>
<div class="settings-section">
@@ -984,6 +1042,22 @@ function SettingsPage({ user, theme, onThemeChange }) {
</div>
</div>
+ ${groups.length > 0 && html`
+ <div class="settings-section">
+ <h3 class="settings-heading">${t('settings.groups')}</h3>
+ ${groups.map(g => html`
+ <div class="settings-row" key=${g.id}>
+ <span class="settings-label">${g.name}</span>
+ <label class="settings-value" style="cursor:pointer">
+ <input type="checkbox" checked=${!muted[g.id]}
+ onChange=${() => toggleMute(g.id)} />
+ ${' '}${t('settings.notifications')}
+ </label>
+ </div>
+ `)}
+ </div>
+ `}
+
<div class="settings-section">
<h3 class="settings-heading">${t('settings.about')}</h3>
<div class="settings-row">
@@ -1339,6 +1413,8 @@ function App() {
const [user, setUser] = useState(loadAuth);
const [groups, setGroups] = useState([]);
const [menuOpen, setMenuOpen] = useState(false);
+ const [notifications, setNotifications] = useState([]);
+ const [unreadCount, setUnreadCount] = useState(0);
const resolved = resolveTheme(theme);
@@ -1347,13 +1423,31 @@ function App() {
localStorage.setItem(THEME_KEY, theme);
}, [theme, resolved]);
+ const fetchNotifications = useCallback(() => {
+ if (!user) return;
+ hubFetch('/v1/notifications?limit=20', { token: user.token })
+ .then(data => {
+ setNotifications(data.notifications || []);
+ setUnreadCount(data.unread_count || 0);
+ })
+ .catch(() => {});
+ }, [user]);
+
useEffect(() => {
- if (!user) { setGroups([]); return; }
+ if (!user) { setGroups([]); setNotifications([]); setUnreadCount(0); return; }
hubFetch('/v1/groups/mine', { token: user.token })
.then(data => setGroups(data.groups || []))
.catch(() => setGroups([]));
+ fetchNotifications();
}, [user]);
+ const markRead = useCallback((id) => {
+ if (!user) return;
+ hubFetch(`/v1/notifications/${id}/read`, { method: 'POST', token: user.token })
+ .then(() => fetchNotifications())
+ .catch(() => {});
+ }, [user, fetchNotifications]);
+
useEffect(() => { setMenuOpen(false); }, [route]);
const toggleTheme = useCallback(() => {
@@ -1408,12 +1502,12 @@ function App() {
} else if (route === '/admin') {
page = (user.role === 'moderator' || user.role === 'admin')
? html`<${AdminPage} token=${user.token} />`
- : html`<${HomePage} groups=${groups} />`;
+ : html`<${HomePage} groups=${groups} notifications=${notifications} onMarkRead=${markRead} />`;
} else if (route === '/settings') {
page = html`<${SettingsPage} user=${user} theme=${theme}
- onThemeChange=${setTheme} />`;
+ onThemeChange=${setTheme} groups=${groups} />`;
} else {
- page = html`<${HomePage} groups=${groups} />`;
+ page = html`<${HomePage} groups=${groups} notifications=${notifications} onMarkRead=${markRead} />`;
}
return html`
@@ -1423,7 +1517,8 @@ function App() {
theme=${resolved}
onThemeToggle=${toggleTheme}
onLogout=${authCtx.logout}
- onMenuToggle=${() => setMenuOpen(o => !o)} />
+ onMenuToggle=${() => setMenuOpen(o => !o)}
+ unreadCount=${unreadCount} />
<div class="layout">
${user && html`<${Sidebar}
groups=${groups}