Skip to content

Commit

Permalink
chore: finalize profile picture design and implement form actions
Browse files Browse the repository at this point in the history
- start changing avatar to profile picture as users could be confused
- moved heading from UserForm to actual page
  • Loading branch information
V-ed committed Aug 26, 2023
1 parent 76e0972 commit babcb59
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 79 deletions.
92 changes: 57 additions & 35 deletions client/src/routes/users/[email]/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import {
EditOtherUserInfoStore,
EditOtherUserInfoWithAvatarStore,
GetEditableUserStore,
type EditOtherUserInfo$input,
type EditOtherUserInfoWithAvatar$input,
} from '$houdini';
import { redirect } from '@sveltejs/kit';
import { createToasts } from '$/lib/components/ToastManager/helper';
import { DeleteUserProfilePictureStore, EditOtherUserInfoStore, EditUserProfilePictureStore, GetEditableUserStore } from '$houdini';
import { fail, redirect } from '@sveltejs/kit';
import { StatusCodes } from 'http-status-codes';
import { setError, superValidate } from 'sveltekit-superforms/server';
import { superValidate } from 'sveltekit-superforms/server';
import { userFormSchema } from '../components/userform.schema';
import type { Actions, PageServerLoad } from './$types';

Expand Down Expand Up @@ -45,51 +40,78 @@ export const load = (async ({
}) satisfies PageServerLoad;

export const actions = {
default: async ({
user: async ({
request,
locals: {
gql: { mutate },
},
params: { email: editableUserEmail },
}) => {
const formData = await request.formData();
const form = await superValidate(formData, userFormSchema);
const form = await superValidate(request, userFormSchema);

if (!form.valid) return { form };

const { email, firstName, lastName, roles } = form.data;

const result = await mutate(EditOtherUserInfoStore, {
oldEmail: editableUserEmail,
email,
firstName,
lastName,
roles: roles.map((role) => ({
text: role,
})),
});

if (result.type === 'failure') {
return result.kitHandler('error');
}

throw redirect(StatusCodes.SEE_OTHER, '/users');
},
profilePicture: async ({
request,
locals: {
gql: { mutate },
sessionUser,
},
}) => {
const formData = await request.formData();

const avatarFile = formData.get('avatar');

const [EditUserStore, variables] = (():
| [typeof EditOtherUserInfoStore, EditOtherUserInfo$input]
| [typeof EditOtherUserInfoWithAvatarStore, EditOtherUserInfoWithAvatar$input] => {
const variables = {
oldEmail: editableUserEmail,
email,
firstName,
lastName,
roles: roles.map((role) => ({
text: role,
})),
};
if (!(avatarFile instanceof File)) {
const toasts = createToasts([
{
text: 'Missing avatar file!',
},
]);

if (avatarFile instanceof File && avatarFile.size > 0) {
(variables as EditOtherUserInfoWithAvatar$input).avatar = avatarFile;
return fail(StatusCodes.BAD_REQUEST, { toasts });
}

return [EditOtherUserInfoWithAvatarStore, variables];
}
const result = await mutate(EditUserProfilePictureStore, {
profilePicture: avatarFile,
});

return [EditOtherUserInfoStore, variables];
})();
if (result.type === 'failure') {
return result.kitHandler('error');
}

const result = await mutate(EditUserStore, variables);
throw redirect(StatusCodes.SEE_OTHER, `/users/${sessionUser!.email}`);
},
deleteProfilePicture: async ({
locals: {
gql: { mutate },
sessionUser,
},
}) => {
const result = await mutate(DeleteUserProfilePictureStore, null);

if (result.type === 'failure') {
return result.kitHandler('custom', ({ errors }) => {
return setError(form, 'avatarFile', errors?.at(0)?.message ?? 'Unknown error');
});
return result.kitHandler('error');
}

throw redirect(StatusCodes.SEE_OTHER, '/users');
throw redirect(StatusCodes.SEE_OTHER, `/users/${sessionUser!.email}`);
},
} satisfies Actions;
67 changes: 48 additions & 19 deletions client/src/routes/users/[email]/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
<script lang="ts">
import Icon from '$/lib/components/Icon.svelte';
import VDropzone from '$/lib/components/flowbite-custom/VDropzone.svelte';
import { getAvatarImageUrl } from '$/lib/utils/images';
import { browser } from '$app/environment';
import { enhance } from '$app/forms';
import { mdiUpload } from '@mdi/js';
import { Helper, Label } from 'flowbite-svelte';
import { Button, Heading, Helper, Hr, Label } from 'flowbite-svelte';
import { superForm } from 'sveltekit-superforms/client';
import UserForm from '../components/UserForm.svelte';
import { ACCEPTED_AVATAR_TYPES } from '../components/userform.schema';
export let data;
const superFormData = superForm(data.form, { dataType: 'json' });
$: currentAvatarRef = data.sessionUser?.avatarRef;
$: superFormData = superForm(data.form, { dataType: 'json' });
$: ({ errors } = superFormData);
let inputRef: HTMLInputElement;
Expand Down Expand Up @@ -51,8 +55,11 @@
};
</script>

<UserForm headerText="Editing user {data.editableUser?.email}" {superFormData} roles={data.roles}>
<div slot="below">
<Heading tag="h2" class="overflow-x-clip text-ellipsis">Editing user {data.editableUser?.email}</Heading>

<div class="grid grid-cols-1 sm:grid-cols-3 gap-y-5 sm:gap-10">
<form method="post" enctype="multipart/form-data" use:enhance class="col-span-1 order-2 sm:order-none">
<Hr classHr="sm:hidden" />
<Label>Profile Picture</Label>
<VDropzone
id="dropzone"
Expand All @@ -64,25 +71,47 @@
on:change={handlePictureChange}
name="avatar"
class="mt-2"
accept={ACCEPTED_AVATAR_TYPES.map((type) => `image/${type}`).join(' ')}
accept={ACCEPTED_AVATAR_TYPES.map((type) => `image/${type}`).join(', ')}
bind:input={inputRef}
let:isDraggingOver
>
{#if !avatarFile}
<Icon path={mdiUpload}></Icon>
<p class="mb-2 text-sm text-gray-500 dark:text-gray-400"><span class="font-semibold">Click to upload</span> or drag and drop</p>
<p class="text-xs text-gray-500 dark:text-gray-400">
Accepted formats: {new Intl.ListFormat('en', { style: 'long', type: 'conjunction' }).format(ACCEPTED_AVATAR_TYPES)}.
</p>
{#if isDraggingOver}
<p>Yes, right here!</p>
{/if}
{:else}
<img src={URL.createObjectURL(avatarFile)} alt="user profile" class="max-h-64" />
{/if}
<div class="grid grid-cols-2 p-5 gap-3">
<div class="flex flex-col justify-center items-center">
<Icon path={mdiUpload}></Icon>
<p class="mb-2 text-sm text-gray-500 dark:text-gray-400"><span class="font-semibold">Click to upload</span> or drag and drop</p>
<p class="text-xs text-gray-500 dark:text-gray-400">
Accepted formats: {new Intl.ListFormat('en', { style: 'long', type: 'conjunction' }).format(ACCEPTED_AVATAR_TYPES)}.
</p>
{#if isDraggingOver}
<p>Yes, right here!</p>
{/if}
</div>

<div class="flex justify-center items-center">
<img
src={avatarFile ? URL.createObjectURL(avatarFile) : getAvatarImageUrl(currentAvatarRef)}
alt="user profile"
class="max-h-36"
/>
</div>
</div>
<!-- <Icon path={mdiUpload}></Icon>
<p class="mb-2 text-sm text-gray-500 dark:text-gray-400"><span class="font-semibold">Click to upload</span> or drag and drop</p>
<p class="text-xs text-gray-500 dark:text-gray-400">
Accepted formats: {new Intl.ListFormat('en', { style: 'long', type: 'conjunction' }).format(ACCEPTED_AVATAR_TYPES)}.
</p>
{#if isDraggingOver}
<p>Yes, right here!</p>
{/if} -->
</VDropzone>
{#if $errors.avatarFile}
<Helper class="mt-2" color="red">{$errors.avatarFile}</Helper>
{/if}
</div>
</UserForm>

<div class="mt-5 grid grid-cols-2 gap-5">
<Button type="submit" formaction="?/deleteProfilePicture" disabled={!currentAvatarRef} color="red">Delete</Button>
<Button type="submit" formaction="?/profilePicture" disabled={!(!browser || avatarFile)}>Submit Profile Picture</Button>
</div>
</form>
<UserForm action="?/user" {superFormData} roles={data.roles} class="col-span-2 order-1 sm:order-none" />
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mutation DeleteUserProfilePicture {
deleteAvatar
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mutation EditUserProfilePicture($profilePicture: Upload!) {
uploadAvatar(avatar: $profilePicture) {
fileName
}
}
7 changes: 2 additions & 5 deletions client/src/routes/users/components/UserForm.svelte
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
<script lang="ts">
import type { Role } from '$/graphql/@generated';
import { Button, Heading, Helper, Input, Label, MultiSelect } from 'flowbite-svelte';
import { Button, Helper, Input, Label, MultiSelect } from 'flowbite-svelte';
import type { SelectOptionType } from 'flowbite-svelte/dist/types';
import type { SuperForm } from 'sveltekit-superforms/client';
import type { userFormSchema } from './userform.schema';
export let headerText: string;
export let superFormData: SuperForm<typeof userFormSchema>;
export let roles: Pick<Role, 'id' | 'text'>[];
Expand All @@ -17,9 +16,7 @@
}));
</script>

<Heading tag="h2">{headerText}</Heading>

<form method="post" enctype="multipart/form-data" use:enhance>
<form method="post" use:enhance {...$$restProps}>
<div class="grid gap-3">
<slot name="above" />

Expand Down
5 changes: 4 additions & 1 deletion client/src/routes/users/new/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script lang="ts">
import { Heading } from 'flowbite-svelte';
import { superForm } from 'sveltekit-superforms/client';
import UserForm from '../components/UserForm.svelte';
import type { PageData } from './$houdini';
Expand All @@ -10,4 +11,6 @@
const superFormData = superForm(data.form, { dataType: 'json' });
</script>

<UserForm headerText="Creating New User" {superFormData} roles={$GetRolesForNewUser.data?.getRoles ?? []} />
<Heading tag="h2">Creating New User</Heading>

<UserForm {superFormData} roles={$GetRolesForNewUser.data?.getRoles ?? []} />

0 comments on commit babcb59

Please sign in to comment.