feat: signal server tag

This commit is contained in:
2026-06-05 06:16:02 +02:00
parent 6865147e8f
commit bf4e6891d1
69 changed files with 2808 additions and 1269 deletions

View File

@@ -1,408 +1,418 @@
<ng-template #pageContent>
<div class="h-full min-h-0 overflow-y-auto bg-background text-foreground">
<div class="mx-auto w-full max-w-5xl space-y-8 p-4 sm:p-6 lg:py-8">
<header class="space-y-1">
<h1 class="text-2xl font-semibold text-foreground">
@if (currentUser()) {
Welcome back, {{ currentUser()!.displayName || 'there' }}
} @else {
Welcome to MetoYou
}
</h1>
<p class="text-sm text-muted-foreground">Find people, discover servers, or start your own community.</p>
</header>
<div>
<div class="relative">
<ng-icon
name="lucideSearch"
class="absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground"
/>
<input
#searchInput
type="text"
aria-label="Search people, servers, and invites"
class="h-12 w-full rounded-xl border border-border bg-secondary py-2 pl-11 pr-20 text-base text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
placeholder="Search for people, servers, or paste an invite..."
[ngModel]="searchQuery()"
(ngModelChange)="onSearchChange($event)"
(keydown.enter)="submitSearch()"
/>
<kbd class="pointer-events-none absolute right-3 top-1/2 hidden -translate-y-1/2 items-center gap-1 rounded border border-border bg-card px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground sm:flex">
Ctrl K
</kbd>
</div>
@if (!isSearchMode() && recentSearches().length > 0) {
<div class="mt-3 flex flex-wrap items-center gap-2">
<span class="text-xs font-medium text-muted-foreground">Recent:</span>
@for (term of recentSearches(); track term) {
<span class="group inline-flex items-center gap-1 rounded-full border border-border bg-secondary py-1 pl-3 pr-1 text-xs text-foreground">
<button
type="button"
class="max-w-[10rem] truncate hover:text-primary"
(click)="applyRecentSearch(term)"
>
{{ term }}
</button>
<button
type="button"
class="grid h-4 w-4 place-items-center rounded-full text-muted-foreground hover:bg-card hover:text-foreground"
[attr.aria-label]="'Remove ' + term"
(click)="removeRecentSearch(term)"
>
<ng-icon
name="lucideX"
class="h-3 w-3"
/>
</button>
</span>
<div class="h-full min-h-0 overflow-y-auto bg-background text-foreground">
<div class="mx-auto w-full max-w-5xl space-y-8 p-4 sm:p-6 lg:py-8">
<header class="space-y-1">
<h1 class="text-2xl font-semibold text-foreground">
@if (currentUser()) {
Welcome back, {{ currentUser()!.displayName || 'there' }}
} @else {
Welcome to MetoYou
}
<button
type="button"
class="text-xs font-medium text-muted-foreground hover:text-foreground hover:underline"
(click)="clearRecentSearches()"
>
Clear
</button>
</div>
}
</div>
</h1>
<p class="text-sm text-muted-foreground">Find people, discover servers, or start your own community.</p>
</header>
@if (isSearchMode()) {
<section class="space-y-5">
@if (inviteResult(); as invite) {
<div>
<h2 class="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">Invite</h2>
<div>
<div class="relative">
<ng-icon
name="lucideSearch"
class="absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground"
/>
<input
#searchInput
type="text"
aria-label="Search people, servers, and invites"
class="h-12 w-full rounded-xl border border-border bg-secondary py-2 pl-11 pr-20 text-base text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
placeholder="Search for people, servers, or paste an invite..."
[ngModel]="searchQuery()"
(ngModelChange)="onSearchChange($event)"
(keydown.enter)="submitSearch()"
/>
<kbd
class="pointer-events-none absolute right-3 top-1/2 hidden -translate-y-1/2 items-center gap-1 rounded border border-border bg-card px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground sm:flex"
>
Ctrl K
</kbd>
</div>
@if (!isSearchMode() && recentSearches().length > 0) {
<div class="mt-3 flex flex-wrap items-center gap-2">
<span class="text-xs font-medium text-muted-foreground">Recent:</span>
@for (term of recentSearches(); track term) {
<span
class="group inline-flex items-center gap-1 rounded-full border border-border bg-secondary py-1 pl-3 pr-1 text-xs text-foreground"
>
<button
type="button"
class="max-w-[10rem] truncate hover:text-primary"
(click)="applyRecentSearch(term)"
>
{{ term }}
</button>
<button
type="button"
class="grid h-4 w-4 place-items-center rounded-full text-muted-foreground hover:bg-card hover:text-foreground"
[attr.aria-label]="'Remove ' + term"
(click)="removeRecentSearch(term)"
>
<ng-icon
name="lucideX"
class="h-3 w-3"
/>
</button>
</span>
}
<button
type="button"
class="flex w-full items-center gap-3 rounded-lg border border-border bg-card p-3 text-left transition-colors hover:border-primary/50 hover:bg-card/80"
(click)="openInvite()"
class="text-xs font-medium text-muted-foreground hover:text-foreground hover:underline"
(click)="clearRecentSearches()"
>
<div class="grid h-10 w-10 shrink-0 place-items-center rounded-lg bg-primary/10 text-primary">
<ng-icon
name="lucideTicket"
class="h-5 w-5"
/>
</div>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-semibold text-foreground">Open invite</p>
<p class="truncate text-xs text-muted-foreground">{{ invite }}</p>
</div>
<ng-icon
name="lucideArrowRight"
class="h-4 w-4 text-muted-foreground"
/>
Clear
</button>
</div>
}
</div>
@if (topServerResults().length > 0) {
<div>
<div class="mb-2 flex items-center justify-between">
<h2 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Servers</h2>
<a
routerLink="/servers"
class="text-xs font-medium text-primary hover:underline"
>View all</a
@if (isSearchMode()) {
<section class="space-y-5">
@if (inviteResult(); as invite) {
<div>
<h2 class="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">Invite</h2>
<button
type="button"
class="flex w-full items-center gap-3 rounded-lg border border-border bg-card p-3 text-left transition-colors hover:border-primary/50 hover:bg-card/80"
(click)="openInvite()"
>
</div>
<div class="space-y-2">
@for (server of topServerResults(); track server.id) {
<button
type="button"
class="flex w-full items-center gap-3 rounded-lg border border-border bg-card p-3 text-left transition-colors hover:border-primary/50 hover:bg-card/80"
(click)="openServer(server)"
>
<div class="grid h-10 w-10 shrink-0 place-items-center overflow-hidden rounded-lg bg-secondary text-sm font-semibold text-foreground">
@if (server.icon) {
<div
aria-hidden="true"
class="h-full w-full bg-cover bg-center bg-no-repeat"
[style.backgroundImage]="'url(' + server.icon + ')'"
></div>
} @else {
{{ serverInitial(server) }}
}
</div>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-semibold text-foreground">{{ server.name }}</p>
<p class="truncate text-xs text-muted-foreground">{{ serverMetaLabel(server) }}</p>
</div>
<div class="grid h-10 w-10 shrink-0 place-items-center rounded-lg bg-primary/10 text-primary">
<ng-icon
name="lucideArrowRight"
class="h-4 w-4 text-muted-foreground"
name="lucideTicket"
class="h-5 w-5"
/>
</button>
}
</div>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-semibold text-foreground">Open invite</p>
<p class="truncate text-xs text-muted-foreground">{{ invite }}</p>
</div>
<ng-icon
name="lucideArrowRight"
class="h-4 w-4 text-muted-foreground"
/>
</button>
</div>
</div>
}
@if (topServerResults().length > 0) {
<div>
<div class="mb-2 flex items-center justify-between">
<h2 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Servers</h2>
<a
routerLink="/servers"
class="text-xs font-medium text-primary hover:underline"
>View all</a
>
</div>
<div class="space-y-2">
@for (server of topServerResults(); track server.id) {
<button
type="button"
class="flex w-full items-center gap-3 rounded-lg border border-border bg-card p-3 text-left transition-colors hover:border-primary/50 hover:bg-card/80"
(click)="openServer(server)"
>
<div
class="grid h-10 w-10 shrink-0 place-items-center overflow-hidden rounded-lg bg-secondary text-sm font-semibold text-foreground"
>
@if (server.icon) {
<div
aria-hidden="true"
class="h-full w-full bg-cover bg-center bg-no-repeat"
[style.backgroundImage]="'url(' + server.icon + ')'"
></div>
} @else {
{{ serverInitial(server) }}
}
</div>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-semibold text-foreground">{{ server.name }}</p>
<p class="truncate text-xs text-muted-foreground">{{ serverMetaLabel(server) }}</p>
</div>
<ng-icon
name="lucideArrowRight"
class="h-4 w-4 text-muted-foreground"
/>
</button>
}
</div>
</div>
}
@if (topPeopleResults().length > 0) {
<div>
<div class="mb-2 flex items-center justify-between">
<h2 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">People</h2>
<a
routerLink="/people"
class="text-xs font-medium text-primary hover:underline"
>View all</a
>
</div>
<div class="space-y-2">
@for (person of topPeopleResults(); track person.id) {
<a
routerLink="/people"
class="flex w-full items-center gap-3 rounded-lg border border-border bg-card p-3 text-left transition-colors hover:border-primary/50 hover:bg-card/80"
>
<app-user-avatar
[name]="personLabel(person)"
[avatarUrl]="person.avatarUrl"
size="md"
[status]="person.status"
[showStatusBadge]="true"
/>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-semibold text-foreground">{{ personLabel(person) }}</p>
<p class="text-xs text-muted-foreground">{{ isOnline(person) ? 'Online' : 'Offline' }}</p>
</div>
<app-friend-button [user]="person" />
</a>
}
</div>
</div>
}
@if (hasNoQuickResults() && !isSearching()) {
<div class="rounded-lg border border-border bg-card px-4 py-8 text-center text-sm text-muted-foreground">
No people, servers, or invites match
<span class="font-medium text-foreground">{{ searchQuery() }}</span
>.
</div>
}
</section>
} @else {
<!-- Primary actions -->
<section class="grid gap-3 sm:grid-cols-3">
<a
routerLink="/people"
class="group flex items-center gap-3 rounded-xl border border-border bg-card p-4 transition-colors hover:border-primary/40 hover:bg-card/80"
>
<div class="grid h-11 w-11 shrink-0 place-items-center rounded-lg bg-purple-500/15 text-purple-400">
<ng-icon
name="lucideUsers"
class="h-6 w-6"
/>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm font-semibold text-foreground">Find People</p>
<p class="mt-0.5 text-xs text-muted-foreground">Connect with friends.</p>
</div>
<ng-icon
name="lucideArrowRight"
class="h-4 w-4 text-muted-foreground transition-transform group-hover:translate-x-0.5"
/>
</a>
<a
routerLink="/servers"
class="group flex items-center gap-3 rounded-xl border border-border bg-card p-4 transition-colors hover:border-primary/40 hover:bg-card/80"
>
<div class="grid h-11 w-11 shrink-0 place-items-center rounded-lg bg-blue-500/15 text-blue-400">
<ng-icon
name="lucideCompass"
class="h-6 w-6"
/>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm font-semibold text-foreground">Find Servers</p>
<p class="mt-0.5 text-xs text-muted-foreground">Browse communities.</p>
</div>
<ng-icon
name="lucideArrowRight"
class="h-4 w-4 text-muted-foreground transition-transform group-hover:translate-x-0.5"
/>
</a>
<a
routerLink="/create-server"
class="group flex items-center gap-3 rounded-xl border border-emerald-500/40 bg-emerald-500/10 p-4 transition-colors hover:bg-emerald-500/15"
>
<div class="grid h-11 w-11 shrink-0 place-items-center rounded-lg bg-emerald-500/20 text-emerald-400">
<ng-icon
name="lucidePlus"
class="h-6 w-6"
/>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm font-semibold text-foreground">Create Server</p>
<p class="mt-0.5 text-xs text-muted-foreground">Start your own.</p>
</div>
<ng-icon
name="lucideArrowRight"
class="h-4 w-4 text-emerald-400 transition-transform group-hover:translate-x-0.5"
/>
</a>
</section>
@if (isNewUser()) {
<section class="rounded-xl border border-border bg-card p-6 text-center">
<div class="mx-auto mb-3 grid h-12 w-12 place-items-center rounded-full bg-secondary">
<ng-icon
name="lucideServer"
class="h-6 w-6 text-muted-foreground"
/>
</div>
<h2 class="text-base font-semibold text-foreground">Get started</h2>
<p class="mx-auto mt-1 max-w-sm text-sm text-muted-foreground">
You have not joined any servers yet. Find a community to join, or create your own to invite friends.
</p>
</section>
}
@if (topPeopleResults().length > 0) {
<div>
<div class="mb-2 flex items-center justify-between">
<h2 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">People</h2>
<!-- People + Popular servers -->
<section class="grid gap-4 lg:grid-cols-2">
<div class="rounded-xl border border-border bg-card/40 p-4">
<div class="mb-3 flex items-center justify-between">
<h2 class="text-sm font-semibold text-foreground">People you might know</h2>
<a
routerLink="/people"
class="text-xs font-medium text-primary hover:underline"
>View all</a
>See all</a
>
</div>
<div class="space-y-2">
@for (person of topPeopleResults(); track person.id) {
<a
routerLink="/people"
class="flex w-full items-center gap-3 rounded-lg border border-border bg-card p-3 text-left transition-colors hover:border-primary/50 hover:bg-card/80"
>
<app-user-avatar
[name]="personLabel(person)"
[avatarUrl]="person.avatarUrl"
size="md"
[status]="person.status"
[showStatusBadge]="true"
/>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-semibold text-foreground">{{ personLabel(person) }}</p>
<p class="text-xs text-muted-foreground">{{ isOnline(person) ? 'Online' : 'Offline' }}</p>
@if (peopleYouMightKnow().length > 0) {
<div class="space-y-1">
@for (person of peopleYouMightKnow(); track person.id) {
<div class="flex items-center gap-3 rounded-lg px-2 py-2 transition-colors hover:bg-secondary/60">
<app-user-avatar
[name]="personLabel(person)"
[avatarUrl]="person.avatarUrl"
size="md"
[status]="person.status"
[showStatusBadge]="true"
/>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-foreground">{{ personLabel(person) }}</p>
<p class="text-xs text-muted-foreground">{{ isOnline(person) ? 'Online' : 'Offline' }}</p>
</div>
<app-friend-button [user]="person" />
</div>
<app-friend-button [user]="person" />
</a>
}
}
</div>
} @else {
<p class="py-6 text-center text-sm text-muted-foreground">No people to suggest yet.</p>
}
</div>
<div class="rounded-xl border border-border bg-card/40 p-4">
<div class="mb-3 flex items-center justify-between">
<h2 class="text-sm font-semibold text-foreground">Popular Servers</h2>
<a
routerLink="/servers"
class="text-xs font-medium text-primary hover:underline"
>See all</a
>
</div>
@if (popularServers().length > 0) {
<div class="space-y-1">
@for (server of popularServers(); track server.id) {
<div class="flex items-center gap-3 rounded-lg px-2 py-2 transition-colors hover:bg-secondary/60">
<div
class="grid h-10 w-10 shrink-0 place-items-center overflow-hidden rounded-lg bg-secondary text-sm font-semibold text-foreground"
>
@if (server.icon) {
<div
aria-hidden="true"
class="h-full w-full bg-cover bg-center bg-no-repeat"
[style.backgroundImage]="'url(' + server.icon + ')'"
></div>
} @else {
{{ serverInitial(server) }}
}
</div>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-foreground">{{ server.name }}</p>
<p class="truncate text-xs text-muted-foreground">{{ serverMetaLabel(server) }}</p>
</div>
<button
type="button"
class="shrink-0 rounded-md bg-primary px-3 py-1.5 text-xs font-semibold text-primary-foreground transition-colors hover:bg-primary/90"
(click)="openServer(server)"
>
Join
</button>
</div>
}
</div>
} @else {
<p class="py-6 text-center text-sm text-muted-foreground">No popular servers right now.</p>
}
</div>
}
@if (hasNoQuickResults() && !isSearching()) {
<div class="rounded-lg border border-border bg-card px-4 py-8 text-center text-sm text-muted-foreground">
No people, servers, or invites match
<span class="font-medium text-foreground">{{ searchQuery() }}</span
>.
</div>
}
</section>
} @else {
<!-- Primary actions -->
<section class="grid gap-3 sm:grid-cols-3">
<a
routerLink="/people"
class="group flex items-center gap-3 rounded-xl border border-border bg-card p-4 transition-colors hover:border-primary/40 hover:bg-card/80"
>
<div class="grid h-11 w-11 shrink-0 place-items-center rounded-lg bg-purple-500/15 text-purple-400">
<ng-icon
name="lucideUsers"
class="h-6 w-6"
/>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm font-semibold text-foreground">Find People</p>
<p class="mt-0.5 text-xs text-muted-foreground">Connect with friends.</p>
</div>
<ng-icon
name="lucideArrowRight"
class="h-4 w-4 text-muted-foreground transition-transform group-hover:translate-x-0.5"
/>
</a>
<a
routerLink="/servers"
class="group flex items-center gap-3 rounded-xl border border-border bg-card p-4 transition-colors hover:border-primary/40 hover:bg-card/80"
>
<div class="grid h-11 w-11 shrink-0 place-items-center rounded-lg bg-blue-500/15 text-blue-400">
<ng-icon
name="lucideCompass"
class="h-6 w-6"
/>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm font-semibold text-foreground">Find Servers</p>
<p class="mt-0.5 text-xs text-muted-foreground">Browse communities.</p>
</div>
<ng-icon
name="lucideArrowRight"
class="h-4 w-4 text-muted-foreground transition-transform group-hover:translate-x-0.5"
/>
</a>
<a
routerLink="/create-server"
class="group flex items-center gap-3 rounded-xl border border-emerald-500/40 bg-emerald-500/10 p-4 transition-colors hover:bg-emerald-500/15"
>
<div class="grid h-11 w-11 shrink-0 place-items-center rounded-lg bg-emerald-500/20 text-emerald-400">
<ng-icon
name="lucidePlus"
class="h-6 w-6"
/>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm font-semibold text-foreground">Create Server</p>
<p class="mt-0.5 text-xs text-muted-foreground">Start your own.</p>
</div>
<ng-icon
name="lucideArrowRight"
class="h-4 w-4 text-emerald-400 transition-transform group-hover:translate-x-0.5"
/>
</a>
</section>
@if (isNewUser()) {
<section class="rounded-xl border border-border bg-card p-6 text-center">
<div class="mx-auto mb-3 grid h-12 w-12 place-items-center rounded-full bg-secondary">
<ng-icon
name="lucideServer"
class="h-6 w-6 text-muted-foreground"
/>
</div>
<h2 class="text-base font-semibold text-foreground">Get started</h2>
<p class="mx-auto mt-1 max-w-sm text-sm text-muted-foreground">
You have not joined any servers yet. Find a community to join, or create your own to invite friends.
</p>
</section>
}
<!-- People + Popular servers -->
<section class="grid gap-4 lg:grid-cols-2">
<div class="rounded-xl border border-border bg-card/40 p-4">
<div class="mb-3 flex items-center justify-between">
<h2 class="text-sm font-semibold text-foreground">People you might know</h2>
<a
routerLink="/people"
class="text-xs font-medium text-primary hover:underline"
>See all</a
>
</div>
@if (peopleYouMightKnow().length > 0) {
<div class="space-y-1">
@for (person of peopleYouMightKnow(); track person.id) {
<div class="flex items-center gap-3 rounded-lg px-2 py-2 transition-colors hover:bg-secondary/60">
<!-- Your friends -->
@if (friends().length > 0) {
<section>
<div class="mb-3 flex items-center justify-between">
<h2 class="text-sm font-semibold text-foreground">Your Friends</h2>
<a
routerLink="/people"
class="text-xs font-medium text-primary hover:underline"
>Manage</a
>
</div>
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
@for (friend of friends(); track friend.id) {
<div class="flex items-center gap-3 rounded-xl border border-border bg-card p-3">
<app-user-avatar
[name]="personLabel(person)"
[avatarUrl]="person.avatarUrl"
[name]="personLabel(friend)"
[avatarUrl]="friend.avatarUrl"
size="md"
[status]="person.status"
[status]="friend.status"
[showStatusBadge]="true"
/>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-foreground">{{ personLabel(person) }}</p>
<p class="text-xs text-muted-foreground">{{ isOnline(person) ? 'Online' : 'Offline' }}</p>
<p class="truncate text-sm font-medium text-foreground">{{ personLabel(friend) }}</p>
<p class="text-xs text-muted-foreground">{{ isOnline(friend) ? 'Online' : 'Offline' }}</p>
</div>
<app-friend-button [user]="person" />
<app-friend-button [user]="friend" />
</div>
}
</div>
} @else {
<p class="py-6 text-center text-sm text-muted-foreground">No people to suggest yet.</p>
}
</div>
</section>
}
<div class="rounded-xl border border-border bg-card/40 p-4">
<div class="mb-3 flex items-center justify-between">
<h2 class="text-sm font-semibold text-foreground">Popular Servers</h2>
<a
routerLink="/servers"
class="text-xs font-medium text-primary hover:underline"
>See all</a
>
</div>
@if (popularServers().length > 0) {
<div class="space-y-1">
@for (server of popularServers(); track server.id) {
<div class="flex items-center gap-3 rounded-lg px-2 py-2 transition-colors hover:bg-secondary/60">
<div class="grid h-10 w-10 shrink-0 place-items-center overflow-hidden rounded-lg bg-secondary text-sm font-semibold text-foreground">
@if (server.icon) {
<!-- Recently active servers -->
@if (recentlyActiveServers().length > 0) {
<section>
<h2 class="mb-3 text-sm font-semibold text-foreground">Recently Active Servers</h2>
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
@for (room of recentlyActiveServers(); track room.id) {
<button
type="button"
class="flex flex-col items-center gap-2 rounded-xl border border-border bg-card p-4 text-center transition-colors hover:border-primary/50 hover:bg-card/80"
(click)="openSavedRoom(room)"
>
<div
class="grid h-12 w-12 shrink-0 place-items-center overflow-hidden rounded-xl bg-secondary text-base font-semibold text-foreground"
>
@if (room.icon) {
<div
aria-hidden="true"
class="h-full w-full bg-cover bg-center bg-no-repeat"
[style.backgroundImage]="'url(' + server.icon + ')'"
[style.backgroundImage]="'url(' + room.icon + ')'"
></div>
} @else {
{{ serverInitial(server) }}
{{ room.name[0]?.toUpperCase() || '?' }}
}
</div>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-foreground">{{ server.name }}</p>
<p class="truncate text-xs text-muted-foreground">{{ serverMetaLabel(server) }}</p>
</div>
<button
type="button"
class="shrink-0 rounded-md bg-primary px-3 py-1.5 text-xs font-semibold text-primary-foreground transition-colors hover:bg-primary/90"
(click)="openServer(server)"
>
Join
</button>
</div>
<p class="w-full truncate text-sm font-medium text-foreground">{{ room.name }}</p>
<p class="text-xs text-muted-foreground">{{ room.userCount }} members</p>
</button>
}
</div>
} @else {
<p class="py-6 text-center text-sm text-muted-foreground">No popular servers right now.</p>
}
</div>
</section>
<!-- Your friends -->
@if (friends().length > 0) {
<section>
<div class="mb-3 flex items-center justify-between">
<h2 class="text-sm font-semibold text-foreground">Your Friends</h2>
<a
routerLink="/people"
class="text-xs font-medium text-primary hover:underline"
>Manage</a
>
</div>
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
@for (friend of friends(); track friend.id) {
<div class="flex items-center gap-3 rounded-xl border border-border bg-card p-3">
<app-user-avatar
[name]="personLabel(friend)"
[avatarUrl]="friend.avatarUrl"
size="md"
[status]="friend.status"
[showStatusBadge]="true"
/>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-foreground">{{ personLabel(friend) }}</p>
<p class="text-xs text-muted-foreground">{{ isOnline(friend) ? 'Online' : 'Offline' }}</p>
</div>
<app-friend-button [user]="friend" />
</div>
}
</div>
</section>
</section>
}
}
<!-- Recently active servers -->
@if (recentlyActiveServers().length > 0) {
<section>
<h2 class="mb-3 text-sm font-semibold text-foreground">Recently Active Servers</h2>
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
@for (room of recentlyActiveServers(); track room.id) {
<button
type="button"
class="flex flex-col items-center gap-2 rounded-xl border border-border bg-card p-4 text-center transition-colors hover:border-primary/50 hover:bg-card/80"
(click)="openSavedRoom(room)"
>
<div class="grid h-12 w-12 shrink-0 place-items-center overflow-hidden rounded-xl bg-secondary text-base font-semibold text-foreground">
@if (room.icon) {
<div
aria-hidden="true"
class="h-full w-full bg-cover bg-center bg-no-repeat"
[style.backgroundImage]="'url(' + room.icon + ')'"
></div>
} @else {
{{ room.name[0]?.toUpperCase() || '?' }}
}
</div>
<p class="w-full truncate text-sm font-medium text-foreground">{{ room.name }}</p>
<p class="text-xs text-muted-foreground">{{ room.userCount }} members</p>
</button>
}
</div>
</section>
}
}
</div>
</div>
</div>
</ng-template>
@if (isMobile()) {