Move toju-app into own its folder

This commit is contained in:
2026-03-29 23:30:37 +02:00
parent 0467a7b612
commit 8162e0444a
287 changed files with 42 additions and 34 deletions

View File

@@ -0,0 +1,227 @@
@if (serverData()) {
<div class="space-y-5 max-w-xl">
<section>
<h4 class="text-sm font-semibold text-foreground mb-3">Room Settings</h4>
@if (!isAdmin()) {
<p class="text-xs text-muted-foreground mb-3">
You are viewing this server's settings as a non-admin. Only the server owner can make changes.
</p>
}
<div class="space-y-4">
<div>
<label
for="room-name"
class="block text-xs font-medium text-muted-foreground mb-1"
>Room Name</label
>
<input
type="text"
[(ngModel)]="roomName"
[readOnly]="!isAdmin()"
id="room-name"
class="w-full px-3 py-2 bg-secondary rounded-lg border border-border text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary"
[class.opacity-60]="!isAdmin()"
[class.cursor-not-allowed]="!isAdmin()"
/>
</div>
<div>
<label
for="room-description"
class="block text-xs font-medium text-muted-foreground mb-1"
>Description</label
>
<textarea
[(ngModel)]="roomDescription"
[readOnly]="!isAdmin()"
rows="3"
id="room-description"
class="w-full px-3 py-2 bg-secondary rounded-lg border border-border text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary resize-none"
[class.opacity-60]="!isAdmin()"
[class.cursor-not-allowed]="!isAdmin()"
></textarea>
</div>
@if (isAdmin()) {
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-foreground">Private Room</p>
<p class="text-xs text-muted-foreground">Require approval to join</p>
</div>
<button
(click)="togglePrivate()"
type="button"
class="p-2 rounded-lg transition-colors"
[class.bg-primary]="isPrivate()"
[class.text-primary-foreground]="isPrivate()"
[class.bg-secondary]="!isPrivate()"
[class.text-muted-foreground]="!isPrivate()"
>
@if (isPrivate()) {
<ng-icon
name="lucideLock"
class="w-4 h-4"
/>
} @else {
<ng-icon
name="lucideUnlock"
class="w-4 h-4"
/>
}
</button>
</div>
} @else {
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-foreground">Private Room</p>
<p class="text-xs text-muted-foreground">Require approval to join</p>
</div>
<span class="text-sm text-muted-foreground">{{ isPrivate() ? 'Yes' : 'No' }}</span>
</div>
}
<div>
<label
for="room-max-users"
class="block text-xs font-medium text-muted-foreground mb-1"
>
Max Users (0 = unlimited)
</label>
<input
type="number"
[(ngModel)]="maxUsers"
[readOnly]="!isAdmin()"
min="0"
id="room-max-users"
class="w-full px-3 py-2 bg-secondary rounded-lg border border-border text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary"
[class.opacity-60]="!isAdmin()"
[class.cursor-not-allowed]="!isAdmin()"
/>
</div>
@if (isAdmin()) {
<div class="rounded-lg border border-border bg-secondary/40 p-4 space-y-3">
<div class="flex items-start justify-between gap-4">
<div>
<p class="text-sm font-medium text-foreground">Server Password</p>
<p class="text-xs text-muted-foreground">
@if (hasPassword() && passwordAction() !== 'remove') {
Joined members stay whitelisted until they are kicked or banned.
} @else {
Add an optional password so new members need it to join.
}
</p>
</div>
@if (hasPassword() && passwordAction() !== 'remove') {
<button
type="button"
(click)="markPasswordForRemoval()"
class="rounded-lg border border-border px-3 py-2 text-xs font-medium text-muted-foreground transition-colors hover:bg-secondary"
>
Remove Password
</button>
} @else if (hasPassword() && passwordAction() === 'remove') {
<button
type="button"
(click)="keepCurrentPassword()"
class="rounded-lg border border-border px-3 py-2 text-xs font-medium text-muted-foreground transition-colors hover:bg-secondary"
>
Keep Password
</button>
}
</div>
<div class="text-xs text-muted-foreground">
@if (hasPassword() && passwordAction() !== 'remove') {
Password protection is currently enabled.
} @else if (hasPassword() && passwordAction() === 'remove') {
Password protection will be removed when you save.
} @else {
Password protection is currently disabled.
}
</div>
<div>
<label
for="room-password"
class="block text-xs font-medium text-muted-foreground mb-1"
>
{{ hasPassword() ? 'Set New Password' : 'Set Password' }}
</label>
<input
type="password"
id="room-password"
[ngModel]="roomPassword"
(ngModelChange)="onPasswordInput($event)"
class="w-full px-3 py-2 bg-secondary rounded-lg border border-border text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary"
[placeholder]="hasPassword() ? 'Leave blank to keep the current password' : 'Optional password required for new joins'"
/>
@if (passwordAction() === 'update') {
<p class="mt-2 text-xs text-muted-foreground">The new password will replace the current one when you save.</p>
}
@if (passwordError()) {
<p class="mt-2 text-xs text-destructive">{{ passwordError() }}</p>
}
</div>
</div>
} @else {
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-foreground">Server Password</p>
<p class="text-xs text-muted-foreground">Invite links bypass the password, but bans still apply.</p>
</div>
<span class="text-sm text-muted-foreground">{{ hasPassword() ? 'Enabled' : 'Disabled' }}</span>
</div>
}
</div>
</section>
@if (isAdmin()) {
<button
(click)="saveServerSettings()"
type="button"
class="w-full px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors flex items-center justify-center gap-2 text-sm"
[class.bg-green-600]="saveSuccess() === 'server'"
[class.hover:bg-green-600]="saveSuccess() === 'server'"
>
<ng-icon
name="lucideCheck"
class="w-4 h-4"
/>
{{ saveSuccess() === 'server' ? 'Saved!' : 'Save Settings' }}
</button>
<!-- Danger Zone -->
<div class="pt-4 border-t border-border">
<h4 class="text-sm font-medium text-destructive mb-3">Danger Zone</h4>
<button
(click)="confirmDeleteRoom()"
type="button"
class="w-full px-4 py-2 bg-destructive/10 text-destructive border border-destructive/20 rounded-lg hover:bg-destructive/20 transition-colors flex items-center justify-center gap-2 text-sm"
>
<ng-icon
name="lucideTrash2"
class="w-4 h-4"
/>
Delete Room
</button>
</div>
}
</div>
<!-- Delete Confirmation (sub-modal) -->
@if (showDeleteConfirm()) {
<app-confirm-dialog
title="Delete Room"
confirmLabel="Delete Room"
variant="danger"
[widthClass]="'w-96 max-w-[90vw]'"
(confirmed)="deleteRoom()"
(cancelled)="showDeleteConfirm.set(false)"
>
<p>Are you sure you want to delete this room? This action cannot be undone.</p>
</app-confirm-dialog>
}
} @else {
<div class="flex items-center justify-center h-40 text-muted-foreground text-sm">Select a server from the sidebar to manage</div>
}

