Rework design part 1

This commit is contained in:
2026-04-01 19:31:00 +02:00
parent fed270d28d
commit 65b9419869
15 changed files with 324 additions and 222 deletions

View File

@@ -1,8 +1,8 @@
<nav class="h-full w-16 flex flex-col items-center gap-3 py-3 border-r border-border bg-card relative">
<nav class="relative flex h-full w-full flex-col items-center gap-2 border-r border-border bg-secondary/35 px-2 py-3">
<!-- Create button -->
<button
type="button"
class="w-10 h-10 rounded-2xl flex items-center justify-center bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
class="flex h-10 w-10 items-center justify-center rounded-md bg-primary text-primary-foreground transition-colors hover:bg-primary/90"
title="Create Server"
(click)="createServer()"
>
@@ -13,22 +13,21 @@
</button>
<!-- Saved servers icons -->
<div class="flex-1 w-full overflow-y-auto flex flex-col items-center gap-2 mt-2">
<div class="no-scrollbar mt-2 flex w-full flex-1 flex-col items-center gap-2 overflow-y-auto">
@for (room of visibleSavedRooms(); track room.id) {
<div class="relative flex w-full justify-center pl-2">
<div class="relative flex w-full justify-center">
@if (isSelectedRoom(room)) {
<span
aria-hidden="true"
class="absolute left-1 top-1/2 h-7 w-1 -translate-y-1/2 rounded-full bg-primary shadow-[0_0_10px_rgba(59,130,246,0.45)]"
class="pointer-events-none absolute left-0 top-1/2 h-10 w-1 -translate-y-1/2 rounded-l-full bg-primary"
></span>
}
<button
type="button"
class="relative w-10 h-10 flex-shrink-0 rounded-2xl border border-border hover:border-primary/60 hover:shadow-sm transition-all"
[class.border-primary]="isSelectedRoom(room)"
[class.shadow-[0_0_0_1px_rgba(59,130,246,0.45),0_10px_20px_rgba(15,23,42,0.25)]]="isSelectedRoom(room)"
[class.scale-105]="isSelectedRoom(room)"
class="relative z-10 flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-md border border-transparent bg-card transition-colors hover:border-border hover:bg-card"
[class.border-primary/30]="isSelectedRoom(room)"
[class.bg-primary/10]="isSelectedRoom(room)"
[title]="room.name"
[attr.aria-current]="isSelectedRoom(room) ? 'page' : null"
(click)="joinSavedRoom(room)"
@@ -39,17 +38,18 @@
<img
[ngSrc]="room.icon"
[alt]="room.name"
class="w-full h-full object-cover"
class="h-full w-full object-cover"
/>
} @else {
<div
class="w-full h-full flex items-center justify-center bg-secondary transition-colors"
class="flex h-full w-full items-center justify-center bg-secondary transition-colors"
[class.bg-primary/15]="isSelectedRoom(room)"
>
<span
class="text-sm font-semibold text-muted-foreground transition-colors"
[class.text-foreground]="isSelectedRoom(room)"
>{{ initial(room.name) }}</span>
>{{ initial(room.name) }}</span
>
</div>
}
</div>
@@ -59,6 +59,23 @@
{{ formatUnreadCount(roomUnreadCount(room.id)) }}
</span>
}
@if (voicePresenceCount(room.id) > 0) {
<span
class="absolute -bottom-1 -right-1 flex h-4 w-4 items-center justify-center rounded-full bg-emerald-500 text-white shadow-sm ring-2 ring-card"
[title]="voicePresenceCount(room.id) + (voicePresenceCount(room.id) === 1 ? ' user in voice' : ' users in voice')"
>
<svg
viewBox="0 0 16 16"
aria-hidden="true"
class="h-2.5 w-2.5 fill-current"
>
<path
d="M6.25 4.1a.75.75 0 0 1 1.25.57v6.66a.75.75 0 0 1-1.25.57L3.68 9.6H2.25A1.25 1.25 0 0 1 1 8.35v-.7c0-.69.56-1.25 1.25-1.25h1.43L6.25 4.1Zm4.1 1.35a.75.75 0 0 1 1.05.1 4.2 4.2 0 0 1 0 4.9.75.75 0 0 1-1.15-.96 2.7 2.7 0 0 0 0-2.98.75.75 0 0 1 .1-1.06Z"
/>
</svg>
</span>
}
</button>
</div>
}

View File

@@ -25,7 +25,10 @@ import {
import { Room, User } from '../../shared-kernel';
import { VoiceSessionFacade } from '../../domains/voice-session';
import { selectSavedRooms, selectCurrentRoom } from '../../store/rooms/rooms.selectors';
import { selectCurrentUser } from '../../store/users/users.selectors';
import {
selectCurrentUser,
selectOnlineUsers
} from '../../store/users/users.selectors';
import { RoomsActions } from '../../store/rooms/rooms.actions';
import { DatabaseService } from '../../infrastructure/persistence';
import { NotificationsFacade } from '../../domains/notifications';
@@ -74,6 +77,7 @@ export class ServersRailComponent {
contextRoom = signal<Room | null>(null);
showLeaveConfirm = signal(false);
currentUser = this.store.selectSignal(selectCurrentUser);
onlineUsers = this.store.selectSignal(selectOnlineUsers);
bannedRoomLookup = signal<Record<string, boolean>>({});
bannedServerName = signal('');
showBannedDialog = signal(false);
@@ -82,6 +86,46 @@ export class ServersRailComponent {
joinPassword = signal('');
joinPasswordError = signal<string | null>(null);
visibleSavedRooms = computed(() => this.savedRooms().filter((room) => !this.isRoomMarkedBanned(room)));
voicePresenceByRoom = computed(() => {
const presence: Record<string, number> = {};
const seenByRoom = new Map<string, Set<string>>();
const addVoicePresence = (user: User | null | undefined): void => {
if (!user) {
return;
}
const voiceState = user?.voiceState;
const roomId = voiceState?.serverId;
if (!voiceState?.isConnected || !roomId) {
return;
}
const userKey = user.oderId || user.id;
let seenUsers = seenByRoom.get(roomId);
if (!seenUsers) {
seenUsers = new Set<string>();
seenByRoom.set(roomId, seenUsers);
}
if (seenUsers.has(userKey)) {
return;
}
seenUsers.add(userKey);
presence[roomId] = (presence[roomId] ?? 0) + 1;
};
for (const user of this.onlineUsers()) {
addVoicePresence(user);
}
addVoicePresence(this.currentUser());
return presence;
});
constructor() {
effect(() => {
@@ -236,6 +280,10 @@ export class ServersRailComponent {
return this.notifications.roomUnreadCount(roomId);
}
voicePresenceCount(roomId: string): number {
return this.voicePresenceByRoom()[roomId] ?? 0;
}
formatUnreadCount(count: number): string {
return count > 99 ? '99+' : String(count);
}