This commit is contained in:
2025-12-28 05:37:19 +01:00
commit 87c722b5ae
74 changed files with 10264 additions and 0 deletions

View File

@@ -0,0 +1,340 @@
import { Component, inject, OnInit, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { debounceTime, distinctUntilChanged, Subject } from 'rxjs';
import { NgIcon, provideIcons } from '@ng-icons/core';
import {
lucideSearch,
lucideUsers,
lucideLock,
lucideGlobe,
lucidePlus,
lucideSettings,
} from '@ng-icons/lucide';
import * as RoomsActions from '../../store/rooms/rooms.actions';
import {
selectSearchResults,
selectIsSearching,
selectRoomsError,
selectSavedRooms,
} from '../../store/rooms/rooms.selectors';
import { Room } from '../../core/models';
import { ServerInfo } from '../../core/models';
@Component({
selector: 'app-server-search',
standalone: true,
imports: [CommonModule, FormsModule, NgIcon],
viewProviders: [
provideIcons({ lucideSearch, lucideUsers, lucideLock, lucideGlobe, lucidePlus, lucideSettings }),
],
template: `
<div class="flex flex-col h-full">
<!-- My Servers -->
<div class="p-4 border-b border-border">
<h3 class="font-semibold text-foreground mb-2">My Servers</h3>
@if (savedRooms().length === 0) {
<p class="text-sm text-muted-foreground">No joined servers yet</p>
} @else {
<div class="flex flex-wrap gap-2">
@for (room of savedRooms(); track room.id) {
<button
(click)="joinSavedRoom(room)"
class="px-3 py-1.5 text-xs rounded-full bg-secondary hover:bg-secondary/80 border border-border text-foreground"
>
{{ room.name }}
</button>
}
</div>
}
</div>
<!-- Search Header -->
<div class="p-4 border-b border-border">
<div class="flex items-center gap-2">
<div class="relative flex-1">
<ng-icon
name="lucideSearch"
class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground w-4 h-4"
/>
<input
type="text"
[(ngModel)]="searchQuery"
(ngModelChange)="onSearchChange($event)"
placeholder="Search servers..."
class="w-full pl-10 pr-4 py-2 bg-secondary rounded-lg border border-border text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
<button
(click)="openSettings()"
class="p-2 bg-secondary hover:bg-secondary/80 rounded-lg border border-border transition-colors"
title="Settings"
>
<ng-icon name="lucideSettings" class="w-5 h-5 text-muted-foreground" />
</button>
</div>
</div>
<!-- Create Server Button -->
<div class="p-4 border-b border-border">
<button
(click)="openCreateDialog()"
class="w-full flex items-center justify-center gap-2 px-4 py-3 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors"
>
<ng-icon name="lucidePlus" class="w-4 h-4" />
Create New Server
</button>
</div>
<!-- Search Results -->
<div class="flex-1 overflow-y-auto">
@if (isSearching()) {
<div class="flex items-center justify-center py-8">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
} @else if (searchResults().length === 0) {
<div class="flex flex-col items-center justify-center py-12 text-muted-foreground">
<ng-icon name="lucideSearch" class="w-12 h-12 mb-4 opacity-50" />
<p class="text-lg">No servers found</p>
<p class="text-sm">Try a different search or create your own</p>
</div>
} @else {
<div class="p-4 space-y-3">
@for (server of searchResults(); track server.id) {
<button
(click)="joinServer(server)"
class="w-full p-4 bg-card rounded-lg border border-border hover:border-primary/50 hover:bg-card/80 transition-all text-left group"
>
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center gap-2">
<h3 class="font-semibold text-foreground group-hover:text-primary transition-colors">
{{ server.name }}
</h3>
@if (server.isPrivate) {
<ng-icon name="lucideLock" class="w-4 h-4 text-muted-foreground" />
} @else {
<ng-icon name="lucideGlobe" class="w-4 h-4 text-muted-foreground" />
}
</div>
@if (server.description) {
<p class="text-sm text-muted-foreground mt-1 line-clamp-2">
{{ server.description }}
</p>
}
@if (server.topic) {
<span class="inline-block mt-2 px-2 py-0.5 text-xs bg-secondary rounded-full text-muted-foreground">
{{ server.topic }}
</span>
}
</div>
<div class="flex items-center gap-1 text-muted-foreground text-sm ml-4">
<ng-icon name="lucideUsers" class="w-4 h-4" />
<span>{{ server.userCount }}/{{ server.maxUsers }}</span>
</div>
</div>
<div class="mt-2 text-xs text-muted-foreground">
Hosted by {{ server.hostName }}
</div>
</button>
}
</div>
}
</div>
@if (error()) {
<div class="p-4 bg-destructive/10 border-t border-destructive">
<p class="text-sm text-destructive">{{ error() }}</p>
</div>
}
</div>
<!-- Create Server Dialog -->
@if (showCreateDialog()) {
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50" (click)="closeCreateDialog()">
<div class="bg-card border border-border rounded-lg p-6 w-full max-w-md m-4" (click)="$event.stopPropagation()">
<h2 class="text-xl font-semibold text-foreground mb-4">Create Server</h2>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-foreground mb-1">Server Name</label>
<input
type="text"
[(ngModel)]="newServerName"
placeholder="My Awesome Server"
class="w-full px-3 py-2 bg-secondary rounded-lg border border-border text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
<div>
<label class="block text-sm font-medium text-foreground mb-1">Description (optional)</label>
<textarea
[(ngModel)]="newServerDescription"
placeholder="What's your server about?"
rows="3"
class="w-full px-3 py-2 bg-secondary rounded-lg border border-border text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary resize-none"
></textarea>
</div>
<div>
<label class="block text-sm font-medium text-foreground mb-1">Topic (optional)</label>
<input
type="text"
[(ngModel)]="newServerTopic"
placeholder="gaming, music, coding..."
class="w-full px-3 py-2 bg-secondary rounded-lg border border-border text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
<div class="flex items-center gap-2">
<input
type="checkbox"
[(ngModel)]="newServerPrivate"
id="private"
class="w-4 h-4 rounded border-border bg-secondary"
/>
<label for="private" class="text-sm text-foreground">Private server</label>
</div>
@if (newServerPrivate()) {
<div>
<label class="block text-sm font-medium text-foreground mb-1">Password</label>
<input
type="password"
[(ngModel)]="newServerPassword"
placeholder="Enter password"
class="w-full px-3 py-2 bg-secondary rounded-lg border border-border text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
}
</div>
<div class="flex gap-3 mt-6">
<button
(click)="closeCreateDialog()"
class="flex-1 px-4 py-2 bg-secondary text-foreground rounded-lg hover:bg-secondary/80 transition-colors"
>
Cancel
</button>
<button
(click)="createServer()"
[disabled]="!newServerName()"
class="flex-1 px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
Create
</button>
</div>
</div>
</div>
}
`,
})
export class ServerSearchComponent implements OnInit {
private store = inject(Store);
private router = inject(Router);
private searchSubject = new Subject<string>();
searchQuery = '';
searchResults = this.store.selectSignal(selectSearchResults);
isSearching = this.store.selectSignal(selectIsSearching);
error = this.store.selectSignal(selectRoomsError);
savedRooms = this.store.selectSignal(selectSavedRooms);
// Create dialog state
showCreateDialog = signal(false);
newServerName = signal('');
newServerDescription = signal('');
newServerTopic = signal('');
newServerPrivate = signal(false);
newServerPassword = signal('');
ngOnInit(): void {
// Initial load
this.store.dispatch(RoomsActions.searchServers({ query: '' }));
this.store.dispatch(RoomsActions.loadRooms());
// Setup debounced search
this.searchSubject
.pipe(debounceTime(300), distinctUntilChanged())
.subscribe((query) => {
this.store.dispatch(RoomsActions.searchServers({ query }));
});
}
onSearchChange(query: string): void {
this.searchSubject.next(query);
}
joinServer(server: ServerInfo): void {
const currentUserId = localStorage.getItem('metoyou_currentUserId');
if (!currentUserId) {
this.router.navigate(['/login']);
return;
}
this.store.dispatch(RoomsActions.joinRoom({
roomId: server.id,
serverInfo: {
name: server.name,
description: server.description,
hostName: server.hostName,
}
}));
}
openCreateDialog(): void {
this.showCreateDialog.set(true);
}
closeCreateDialog(): void {
this.showCreateDialog.set(false);
this.resetCreateForm();
}
createServer(): void {
if (!this.newServerName()) return;
const currentUserId = localStorage.getItem('metoyou_currentUserId');
if (!currentUserId) {
this.router.navigate(['/login']);
return;
}
this.store.dispatch(
RoomsActions.createRoom({
name: this.newServerName(),
description: this.newServerDescription() || undefined,
topic: this.newServerTopic() || undefined,
isPrivate: this.newServerPrivate(),
password: this.newServerPrivate() ? this.newServerPassword() : undefined,
})
);
this.closeCreateDialog();
}
openSettings(): void {
this.router.navigate(['/settings']);
}
joinSavedRoom(room: Room): void {
this.joinServer({
id: room.id,
name: room.name,
description: room.description,
hostName: room.hostId || 'Unknown',
userCount: room.userCount,
maxUsers: room.maxUsers || 50,
isPrivate: !!room.password,
createdAt: room.createdAt,
} as any);
}
private resetCreateForm(): void {
this.newServerName.set('');
this.newServerDescription.set('');
this.newServerTopic.set('');
this.newServerPrivate.set(false);
this.newServerPassword.set('');
}
}