View File

@@ -0,0 +1,181 @@
/* eslint-disable @typescript-eslint/member-ordering */
import {
Component,
effect,
inject,
input,
signal
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { Store } from '@ngrx/store';
import {
lucideCheck,
lucideTrash2,
lucideLock,
lucideUnlock
} from '@ng-icons/lucide';
import { Room } from '../../../../shared-kernel';
import { RoomsActions } from '../../../../store/rooms/rooms.actions';
import { ConfirmDialogComponent } from '../../../../shared';
import { SettingsModalService } from '../../../../core/services/settings-modal.service';
@Component({
selector: 'app-server-settings',
standalone: true,
imports: [
CommonModule,
FormsModule,
NgIcon,
ConfirmDialogComponent
],
viewProviders: [
provideIcons({
lucideCheck,
lucideTrash2,
lucideLock,
lucideUnlock
})
],
templateUrl: './server-settings.component.html'
})
export class ServerSettingsComponent {
private store = inject(Store);
private modal = inject(SettingsModalService);
/** The currently selected server, passed from the parent. */
server = input<Room | null>(null);
/** Whether the current user is admin of this server. */
isAdmin = input(false);
roomName = '';
roomDescription = '';
isPrivate = signal(false);
hasPassword = signal(false);
passwordAction = signal<'keep' | 'update' | 'remove'>('keep');
passwordError = signal<string | null>(null);
roomPassword = '';
maxUsers = 0;
showDeleteConfirm = signal(false);
saveSuccess = signal<string | null>(null);
private saveTimeout: ReturnType<typeof setTimeout> | null = null;
/** Reload form fields whenever the server input changes. */
readonly serverData = this.server;
constructor() {
effect(() => {
const room = this.server();
if (!room)
return;
this.roomName = room.name;
this.roomDescription = room.description || '';
this.isPrivate.set(room.isPrivate);
this.hasPassword.set(typeof room.hasPassword === 'boolean' ? room.hasPassword : !!room.password);
this.passwordAction.set('keep');
this.passwordError.set(null);
this.roomPassword = '';
this.maxUsers = room.maxUsers || 0;
});
}
togglePrivate(): void {
this.isPrivate.update((currentValue) => !currentValue);
}
saveServerSettings(): void {
const room = this.server();
if (!room)
return;
const normalizedPassword = this.roomPassword.trim();
const settings: {
description: string;
hasPassword?: boolean;
isPrivate: boolean;
maxUsers: number;
name: string;
password?: string;
} = {
name: this.roomName,
description: this.roomDescription,
isPrivate: this.isPrivate(),
maxUsers: this.maxUsers
};
if (this.passwordAction() === 'remove') {
settings.password = '';
settings.hasPassword = false;
} else if (normalizedPassword) {
settings.password = normalizedPassword;
settings.hasPassword = true;
}
this.store.dispatch(
RoomsActions.updateRoomSettings({
roomId: room.id,
settings
})
);
this.hasPassword.set(settings.hasPassword ?? this.hasPassword());
this.passwordAction.set('keep');
this.passwordError.set(null);
this.roomPassword = '';
this.showSaveSuccess('server');
}
markPasswordForRemoval(): void {
this.passwordAction.set('remove');
this.passwordError.set(null);
this.roomPassword = '';
}
keepCurrentPassword(): void {
this.passwordAction.set('keep');
this.passwordError.set(null);
this.roomPassword = '';
}
onPasswordInput(value: string): void {
this.roomPassword = value;
this.passwordError.set(null);
if (value.trim().length > 0) {
this.passwordAction.set('update');
return;
}
this.passwordAction.set('keep');
}
confirmDeleteRoom(): void {
this.showDeleteConfirm.set(true);
}
deleteRoom(): void {
const room = this.server();
if (!room)
return;
this.store.dispatch(RoomsActions.deleteRoom({ roomId: room.id }));
this.showDeleteConfirm.set(false);
this.modal.navigate('network');
}
private showSaveSuccess(key: string): void {
this.saveSuccess.set(key);
if (this.saveTimeout)
clearTimeout(this.saveTimeout);
this.saveTimeout = setTimeout(() => this.saveSuccess.set(null), 2000);
}
}