add alt text, source
this code is fucking terrible. i'll clean it up later
This commit is contained in:
parent
b7068ed630
commit
968a49bd46
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "ava-node",
|
"name": "ava-node",
|
||||||
"version": "1.1.1",
|
"version": "1.2.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ava-node",
|
"name": "ava-node",
|
||||||
"version": "1.1.1",
|
"version": "1.2.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/inter": "^5.0.18",
|
"@fontsource-variable/inter": "^5.0.18",
|
||||||
"@fontsource-variable/noto-sans-mono": "^5.0.20",
|
"@fontsource-variable/noto-sans-mono": "^5.0.20",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "ava-node",
|
"name": "ava-node",
|
||||||
"version": "1.2.1",
|
"version": "1.3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Avatar" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
CONSTRAINT "Avatar_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("userId") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Avatar_id_key" ON "Avatar"("id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Avatar_userId_key" ON "Avatar"("userId");
|
|
@ -0,0 +1,3 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Avatar" ADD COLUMN "altText" TEXT;
|
||||||
|
ALTER TABLE "Avatar" ADD COLUMN "source" TEXT;
|
2
prisma/migrations/20240823034401_add_name/migration.sql
Normal file
2
prisma/migrations/20240823034401_add_name/migration.sql
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "name" TEXT;
|
|
@ -21,4 +21,14 @@ model Token {
|
||||||
model User {
|
model User {
|
||||||
userId String @id @unique
|
userId String @id @unique
|
||||||
identifier String
|
identifier String
|
||||||
|
name String?
|
||||||
|
avatar Avatar?
|
||||||
|
}
|
||||||
|
|
||||||
|
model Avatar {
|
||||||
|
id String @id @unique @default(uuid())
|
||||||
|
user User @relation(fields: [userId], references: [userId])
|
||||||
|
userId String @unique
|
||||||
|
altText String?
|
||||||
|
source String?
|
||||||
}
|
}
|
|
@ -4,6 +4,7 @@ import { join } from "node:path"
|
||||||
import { prisma } from "./clientsingleton"
|
import { prisma } from "./clientsingleton"
|
||||||
import configuration from "./configuration"
|
import configuration from "./configuration"
|
||||||
import Sharp, { type FormatEnum } from "sharp"
|
import Sharp, { type FormatEnum } from "sharp"
|
||||||
|
import type { Avatar } from "@prisma/client"
|
||||||
|
|
||||||
// todo: make customizable
|
// todo: make customizable
|
||||||
export const avatarDirectory = "./.data/avatars"
|
export const avatarDirectory = "./.data/avatars"
|
||||||
|
@ -104,6 +105,42 @@ export async function getPathToAvatarForIdentifier(identifier: string, size: num
|
||||||
return getPathToAvatarForUid(user?.userId, size, fmt)
|
return getPathToAvatarForUid(user?.userId, size, fmt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizeAvatar(avatar: Avatar | null) {
|
||||||
|
return avatar
|
||||||
|
? {
|
||||||
|
altText: avatar.altText || "",
|
||||||
|
source: avatar.source || "",
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
altText: "Default profile picture",
|
||||||
|
source: "https://git.sucks.win/split/ava",
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMetadataForIdentifier(identifier: string) {
|
||||||
|
let avatar = await prisma.avatar.findFirst({
|
||||||
|
where: {
|
||||||
|
user: {
|
||||||
|
identifier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return sanitizeAvatar(avatar)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMetadataForUserId(userId: string) {
|
||||||
|
let avatar = await prisma.avatar.findFirst({
|
||||||
|
where: {
|
||||||
|
userId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return sanitizeAvatar(avatar)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Render an avatar at the specified size and format
|
* @description Render an avatar at the specified size and format
|
||||||
* @param bin Image to rerender
|
* @param bin Image to rerender
|
||||||
|
@ -126,7 +163,7 @@ export async function renderAvatar(bin: ArrayBuffer|Buffer, squareSize: number,
|
||||||
if (format) img.toFormat(format)
|
if (format) img.toFormat(format)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
buf: await img.toBuffer(),
|
img,
|
||||||
extension: format || metadata.format,
|
extension: format || metadata.format,
|
||||||
requestedFormat: format,
|
requestedFormat: format,
|
||||||
squareSize,
|
squareSize,
|
||||||
|
@ -153,7 +190,7 @@ export async function writeAvatar(avatarDir: string, renderedAvatar: Awaited<Ret
|
||||||
|
|
||||||
await writeFile(
|
await writeFile(
|
||||||
targetPath,
|
targetPath,
|
||||||
renderedAvatar.buf
|
renderedAvatar.img
|
||||||
)
|
)
|
||||||
|
|
||||||
return targetPath
|
return targetPath
|
||||||
|
@ -163,9 +200,14 @@ export async function setNewAvatar(uid: string, avatar?: File) {
|
||||||
if (uid?.includes("/"))
|
if (uid?.includes("/"))
|
||||||
throw Error("UID cannot include /")
|
throw Error("UID cannot include /")
|
||||||
|
|
||||||
// Delete current avatar directory
|
// Delete current avatar directory and avatar database entry
|
||||||
const userAvatarDirectory = join(avatarDirectory, uid)
|
const userAvatarDirectory = join(avatarDirectory, uid)
|
||||||
await rm(userAvatarDirectory, { recursive: true, force: true })
|
await rm(userAvatarDirectory, { recursive: true, force: true })
|
||||||
|
await prisma.avatar.deleteMany({
|
||||||
|
where: {
|
||||||
|
userId: uid
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if (!avatar) return {} // we don't need to set a new one
|
if (!avatar) return {} // we don't need to set a new one
|
||||||
|
|
||||||
|
@ -194,5 +236,12 @@ export async function setNewAvatar(uid: string, avatar?: File) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create new Avatar database entry
|
||||||
|
await prisma.avatar.create({
|
||||||
|
data: {
|
||||||
|
userId: uid
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return time
|
return time
|
||||||
}
|
}
|
|
@ -1,38 +1,32 @@
|
||||||
import Sharp, { type FormatEnum } from "sharp"
|
import Sharp, { type FormatEnum } from 'sharp';
|
||||||
|
import { env } from '$env/dynamic/private';
|
||||||
const configuration = {
|
const configuration = {
|
||||||
oauth2: {
|
oauth2: {
|
||||||
endpoints: {
|
endpoints: {
|
||||||
authenticate: process.env.OAUTH2__AUTHENTICATE!,
|
authenticate: env.OAUTH2__AUTHENTICATE!,
|
||||||
logout: process.env.OAUTH2__LOGOUT,
|
logout: env.OAUTH2__LOGOUT,
|
||||||
token: process.env.OAUTH2__GET_TOKEN!
|
token: env.OAUTH2__GET_TOKEN!
|
||||||
},
|
},
|
||||||
client: {
|
client: {
|
||||||
id: process.env.OAUTH2__CLIENT_ID!,
|
id: env.OAUTH2__CLIENT_ID!,
|
||||||
secret: process.env.OAUTH2__CLIENT_SECRET!,
|
secret: env.OAUTH2__CLIENT_SECRET!,
|
||||||
scopes: process.env.OAUTH2__SCOPES!
|
scopes: env.OAUTH2__SCOPES!
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
userinfo: {
|
userinfo: {
|
||||||
route: process.env.USERINFO__ROUTE!,
|
route: env.USERINFO__ROUTE!,
|
||||||
identifier: process.env.USERINFO__IDENTIFIER!
|
identifier: env.USERINFO__IDENTIFIER!
|
||||||
},
|
},
|
||||||
images: {
|
images: {
|
||||||
permitted_input:
|
permitted_input: (env.ALLOWED_TYPES || env.IMAGES__ALLOWED_INPUT_FORMATS)?.split(',') || [],
|
||||||
(
|
default_resolution: parseInt((env.IMAGES__DEFAULT_RESOLUTION || '').toString(), 10) || 512,
|
||||||
process.env.ALLOWED_TYPES
|
extra_output_types:
|
||||||
|| process.env.IMAGES__ALLOWED_INPUT_FORMATS
|
(env.IMAGES__EXTRA_OUTPUT_FORMATS?.split(',').filter(
|
||||||
)?.split(",") || [],
|
(e) => e in Sharp.format
|
||||||
default_resolution: parseInt((process.env.IMAGES__DEFAULT_RESOLUTION || "").toString(), 10) || 512,
|
) as (keyof FormatEnum)[]) || [],
|
||||||
extra_output_types:
|
output_resolutions: env.IMAGES__OUTPUT_RESOLUTIONS?.split(',').map((e) => parseInt(e, 10)) || [
|
||||||
process.env.IMAGES__EXTRA_OUTPUT_FORMATS
|
1024, 512, 256, 128, 64, 32
|
||||||
?.split(",")
|
]
|
||||||
.filter(e => e in Sharp.format) as (keyof FormatEnum)[]
|
}
|
||||||
|| [],
|
};
|
||||||
output_resolutions:
|
export default configuration;
|
||||||
process.env.IMAGES__OUTPUT_RESOLUTIONS
|
|
||||||
?.split(",")
|
|
||||||
.map(e => parseInt(e,10))
|
|
||||||
|| [ 1024, 512, 256, 128, 64, 32 ]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export default configuration
|
|
||||||
|
|
15
src/routes/info/[identifier]/+page.server.ts
Normal file
15
src/routes/info/[identifier]/+page.server.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { getMetadataForIdentifier } from '$lib/avatars.js';
|
||||||
|
import { prisma } from '$lib/clientsingleton.js';
|
||||||
|
import { error, redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export async function load({ params : { identifier } }) {
|
||||||
|
let userInfo = await prisma.user.findFirst({ where: { identifier } })
|
||||||
|
if (!userInfo)
|
||||||
|
throw error(404, "User not found")
|
||||||
|
let metadata = await getMetadataForIdentifier(identifier)
|
||||||
|
return {
|
||||||
|
name: userInfo.name || identifier,
|
||||||
|
identifier,
|
||||||
|
...metadata
|
||||||
|
}
|
||||||
|
}
|
46
src/routes/info/[identifier]/+page.svelte
Normal file
46
src/routes/info/[identifier]/+page.svelte
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import FilePreviewSet from "../../set/FilePreviewSet.svelte";
|
||||||
|
|
||||||
|
export let data: {
|
||||||
|
identifier: string,
|
||||||
|
name: string,
|
||||||
|
altText: string,
|
||||||
|
source: string
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
h5, p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-weight: normal;
|
||||||
|
color: var(--link)
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<h1>{data.name}'s avatar</h1>
|
||||||
|
<FilePreviewSet avatarUrl="/avatar/{data.identifier}" style="border-radius:8px;" />
|
||||||
|
<br>
|
||||||
|
<FilePreviewSet avatarUrl="/avatar/{data.identifier}" style="border-radius:100%;" />
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<h5>Alt text</h5>
|
||||||
|
<p>
|
||||||
|
{#if data.altText}
|
||||||
|
{data.altText}
|
||||||
|
{:else}
|
||||||
|
<em>No alt text available</em>
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<h5>Source</h5>
|
||||||
|
<p>
|
||||||
|
{#if data.source}
|
||||||
|
{data.source}
|
||||||
|
{:else}
|
||||||
|
<em>No source available</em>
|
||||||
|
{/if}
|
||||||
|
</p>
|
10
src/routes/info/[identifier]/json/+server.ts
Normal file
10
src/routes/info/[identifier]/json/+server.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { getMetadataForIdentifier } from '$lib/avatars.js';
|
||||||
|
import { error, redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export async function GET({ params : { identifier } }) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify(
|
||||||
|
await getMetadataForIdentifier(identifier)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
15
src/routes/info/[identifier]/source/+server.ts
Normal file
15
src/routes/info/[identifier]/source/+server.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { getMetadataForIdentifier } from '$lib/avatars.js';
|
||||||
|
import { error, redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export async function GET({ params : { identifier } }) {
|
||||||
|
let { source } = await getMetadataForIdentifier(identifier)
|
||||||
|
let finalUrl: URL | string = `/info/${identifier}`
|
||||||
|
|
||||||
|
if (source) {
|
||||||
|
try {
|
||||||
|
finalUrl = new URL(source)
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw redirect(302, finalUrl)
|
||||||
|
}
|
|
@ -1,15 +1,17 @@
|
||||||
import {getRequestUser, launchLogin} from "$lib/oidc"
|
import {getRequestUser, launchLogin} from "$lib/oidc"
|
||||||
import configuration from "$lib/configuration.js";
|
import configuration from "$lib/configuration.js";
|
||||||
import { fail } from "@sveltejs/kit";
|
import { fail } from "@sveltejs/kit";
|
||||||
import { avatarDirectory, setNewAvatar } from "$lib/avatars.js";
|
import { avatarDirectory, getMetadataForUserId, setNewAvatar } from "$lib/avatars.js";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
import { prisma } from "$lib/clientsingleton";
|
||||||
export async function load({ request, parent, url }) {
|
export async function load({ request, parent, url }) {
|
||||||
const { user } = await parent();
|
const { user } = await parent();
|
||||||
if (!user)
|
if (!user)
|
||||||
launchLogin(url.toString())
|
return launchLogin(url.toString())
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: url.toString(),
|
url: url.toString(),
|
||||||
|
avatar: await getMetadataForUserId(user.sub),
|
||||||
allowedImageTypes: configuration.images.permitted_input,
|
allowedImageTypes: configuration.images.permitted_input,
|
||||||
renderSizes: configuration.images.output_resolutions
|
renderSizes: configuration.images.output_resolutions
|
||||||
}
|
}
|
||||||
|
@ -21,16 +23,33 @@ export const actions = {
|
||||||
if (!user)
|
if (!user)
|
||||||
return fail(401, {error: "unauthenticated"})
|
return fail(401, {error: "unauthenticated"})
|
||||||
|
|
||||||
let submission = await request.formData();
|
let submission = Object.fromEntries((await request.formData()).entries());
|
||||||
let newAvatar = undefined
|
let newAvatar = submission.newAvatar
|
||||||
if (submission.get("action") != "Clear") {
|
let timing: Awaited<ReturnType<typeof setNewAvatar>> = {}
|
||||||
newAvatar = submission.get("newAvatar")
|
let isUploadingNewFile = submission.action == "Clear"
|
||||||
if (newAvatar !== undefined && !(newAvatar instanceof File))
|
|
||||||
return fail(400, {success: false, error: "incorrect entry type"})
|
if (
|
||||||
|
!isUploadingNewFile // if action isn't already clear
|
||||||
|
&& newAvatar !== undefined // and avatar is defined
|
||||||
|
&& (newAvatar instanceof File && newAvatar.size > 0)
|
||||||
|
) {
|
||||||
if (!configuration.images.permitted_input.includes(newAvatar.type))
|
if (!configuration.images.permitted_input.includes(newAvatar.type))
|
||||||
return fail(400, {success: false, error: `allowed types does not include ${newAvatar.type}`})
|
return fail(400, {success: false, error: `allowed types does not include ${newAvatar.type}`})
|
||||||
|
isUploadingNewFile = true
|
||||||
}
|
}
|
||||||
let timing = await setNewAvatar(user.sub, newAvatar)
|
|
||||||
|
if (isUploadingNewFile)
|
||||||
|
timing = await setNewAvatar(user.sub, newAvatar as File|null || undefined)
|
||||||
|
if (await prisma.avatar.findFirst({ where: { userId: user.sub } }))
|
||||||
|
await prisma.avatar.update({
|
||||||
|
where: {
|
||||||
|
userId: user.sub
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
altText: typeof submission.altText == "string" ? submission.altText : null,
|
||||||
|
source: typeof submission.source == "string" ? submission.source : null,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
|
@ -2,37 +2,66 @@
|
||||||
import type { User } from "$lib/types";
|
import type { User } from "$lib/types";
|
||||||
import FilePreviewSet from "./FilePreviewSet.svelte";
|
import FilePreviewSet from "./FilePreviewSet.svelte";
|
||||||
|
|
||||||
export let data: {user: User, url: string, allowedImageTypes: string[], renderSizes: number[]};
|
export let data: {
|
||||||
|
user: User,
|
||||||
|
url: string,
|
||||||
|
avatar: {
|
||||||
|
altText: string,
|
||||||
|
source: string,
|
||||||
|
default: boolean
|
||||||
|
}
|
||||||
|
allowedImageTypes: string[],
|
||||||
|
renderSizes: number[]
|
||||||
|
};
|
||||||
export let form: { success: true, message: string } | { success: false, error: string } | undefined;
|
export let form: { success: true, message: string } | { success: false, error: string } | undefined;
|
||||||
let files: FileList;
|
let files: FileList;
|
||||||
let fileSrc = `/avatar/${data.user.identifier}/`
|
let fileSrc = `/avatar/${data.user.identifier}/`
|
||||||
|
|
||||||
$: if (files && files.length >= 0) {
|
$: if (files && files.length >= 0) {
|
||||||
console.log(files.length)
|
data.avatar.altText = "", data.avatar.source = "", data.avatar.default = false
|
||||||
fileSrc = URL.createObjectURL(files.item(0)!)
|
fileSrc = URL.createObjectURL(files.item(0)!)
|
||||||
} else fileSrc = `/avatar/${data.user.identifier}/`
|
} else fileSrc = `/avatar/${data.user.identifier}/`
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
form {
|
form {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
form, form > .buttons, form > .metadata {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
form > * {
|
form > .metadata {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
form > .metadata > textarea {
|
||||||
|
height: 3em;
|
||||||
|
flex-grow: 1;
|
||||||
|
min-width: 15em;
|
||||||
|
}
|
||||||
|
form > .buttons {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
form input {
|
||||||
font-family: "Inter Variable", "Inter", sans-serif;
|
font-family: "Inter Variable", "Inter", sans-serif;
|
||||||
}
|
}
|
||||||
form > input[type="file"] {
|
form > input[type="file"] {
|
||||||
width: 100%;
|
flex-basis: 100%;
|
||||||
|
min-height: 1em;
|
||||||
}
|
}
|
||||||
form > input[type="submit"], form > input[type="file"] {
|
form input[type="submit"], form input[type="file"] {
|
||||||
padding: 0.5em 1em;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
form input[type="submit"], form input[type="file"], form textarea {
|
||||||
|
padding: 0.5em 1em;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid var(--link);
|
border: 1px solid var(--link);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
background-color: var(--crust);
|
background-color: var(--crust);
|
||||||
}
|
}
|
||||||
|
form textarea:disabled {
|
||||||
|
color: var(--link);
|
||||||
|
}
|
||||||
form > input[type="file"]::file-selector-button {
|
form > input[type="file"]::file-selector-button {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -73,13 +102,19 @@
|
||||||
</p>
|
</p>
|
||||||
<form method="post" enctype="multipart/form-data">
|
<form method="post" enctype="multipart/form-data">
|
||||||
<input type="file" bind:files={files} accept={data.allowedImageTypes.join(",")} name="newAvatar">
|
<input type="file" bind:files={files} accept={data.allowedImageTypes.join(",")} name="newAvatar">
|
||||||
<input type="submit" name="action" value="Upload">
|
<div class="metadata">
|
||||||
<input type="submit" name="action" value="Clear">
|
<textarea name="altText" placeholder="Describe your image" disabled={data.avatar.default}>{data.avatar.altText}</textarea>
|
||||||
|
<textarea name="source" placeholder="Provide a source for your image" disabled={data.avatar.default}>{data.avatar.source}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="buttons">
|
||||||
|
<input type="submit" name="action" value="Save">
|
||||||
|
<input type="submit" name="action" value="Clear">
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{#if form}
|
{#if form}
|
||||||
{#if form.success}
|
{#if form.success}
|
||||||
<details>
|
<details>
|
||||||
<summary><small>Avatar set successfully</small></summary>
|
<summary><small>Avatar updated successfully</small></summary>
|
||||||
<div>
|
<div>
|
||||||
<pre>{form.message}</pre>
|
<pre>{form.message}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue