Add user avatar management and moderation
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user