Skip to content

Commit 7324fc4

Browse files
authored
feat: dashboard signals, profile integrations, top nav, empty states, delete modal (#52)
* feat(profile): move signals feed from dashboard to profile page Signals (Slack, GitHub, JIRA) are user-scoped not workspace-scoped, so the profile page is their natural home. - profile.ts: inject SignalService, add signals state + filter/markRead/archive methods - profile.html: add signals section below profile cards with source/status filter pills, search bar, signal-board, and styled empty state - profile.css: signals section styles — dark-theme filter pills, divider, empty state card - dashboard.ts: remove SignalService, SignalBoard, SearchBar imports and all signal state/methods - dashboard.html: remove signals section; update hero copy to link users to Profile for signals - dashboard.css: add .hero-link style for the inline Profile link in hero copy Ref Sentinent-AI/Sentinent#36 * feat(profile): move integrations (Slack/GitHub/Jira) to profile page Integrations are account-level connections, not workspace content, so the profile page is their natural home. - WorkspaceIntegrationsComponent: add @input() inputWorkspaceId + ngOnChanges so the component reloads when the workspace selection changes when embedded outside the router (e.g. profile page) - profile.ts: inject WorkspaceService, load workspaces, expose selectedWorkspaceId + selectWorkspace() for workspace switcher - profile.html: add integrations section below profile cards — embeds <app-workspace-integrations> with workspace selector dropdown (shown only when user has more than one workspace); empty state if no workspaces - profile.css: integrations section styles, workspace selector dropdown - dashboard.ts/html: revert signals-move (signals stay on dashboard); restore original hero copy - workspace-details.html: replace Integrations nav link with a 'Integrations →' link that routes to /profile Ref Sentinent-AI/Sentinent#36 * feat(nav): add persistent top nav bar with unread signal badge Create AppNavComponent — a fixed dark nav bar shared across all auth'd pages (dashboard, profile, workspace detail). - app-nav: Sentinent logo (links /dashboard), Dashboard + Profile links with routerLinkActive highlight, unread signal badge on Profile (polls every 60s), Logout button - dashboard: remove inline brand/profile-btn/logout-btn; add padding-top: 56px to offset fixed nav - profile: remove 'Back to dashboard' button (nav replaces it); add padding-top offset - workspace-details: add <app-nav>; update padding-top; shorten 'Back to Dashboard' label to '← Dashboard' Ref Sentinent-AI/Sentinent#36 * feat(dashboard): show unread signal count badge on workspace cards After signals load, compute a workspaceId→unreadCount map from the already-fetched signal list (no extra API call). Each workspace card now shows a dark pill badge (e.g. '3 unread') when there are unread signals for that workspace, giving users at-a-glance triage from the dashboard. Ref Sentinent-AI/Sentinent#36 * feat(signals): replace filter pills with source tabs + unread toggle Signal filtering is now client-side on an already-fetched full list, so switching tabs is instant with no extra network calls. - Source tabs (All / Slack / GitHub / JIRA) each show a live count badge; active tab gets an underline indicator - 'Unread only' toggle replaces the old status filter pill — one click to focus, one click to clear - allSignals holds the full status-filtered list; signals is the tab-filtered view; applyFilters() recomputes on tab switch without re-fetching Ref Sentinent-AI/Sentinent#36 * feat(ux): contextual empty states for signals and decisions - Signals: two distinct empty states depending on context - 'No signals yet' with icon + 'Connect integrations' CTA when allSignals is empty (no tools connected) - 'All caught up' with check icon when integrations are connected but the active tab/filter returns nothing; message adapts to sourceTab and showUnreadOnly flag - Decision list: replace bare text with structured empty state (icon, heading, sub-copy, 'Create your first decision' CTA button) - CSS: empty-icon, empty-cta, caught-up/no-integrations variants
1 parent 15e05ce commit 7324fc4

15 files changed

Lines changed: 682 additions & 101 deletions

File tree

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
:host {
2+
display: block;
3+
}
4+
5+
.app-nav {
6+
position: fixed;
7+
top: 0;
8+
left: 0;
9+
right: 0;
10+
z-index: 100;
11+
height: 56px;
12+
display: flex;
13+
align-items: center;
14+
justify-content: space-between;
15+
padding: 0 28px;
16+
background: rgba(5, 5, 5, 0.88);
17+
backdrop-filter: blur(14px);
18+
-webkit-backdrop-filter: blur(14px);
19+
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
20+
}
21+
22+
/* ── Brand ───────────────────────────────────────────────── */
23+
24+
.nav-brand {
25+
display: inline-flex;
26+
align-items: center;
27+
gap: 10px;
28+
text-decoration: none;
29+
color: #f7f7f7;
30+
font-size: 17px;
31+
font-weight: 800;
32+
letter-spacing: -0.02em;
33+
}
34+
35+
.nav-brand-icon {
36+
width: 32px;
37+
height: 32px;
38+
border-radius: 8px;
39+
background: #f7f7f7;
40+
color: #050505;
41+
display: grid;
42+
place-items: center;
43+
flex-shrink: 0;
44+
}
45+
46+
/* ── Links ───────────────────────────────────────────────── */
47+
48+
.nav-links {
49+
display: flex;
50+
align-items: center;
51+
gap: 6px;
52+
}
53+
54+
.nav-link {
55+
position: relative;
56+
display: inline-flex;
57+
align-items: center;
58+
gap: 7px;
59+
text-decoration: none;
60+
color: rgba(247, 247, 247, 0.65);
61+
font-size: 14px;
62+
font-weight: 600;
63+
padding: 6px 12px;
64+
border-radius: 8px;
65+
transition: color 0.15s ease, background 0.15s ease;
66+
}
67+
68+
.nav-link:hover {
69+
color: #f7f7f7;
70+
background: rgba(255, 255, 255, 0.07);
71+
}
72+
73+
.nav-link.active {
74+
color: #f7f7f7;
75+
background: rgba(255, 255, 255, 0.1);
76+
}
77+
78+
/* ── Unread badge ────────────────────────────────────────── */
79+
80+
.nav-badge {
81+
display: inline-flex;
82+
align-items: center;
83+
justify-content: center;
84+
min-width: 18px;
85+
height: 18px;
86+
padding: 0 5px;
87+
border-radius: 999px;
88+
background: #f7f7f7;
89+
color: #050505;
90+
font-size: 11px;
91+
font-weight: 800;
92+
line-height: 1;
93+
}
94+
95+
/* ── Logout ──────────────────────────────────────────────── */
96+
97+
.nav-logout {
98+
border: 1px solid rgba(255, 255, 255, 0.18);
99+
background: transparent;
100+
color: rgba(247, 247, 247, 0.65);
101+
border-radius: 8px;
102+
padding: 6px 12px;
103+
font-size: 14px;
104+
font-weight: 600;
105+
cursor: pointer;
106+
transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
107+
margin-left: 6px;
108+
}
109+
110+
.nav-logout:hover {
111+
color: #f7f7f7;
112+
background: rgba(255, 255, 255, 0.07);
113+
border-color: rgba(255, 255, 255, 0.3);
114+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<nav class="app-nav">
2+
<a routerLink="/dashboard" class="nav-brand" aria-label="Sentinent home">
3+
<div class="nav-brand-icon" aria-hidden="true">
4+
<svg width="18" height="18" viewBox="0 0 32 32" fill="currentColor">
5+
<rect x="4" y="6" width="20" height="8" rx="1.5"></rect>
6+
<rect x="8" y="18" width="20" height="8" rx="1.5"></rect>
7+
</svg>
8+
</div>
9+
<span>Sentinent</span>
10+
</a>
11+
12+
<div class="nav-links">
13+
<a routerLink="/dashboard" routerLinkActive="active" class="nav-link">Dashboard</a>
14+
15+
<a routerLink="/profile" routerLinkActive="active" class="nav-link nav-profile-link">
16+
Profile
17+
<span class="nav-badge" *ngIf="unreadCount > 0" aria-label="{{ unreadCount }} unread signals">
18+
{{ badgeLabel }}
19+
</span>
20+
</a>
21+
22+
<button type="button" class="nav-logout" (click)="logout()">Logout</button>
23+
</div>
24+
</nav>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { CommonModule } from '@angular/common';
2+
import { Component, OnInit, OnDestroy, inject } from '@angular/core';
3+
import { Router, RouterLink, RouterLinkActive } from '@angular/router';
4+
import { AuthService } from '../../services/auth';
5+
import { SignalService } from '../../services/signal.service';
6+
7+
@Component({
8+
selector: 'app-nav',
9+
standalone: true,
10+
imports: [CommonModule, RouterLink, RouterLinkActive],
11+
templateUrl: './app-nav.html',
12+
styleUrl: './app-nav.css',
13+
})
14+
export class AppNavComponent implements OnInit, OnDestroy {
15+
private readonly authService = inject(AuthService);
16+
private readonly signalService = inject(SignalService);
17+
private readonly router = inject(Router);
18+
19+
unreadCount = 0;
20+
private pollInterval?: ReturnType<typeof setInterval>;
21+
22+
ngOnInit(): void {
23+
this.refreshUnread();
24+
// Poll every 60s so the badge stays fresh without hammering the API
25+
this.pollInterval = setInterval(() => this.refreshUnread(), 60_000);
26+
}
27+
28+
ngOnDestroy(): void {
29+
if (this.pollInterval !== undefined) {
30+
clearInterval(this.pollInterval);
31+
}
32+
}
33+
34+
logout(): void {
35+
this.authService.logout();
36+
this.router.navigate(['/login']);
37+
}
38+
39+
get badgeLabel(): string {
40+
return this.unreadCount > 99 ? '99+' : String(this.unreadCount);
41+
}
42+
43+
private refreshUnread(): void {
44+
this.signalService.getSignals({ source: 'all', status: 'unread' }).subscribe({
45+
next: (signals) => { this.unreadCount = signals.length; },
46+
error: () => {}
47+
});
48+
}
49+
}

0 commit comments

Comments
 (0)