feat: server image
This commit is contained in:
@@ -3,11 +3,74 @@
|
||||
<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>
|
||||
<p class="text-xs text-muted-foreground mb-3">You are viewing this server's details without server-management permission.</p>
|
||||
}
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-lg border border-border bg-secondary/40 p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="grid h-14 w-14 shrink-0 place-items-center overflow-hidden rounded-lg bg-secondary text-base font-semibold text-foreground">
|
||||
@if (serverData()?.icon) {
|
||||
<img
|
||||
[src]="serverData()!.icon"
|
||||
[alt]="serverData()!.name + ' icon'"
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
} @else {
|
||||
<ng-icon
|
||||
name="lucideImage"
|
||||
class="h-6 w-6 text-muted-foreground"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-medium text-foreground">Server Image</p>
|
||||
<p class="text-xs text-muted-foreground">Synced to members and shown in server discovery.</p>
|
||||
@if (iconError()) {
|
||||
<p class="mt-1 text-xs text-destructive">{{ iconError() }}</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (canManageIcon()) {
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<label
|
||||
for="server-icon-upload"
|
||||
class="grid h-9 w-9 cursor-pointer place-items-center rounded-lg border border-border bg-card text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
|
||||
title="Upload image"
|
||||
aria-label="Upload server image"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideUpload"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
id="server-icon-upload"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="sr-only"
|
||||
(change)="onServerIconSelected($event)"
|
||||
/>
|
||||
|
||||
@if (serverData()?.icon) {
|
||||
<button
|
||||
type="button"
|
||||
class="grid h-9 w-9 place-items-center rounded-lg border border-border bg-card text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
|
||||
title="Remove image"
|
||||
aria-label="Remove server image"
|
||||
(click)="removeServerIcon()"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideX"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="room-name"
|
||||
@@ -191,21 +254,23 @@
|
||||
{{ 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>
|
||||
@if (canDeleteServer()) {
|
||||
<!-- 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>
|
||||
|
||||
|
||||
@@ -12,9 +12,12 @@ import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import {
|
||||
lucideCheck,
|
||||
lucideImage,
|
||||
lucideTrash2,
|
||||
lucideLock,
|
||||
lucideUnlock
|
||||
lucideUnlock,
|
||||
lucideUpload,
|
||||
lucideX
|
||||
} from '@ng-icons/lucide';
|
||||
|
||||
import { Room } from '../../../../shared-kernel';
|
||||
@@ -34,9 +37,12 @@ import { SettingsModalService } from '../../../../core/services/settings-modal.s
|
||||
viewProviders: [
|
||||
provideIcons({
|
||||
lucideCheck,
|
||||
lucideImage,
|
||||
lucideTrash2,
|
||||
lucideLock,
|
||||
lucideUnlock
|
||||
lucideUnlock,
|
||||
lucideUpload,
|
||||
lucideX
|
||||
})
|
||||
],
|
||||
templateUrl: './server-settings.component.html'
|
||||
@@ -49,6 +55,10 @@ export class ServerSettingsComponent {
|
||||
server = input<Room | null>(null);
|
||||
/** Whether the current user is admin of this server. */
|
||||
isAdmin = input(false);
|
||||
/** Whether the current user can manage this server's icon. */
|
||||
canManageIcon = input(false);
|
||||
/** Whether the current user can delete this server. */
|
||||
canDeleteServer = input(false);
|
||||
|
||||
roomName = '';
|
||||
roomDescription = '';
|
||||
@@ -59,6 +69,7 @@ export class ServerSettingsComponent {
|
||||
roomPassword = '';
|
||||
maxUsers = 0;
|
||||
showDeleteConfirm = signal(false);
|
||||
iconError = signal<string | null>(null);
|
||||
|
||||
saveSuccess = signal<string | null>(null);
|
||||
private saveTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
@@ -170,6 +181,64 @@ export class ServerSettingsComponent {
|
||||
this.modal.navigate('network');
|
||||
}
|
||||
|
||||
onServerIconSelected(event: Event): void {
|
||||
const inputElement = event.target as HTMLInputElement;
|
||||
const file = inputElement.files?.[0];
|
||||
|
||||
inputElement.value = '';
|
||||
|
||||
if (!file || !this.canManageIcon()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file.type.startsWith('image/')) {
|
||||
this.iconError.set('Choose an image file.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.size > 512 * 1024) {
|
||||
this.iconError.set('Choose an image smaller than 512 KB.');
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = () => {
|
||||
const room = this.server();
|
||||
const icon = typeof reader.result === 'string' ? reader.result : '';
|
||||
|
||||
if (!room || !icon) {
|
||||
this.iconError.set('Could not read that image.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.iconError.set(null);
|
||||
this.store.dispatch(RoomsActions.updateServerIcon({
|
||||
roomId: room.id,
|
||||
icon
|
||||
}));
|
||||
this.showSaveSuccess('icon');
|
||||
};
|
||||
|
||||
reader.onerror = () => this.iconError.set('Could not read that image.');
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
removeServerIcon(): void {
|
||||
const room = this.server();
|
||||
|
||||
if (!room || !this.canManageIcon()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.iconError.set(null);
|
||||
this.store.dispatch(RoomsActions.updateServerIcon({
|
||||
roomId: room.id,
|
||||
icon: ''
|
||||
}));
|
||||
this.showSaveSuccess('icon');
|
||||
}
|
||||
|
||||
private showSaveSuccess(key: string): void {
|
||||
this.saveSuccess.set(key);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user