mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-19 20:53:28 +02:00
feat: Migrations, profile pictures, editing profile
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
import { fade } from 'svelte/transition';
|
||||
import { modalStack } from '../Modal/modal.store';
|
||||
|
||||
export let size: 'sm' | 'full' = 'sm';
|
||||
export let size: 'sm' | 'full' | 'lg' | 'dynamic' = 'sm';
|
||||
|
||||
function handleClose() {
|
||||
modalStack.closeTopmost();
|
||||
@@ -19,10 +19,12 @@
|
||||
>
|
||||
<div
|
||||
class={classNames(
|
||||
'flex-1 bg-primary-800 rounded-2xl p-10 relative shadow-xl flex flex-col',
|
||||
'bg-primary-800 rounded-2xl p-12 relative shadow-xl flex flex-col transition-[max-width]',
|
||||
{
|
||||
'max-w-lg min-h-0 overflow-y-auto scrollbar-hide': size === 'sm',
|
||||
'h-full overflow-hidden': size === 'full'
|
||||
'flex-1 max-w-lg min-h-0 overflow-y-auto scrollbar-hide': size === 'sm',
|
||||
'flex-1 h-full overflow-hidden': size === 'full',
|
||||
'flex-1 max-w-[56rem] min-h-0 overflow-y-auto scrollbar-hide': size === 'lg',
|
||||
'': size === 'dynamic'
|
||||
},
|
||||
$$restProps.class
|
||||
)}
|
||||
|
||||
213
src/lib/components/Dialog/EditProfileModal.svelte
Normal file
213
src/lib/components/Dialog/EditProfileModal.svelte
Normal file
@@ -0,0 +1,213 @@
|
||||
<script lang="ts">
|
||||
import Dialog from './Dialog.svelte';
|
||||
import { reiverrApi, type ReiverrUser } from '../../apis/reiverr/reiverr-api';
|
||||
import TextField from '../TextField.svelte';
|
||||
import Button from '../Button.svelte';
|
||||
import { ArrowUp, EyeClosed, EyeOpen, Upload } from 'radix-icons-svelte';
|
||||
import Container from '../../../Container.svelte';
|
||||
import IconToggle from '../IconToggle.svelte';
|
||||
import Tab from '../Tab/Tab.svelte';
|
||||
import { useTabs } from '../Tab/Tab';
|
||||
import SelectField from '../SelectField.svelte';
|
||||
import ProfileIcon from '../ProfileIcon.svelte';
|
||||
import { profilePictures } from '../../profile-pictures';
|
||||
import { modalStack } from '../Modal/modal.store';
|
||||
import { user as userStore } from '../../stores/user.store';
|
||||
|
||||
enum Tabs {
|
||||
EditProfile,
|
||||
ProfilePictures
|
||||
}
|
||||
|
||||
export let user: ReiverrUser;
|
||||
|
||||
const tab = useTabs(Tabs.EditProfile);
|
||||
|
||||
let name = user?.name || '';
|
||||
let oldPassword = '';
|
||||
let oldPasswordVisible = false;
|
||||
let newPassword = '';
|
||||
let newPasswordVisible = false;
|
||||
let profilePictureFiles: FileList;
|
||||
let profilePictureBase64: string = user.profilePicture;
|
||||
let profilePictureTitle: string;
|
||||
let profilePictureFilesInput: HTMLInputElement;
|
||||
$: {
|
||||
const file = profilePictureFiles?.[0];
|
||||
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => setProfilePicture(reader.result as string);
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
switch (profilePictureBase64) {
|
||||
case profilePictures.ana:
|
||||
profilePictureTitle = 'Ana';
|
||||
break;
|
||||
case profilePictures.emma:
|
||||
profilePictureTitle = 'Emma';
|
||||
break;
|
||||
case profilePictures.glen:
|
||||
profilePictureTitle = 'Glen';
|
||||
break;
|
||||
case profilePictures.henry:
|
||||
profilePictureTitle = 'Henry';
|
||||
break;
|
||||
case profilePictures.keanu:
|
||||
profilePictureTitle = 'Keanu';
|
||||
break;
|
||||
case profilePictures.leo:
|
||||
profilePictureTitle = 'Leo';
|
||||
break;
|
||||
case profilePictures.sydney:
|
||||
profilePictureTitle = 'Sydney';
|
||||
break;
|
||||
case profilePictures.zendaya:
|
||||
profilePictureTitle = 'Zendaya';
|
||||
break;
|
||||
default:
|
||||
profilePictureTitle = 'Custom';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$: stale =
|
||||
(name !== user.name && name !== '') ||
|
||||
oldPassword !== newPassword ||
|
||||
profilePictureBase64 !== user.profilePicture;
|
||||
let errorMessage = '';
|
||||
|
||||
function setProfilePicture(image: string) {
|
||||
profilePictureBase64 = image;
|
||||
tab.set(Tabs.EditProfile);
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const error = await userStore.updateUser((u) => ({
|
||||
...u,
|
||||
name,
|
||||
password: newPassword,
|
||||
oldPassword,
|
||||
profilePicture: profilePictureBase64
|
||||
// password: newPassword
|
||||
}));
|
||||
|
||||
if (error) {
|
||||
errorMessage = error;
|
||||
} else {
|
||||
modalStack.closeTopmost();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog class="grid" size={$tab === Tabs.EditProfile ? 'sm' : 'dynamic'}>
|
||||
<Tab {...tab} tab={Tabs.EditProfile} class="space-y-4">
|
||||
<h1 class="header2">Edit Profile</h1>
|
||||
<TextField bind:value={name}>name</TextField>
|
||||
<SelectField value={profilePictureTitle} on:clickOrSelect={() => tab.set(Tabs.ProfilePictures)}>
|
||||
Profile Picture
|
||||
</SelectField>
|
||||
<Container direction="horizontal" class="flex space-x-4 items-end">
|
||||
<TextField
|
||||
class="flex-1"
|
||||
bind:value={oldPassword}
|
||||
type={oldPasswordVisible ? 'text' : 'password'}
|
||||
>
|
||||
Old Password
|
||||
</TextField>
|
||||
<IconToggle
|
||||
on:clickOrSelect={() => (oldPasswordVisible = !oldPasswordVisible)}
|
||||
icon={oldPasswordVisible ? EyeOpen : EyeClosed}
|
||||
/>
|
||||
</Container>
|
||||
<Container direction="horizontal" class="flex space-x-4 items-end">
|
||||
<TextField
|
||||
class="flex-1"
|
||||
bind:value={newPassword}
|
||||
type={newPasswordVisible ? 'text' : 'password'}
|
||||
>
|
||||
New Password
|
||||
</TextField>
|
||||
<IconToggle
|
||||
on:clickOrSelect={() => (newPasswordVisible = !newPasswordVisible)}
|
||||
icon={newPasswordVisible ? EyeOpen : EyeClosed}
|
||||
/>
|
||||
</Container>
|
||||
{#if errorMessage}
|
||||
<div class="text-red-500 mb-4">{errorMessage}</div>
|
||||
{/if}
|
||||
<Button type="primary-dark" disabled={!stale} action={save} class="mt-8">Save</Button>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
{...tab}
|
||||
tab={Tabs.ProfilePictures}
|
||||
on:back={({ detail }) => {
|
||||
tab.set(Tabs.EditProfile);
|
||||
detail.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<h1 class="header2 mb-6">Select Profile Picture</h1>
|
||||
<Container direction="grid" gridCols={3} class="grid grid-cols-3 gap-4">
|
||||
<ProfileIcon
|
||||
url={profilePictures.ana}
|
||||
on:clickOrSelect={() => setProfilePicture(profilePictures.ana)}
|
||||
focusOnMount={profilePictureBase64 === profilePictures.ana}
|
||||
/>
|
||||
<ProfileIcon
|
||||
url={profilePictures.emma}
|
||||
on:clickOrSelect={() => setProfilePicture(profilePictures.emma)}
|
||||
focusOnMount={profilePictureBase64 === profilePictures.emma}
|
||||
/>
|
||||
<ProfileIcon
|
||||
url={profilePictures.glen}
|
||||
on:clickOrSelect={() => setProfilePicture(profilePictures.glen)}
|
||||
focusOnMount={profilePictureBase64 === profilePictures.glen}
|
||||
/>
|
||||
<ProfileIcon
|
||||
url={profilePictures.henry}
|
||||
on:clickOrSelect={() => setProfilePicture(profilePictures.henry)}
|
||||
focusOnMount={profilePictureBase64 === profilePictures.henry}
|
||||
/>
|
||||
<ProfileIcon
|
||||
url={profilePictures.keanu}
|
||||
on:clickOrSelect={() => setProfilePicture(profilePictures.keanu)}
|
||||
focusOnMount={profilePictureBase64 === profilePictures.keanu}
|
||||
/>
|
||||
<ProfileIcon
|
||||
url={profilePictures.leo}
|
||||
on:clickOrSelect={() => setProfilePicture(profilePictures.leo)}
|
||||
focusOnMount={profilePictureBase64 === profilePictures.leo}
|
||||
/>
|
||||
<ProfileIcon
|
||||
url={profilePictures.sydney}
|
||||
on:clickOrSelect={() => setProfilePicture(profilePictures.sydney)}
|
||||
focusOnMount={profilePictureBase64 === profilePictures.sydney}
|
||||
/>
|
||||
<ProfileIcon
|
||||
url={profilePictures.zendaya}
|
||||
on:clickOrSelect={() => setProfilePicture(profilePictures.zendaya)}
|
||||
focusOnMount={profilePictureBase64 === profilePictures.zendaya}
|
||||
/>
|
||||
<ProfileIcon
|
||||
url="profile-pictures/leo.webp"
|
||||
on:clickOrSelect={() => profilePictureFilesInput?.click()}
|
||||
icon={Upload}
|
||||
/>
|
||||
<input
|
||||
bind:this={profilePictureFilesInput}
|
||||
type="file"
|
||||
bind:files={profilePictureFiles}
|
||||
accept="image/png, image/jpeg"
|
||||
class="hidden"
|
||||
/>
|
||||
<!-- <Container>-->
|
||||
<!-- Select File-->
|
||||
<!-- <input type="file" bind:files={profilePictureFiles} accept="image/png, image/jpeg" />-->
|
||||
<!-- </Container>-->
|
||||
</Container>
|
||||
</Tab>
|
||||
</Dialog>
|
||||
Reference in New Issue
Block a user