init
This commit is contained in:
340
src/app/features/server-search/server-search.component.ts
Normal file
340
src/app/features/server-search/server-search.component.ts
Normal 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('');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user