Add user avatar management and moderation

This commit is contained in:
JB
2026-07-25 00:17:18 -04:00
parent c0834804d4
commit 3f78f0f9d8
30 changed files with 915 additions and 18 deletions
+29 -1
View File
@@ -4,6 +4,7 @@ import { hashPassword, verifyPassword } from '$lib/server/auth/password';
import { getDatabase } from '$lib/server/database';
import { fail, redirect } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';
import { AVATAR_PRESETS, AvatarValidationError, processAvatarUpload, saveUploadedAvatar, selectPresetAvatar } from '$lib/server/avatars';
export const load: PageServerLoad = async ({ cookies, url }) => {
const user = await getSessionUser(cookies);
@@ -51,11 +52,38 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
hasPassword: typeof connection?.password_hash === 'string',
chooseDisplayName: url.searchParams.has('choose_display_name'),
discordLinked: url.searchParams.has('discord_linked'),
oauthError
oauthError,
avatarPresets: AVATAR_PRESETS
};
};
export const actions: Actions = {
selectAvatar: async ({ request, cookies }) => {
const user = await getSessionUser(cookies);
if (!user) redirect(303, '/sign-in');
const preset = String((await request.formData()).get('preset') ?? '');
try {
await selectPresetAvatar(await getDatabase(), user.id, preset);
return { avatarChanged: true };
} catch (error) {
if (error instanceof AvatarValidationError) return fail(400, { avatarError: error.message });
throw error;
}
},
uploadAvatar: async ({ request, cookies }) => {
const user = await getSessionUser(cookies);
if (!user) redirect(303, '/sign-in');
const file = (await request.formData()).get('avatar');
if (!(file instanceof File)) return fail(400, { avatarError: 'Choose an image to upload.' });
try {
const output = await processAvatarUpload(file);
await saveUploadedAvatar(await getDatabase(), user.id, output);
return { avatarChanged: true };
} catch (error) {
if (error instanceof AvatarValidationError) return fail(400, { avatarError: error.message });
throw error;
}
},
changeLoginUsername: async ({ request, cookies }) => {
const user = await getSessionUser(cookies);
if (!user) redirect(303, '/sign-in');
+19 -1
View File
@@ -12,7 +12,7 @@
<svelte:head><title>{user.displayName} — SHD.build</title></svelte:head>
<main class="page profile-page">
<section class="profile-command">
<div class="profile-ident"><span class="profile-avatar">{user.displayName.slice(0, 1).toUpperCase()}</span><div><p class="eyebrow"><span></span> AGENT PROFILE</p><h1>{user.displayName}</h1><span class="role-badge">{user.role.toUpperCase()} ACCESS</span></div></div>
<div class="profile-ident"><img class="profile-avatar" src={user.avatarUrl} alt={`${user.displayName}'s avatar`} /><div><p class="eyebrow"><span></span> AGENT PROFILE</p><h1>{user.displayName}</h1><span class="role-badge">{user.role.toUpperCase()} ACCESS</span></div></div>
<div class="profile-metrics"><div><strong>{data.account.builds.length}</strong><span>BUILDS</span></div><div><strong>{data.account.guides.length}</strong><span>GUIDES</span></div><div><strong>{data.account.favorites.length}</strong><span>FAVORITES</span></div><div><strong>{data.account.ratings.length}</strong><span>RATED</span></div></div>
</section>
{#if data.chooseDisplayName}<div class="account-alert"><b>IDENTITY SETUP</b><span>Choose the SHD.build display name other agents will see. Unicode characters are supported.</span></div>{/if}
@@ -28,6 +28,24 @@
</section>
<aside class="account-console">
<div class="console-head"><span>ACCOUNT CONTROL</span><i></i></div>
<p class="eyebrow">AGENT PORTRAIT</p><h2>AVATAR</h2>
<div class="avatar-picker" aria-label="SHD avatar presets">
{#each data.avatarPresets as preset}
<PendingForm action="?/selectAvatar" class="avatar-preset-form">
<button class="avatar-preset" class:selected={user.avatarUrl === preset.url} name="preset" value={preset.id} aria-label={`Use ${preset.label} avatar`} aria-pressed={user.avatarUrl === preset.url}>
<img src={preset.url} alt="" /><span>{preset.label}</span>
</button>
</PendingForm>
{/each}
</div>
<PendingForm action="?/uploadAvatar" enctype="multipart/form-data" class="avatar-upload">
<label class="field">CUSTOM AVATAR<input name="avatar" type="file" accept="image/jpeg,image/png,image/webp,image/avif" required /></label>
<small class="identity-note">JPEG, PNG, WebP, or AVIF · 5 MiB maximum · cropped to a 256 px square.</small>
{#if form?.avatarError}<p class="error">{form.avatarError}</p>{/if}
{#if form?.avatarChanged}<p class="success">Avatar updated.</p>{/if}
<button class="secondary console-action">UPLOAD CUSTOM AVATAR →</button>
</PendingForm>
<hr/>
<p class="eyebrow">PUBLIC IDENTITY</p><h2>DISPLAY NAME</h2>
<PendingForm action="?/changeDisplayName"><label class="field">PUBLIC DISPLAY NAME<input name="displayName" value={user.displayName} maxlength="80" required /></label><small class="identity-note">Visible to other agents. Unicode, spaces, and non-ASCII writing systems are supported.</small>{#if form?.displayNameError}<p class="error">{form.displayNameError}</p>{/if}<button class="primary console-action">UPDATE DISPLAY NAME →</button></PendingForm>
<hr/><p class="eyebrow">PRIVATE CREDENTIAL</p><h2>LOGIN USERNAME</h2>