feat: Add user statuses and cards

This commit is contained in:
2026-04-16 22:52:45 +02:00
parent b4ac0cdc92
commit 2927a86fbb
57 changed files with 1964 additions and 185 deletions

View File

@@ -0,0 +1,134 @@
import {
Component,
inject,
signal
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideChevronDown } from '@ng-icons/lucide';
import { UserAvatarComponent } from '../user-avatar/user-avatar.component';
import { UserStatusService } from '../../../core/services/user-status.service';
import { User, UserStatus } from '../../../shared-kernel';
@Component({
selector: 'app-profile-card',
standalone: true,
imports: [
CommonModule,
NgIcon,
UserAvatarComponent
],
viewProviders: [provideIcons({ lucideChevronDown })],
template: `
<div
class="w-72 rounded-lg border border-border bg-card shadow-xl"
style="animation: profile-card-in 120ms cubic-bezier(0.2, 0, 0, 1) both"
>
<!-- Banner -->
<div class="h-24 rounded-t-lg bg-gradient-to-r from-primary/30 to-primary/10"></div>
<!-- Avatar (overlapping banner) -->
<div class="relative px-4">
<div class="-mt-9">
<app-user-avatar
[name]="user().displayName"
[avatarUrl]="user().avatarUrl"
size="xl"
[status]="user().status"
[showStatusBadge]="true"
ringClass="ring-4 ring-card"
/>
</div>
</div>
<!-- Info -->
<div class="px-5 pt-3 pb-4">
<p class="text-base font-semibold text-foreground truncate">{{ user().displayName }}</p>
<p class="text-sm text-muted-foreground truncate">{{ user().username }}</p>
@if (editable()) {
<!-- Status picker -->
<div class="relative mt-3">
<button
type="button"
class="flex w-full items-center gap-2 rounded-md border border-border px-2.5 py-1.5 text-xs hover:bg-secondary/60 transition-colors"
(click)="toggleStatusMenu()"
>
<span class="w-2 h-2 rounded-full" [class]="currentStatusColor()"></span>
<span class="flex-1 text-left text-foreground">{{ currentStatusLabel() }}</span>
<ng-icon
name="lucideChevronDown"
class="w-3 h-3 text-muted-foreground"
/>
</button>
@if (showStatusMenu()) {
<div class="absolute left-0 bottom-full mb-1 w-full bg-card border border-border rounded-md shadow-lg py-1 z-10">
@for (opt of statusOptions; track opt.label) {
<button
type="button"
class="w-full px-3 py-1.5 text-left text-xs hover:bg-secondary flex items-center gap-2"
(click)="setStatus(opt.value)"
>
<span class="w-2 h-2 rounded-full" [class]="opt.color"></span>
<span>{{ opt.label }}</span>
</button>
}
</div>
}
</div>
} @else {
<div class="mt-2 flex items-center gap-1.5 text-xs text-muted-foreground">
<span class="w-2 h-2 rounded-full" [class]="currentStatusColor()"></span>
<span>{{ currentStatusLabel() }}</span>
</div>
}
</div>
</div>
`
})
export class ProfileCardComponent {
user = signal<User>({ id: '', oderId: '', username: '', displayName: '', status: 'offline', role: 'member', joinedAt: 0 });
editable = signal(false);
private userStatus = inject(UserStatusService);
showStatusMenu = signal(false);
readonly statusOptions: { value: UserStatus | null; label: string; color: string }[] = [
{ value: null, label: 'Online', color: 'bg-green-500' },
{ value: 'away', label: 'Away', color: 'bg-yellow-500' },
{ value: 'busy', label: 'Do Not Disturb', color: 'bg-red-500' },
{ value: 'offline', label: 'Invisible', color: 'bg-gray-500' }
];
currentStatusColor(): string {
switch (this.user().status) {
case 'online': return 'bg-green-500';
case 'away': return 'bg-yellow-500';
case 'busy': return 'bg-red-500';
case 'offline': return 'bg-gray-500';
case 'disconnected': return 'bg-gray-500';
default: return 'bg-green-500';
}
}
currentStatusLabel(): string {
switch (this.user().status) {
case 'online': return 'Online';
case 'away': return 'Away';
case 'busy': return 'Do Not Disturb';
case 'offline': return 'Invisible';
case 'disconnected': return 'Offline';
default: return 'Online';
}
}
toggleStatusMenu(): void {
this.showStatusMenu.update((v) => !v);
}
setStatus(status: UserStatus | null): void {
this.userStatus.setManualStatus(status);
this.showStatusMenu.set(false);
}
}