Files
illusory-mapp/apps/main/src/routes/(main)/account/+page.svelte
2026-02-28 19:39:21 +02:00

262 lines
10 KiB
Svelte

<script lang="ts">
import Icon from "$lib/components/atoms/icon.svelte";
import * as Avatar from "$lib/components/ui/avatar";
import { Button } from "$lib/components/ui/button";
import * as Card from "$lib/components/ui/card";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { Separator } from "$lib/components/ui/separator";
import { secondaryNavTree } from "$lib/core/constants";
import { accountVM } from "$lib/domains/account/account.vm.svelte";
import { breadcrumbs } from "$lib/global.stores";
import AtSign from "@lucide/svelte/icons/at-sign";
import KeyRound from "@lucide/svelte/icons/key-round";
import Save from "@lucide/svelte/icons/save";
import Upload from "@lucide/svelte/icons/upload";
import User from "@lucide/svelte/icons/user";
import type { PageData } from "./$types";
breadcrumbs.set([secondaryNavTree[0]]);
let { data }: { data: PageData } = $props();
const user = $state(data.user!);
// Separate form state for profile and password
let profileData = $state({
name: user.name ?? "",
username: user.username ?? "",
});
let passwordData = $state({
password: "",
confirmPassword: "",
});
// Handle profile form submission (name, username)
async function handleProfileSubmit(e: SubmitEvent) {
e.preventDefault();
await accountVM.updateProfile(profileData);
}
// Handle password form submission
async function handlePasswordSubmit(e: SubmitEvent) {
e.preventDefault();
if (
passwordData.password.length >= 6 &&
passwordData.password === passwordData.confirmPassword
) {
const didChange = await accountVM.changePassword(
passwordData.password,
);
if (didChange) {
passwordData.password = "";
passwordData.confirmPassword = "";
}
}
}
// Handle image upload - would connect to your storage service
function handleImageUpload() {
// In a real implementation, this would trigger a file picker
console.log("Image upload triggered");
}
</script>
<div class="space-y-8">
<!-- Profile Information -->
<Card.Root>
<Card.Header>
<div class="text-center">
<div class="flex flex-col items-center justify-center gap-4">
<div class="relative">
<Avatar.Root class="border-muted h-24 w-24 border-2">
{#if user.image}
<Avatar.Image
src={user.image}
alt={user.name || "User"}
/>
{:else}
<Avatar.Fallback class="bg-primary/10 text-xl">
{(user.name || "User")
.substring(0, 2)
.toUpperCase()}
</Avatar.Fallback>
{/if}
</Avatar.Root>
<button
class="bg-primary text-primary-foreground hover:bg-primary/90 focus:ring-ring absolute right-0 bottom-0 rounded-full p-1.5 shadow focus:ring-2 focus:ring-offset-2 focus:outline-none"
onclick={handleImageUpload}
aria-label="Upload new profile image"
type="button"
>
<Icon icon={Upload} cls="h-3 w-3" />
</button>
</div>
<div>
<h1 class="text-foreground text-xl font-semibold">
{user.name}
</h1>
<p class="text-muted-foreground text-sm">
Member since {new Date(
user.createdAt.toString(),
).toLocaleDateString()}
</p>
</div>
</div>
</div>
<Separator class="mt-4 mb-8" />
<Card.Title>Personal Information</Card.Title>
<Card.Description>
Update your personal information and how others see you on the
platform.
</Card.Description>
</Card.Header>
<Card.Content class="space-y-6">
<!-- Profile Form (Name, Username) -->
<form onsubmit={handleProfileSubmit} class="space-y-6">
<!-- Full Name -->
<div class="grid grid-cols-1 gap-1.5">
<Label for="name" class="flex items-center gap-1.5">
<Icon
icon={User}
cls="h-3.5 w-3.5 text-muted-foreground"
/>
Full Name
</Label>
<Input
id="name"
bind:value={profileData.name}
placeholder="Your name"
minlength={3}
/>
</div>
<!-- Username -->
<div class="grid grid-cols-1 gap-1.5">
<Label for="username" class="flex items-center gap-1.5">
<Icon
icon={AtSign}
cls="h-3.5 w-3.5 text-muted-foreground"
/>
Username
</Label>
<Input
id="username"
bind:value={profileData.username}
placeholder="username"
minlength={6}
maxlength={32}
/>
<p class="text-muted-foreground text-xs">
This is your public username visible to other users.
</p>
</div>
<div class="flex justify-end">
<Button
type="submit"
disabled={accountVM.loading}
class="w-full sm:w-auto"
>
{#if accountVM.loading}
<Icon icon={Save} cls="h-4 w-4 mr-2 animate-spin" />
Saving...
{:else}
<Icon icon={Save} cls="h-4 w-4 mr-2" />
Save Profile
{/if}
</Button>
</div>
</form>
</Card.Content>
<Card.Footer>
<p class="text-muted-foreground text-xs">
Last updated: {new Date(
user.updatedAt.toString(),
).toLocaleString()}
</p>
</Card.Footer>
</Card.Root>
<!-- Password Settings -->
<Card.Root>
<Card.Header>
<Card.Title>Password Settings</Card.Title>
<Card.Description>
Update your account password.
</Card.Description>
</Card.Header>
<Card.Content>
<form onsubmit={handlePasswordSubmit} class="space-y-4">
<div class="grid grid-cols-1 gap-1.5">
<Label for="password" class="flex items-center gap-1.5">
<Icon
icon={KeyRound}
cls="h-3.5 w-3.5 text-muted-foreground"
/>
New Password
</Label>
<Input
id="password"
type="password"
bind:value={passwordData.password}
placeholder="Enter new password"
minlength={6}
/>
</div>
<div class="grid grid-cols-1 gap-1.5">
<Label
for="confirm-password"
class="flex items-center gap-1.5"
>
<Icon
icon={KeyRound}
cls="h-3.5 w-3.5 text-muted-foreground"
/>
Confirm Password
</Label>
<Input
id="confirm-password"
type="password"
bind:value={passwordData.confirmPassword}
placeholder="Re-enter new password"
minlength={6}
/>
</div>
<div class="flex justify-end">
<Button
type="submit"
disabled={accountVM.passwordLoading ||
passwordData.password.length < 6 ||
passwordData.password !==
passwordData.confirmPassword}
variant="outline"
class="w-full sm:w-auto"
>
{#if accountVM.passwordLoading}
<Icon
icon={KeyRound}
cls="h-4 w-4 mr-2 animate-spin"
/>
Updating...
{:else}
<Icon icon={KeyRound} cls="h-4 w-4 mr-2" />
Update Password
{/if}
</Button>
</div>
</form>
</Card.Content>
<Card.Footer>
<p class="text-muted-foreground text-xs">
Choose a strong password with at least 6 characters.
</p>
</Card.Footer>
</Card.Root>
</div>