& so it begins
This commit is contained in:
247
apps/main/src/routes/(main)/account/+page.svelte
Normal file
247
apps/main/src/routes/(main)/account/+page.svelte
Normal file
@@ -0,0 +1,247 @@
|
||||
<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 Mail from "@lucide/svelte/icons/mail";
|
||||
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 email
|
||||
let profileData = $state({
|
||||
name: user.name ?? "",
|
||||
username: user.username ?? "",
|
||||
});
|
||||
|
||||
let emailData = $state({
|
||||
email: user.email,
|
||||
});
|
||||
|
||||
// Handle profile form submission (name, username)
|
||||
async function handleProfileSubmit(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
await accountVM.updateProfile(profileData);
|
||||
}
|
||||
|
||||
// Handle email form submission
|
||||
async function handleEmailSubmit(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
if (user.email !== emailData.email) {
|
||||
await accountVM.changeEmail(emailData.email);
|
||||
}
|
||||
}
|
||||
|
||||
// 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>
|
||||
|
||||
<!-- Email Settings -->
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Email Settings</Card.Title>
|
||||
<Card.Description>
|
||||
Manage your email address and verification status.
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form onsubmit={handleEmailSubmit} class="space-y-4">
|
||||
<!-- Email -->
|
||||
<div class="grid grid-cols-1 gap-1.5">
|
||||
<Label for="email" class="flex items-center gap-1.5">
|
||||
<Icon
|
||||
icon={Mail}
|
||||
cls="h-3.5 w-3.5 text-muted-foreground"
|
||||
/>
|
||||
Email Address
|
||||
</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
bind:value={emailData.email}
|
||||
placeholder="your.email@example.com"
|
||||
minlength={6}
|
||||
maxlength={128}
|
||||
/>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span
|
||||
class={user.emailVerified
|
||||
? "text-xs text-emerald-600 dark:text-emerald-400"
|
||||
: "text-xs text-amber-600"}
|
||||
>
|
||||
{user.emailVerified
|
||||
? "Email verified"
|
||||
: "Email not verified"}
|
||||
</span>
|
||||
{#if !user.emailVerified}
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
class="h-auto p-0 text-xs">Verify now</Button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={accountVM.emailLoading ||
|
||||
user.email === emailData.email}
|
||||
variant="outline"
|
||||
class="w-full sm:w-auto"
|
||||
>
|
||||
{#if accountVM.emailLoading}
|
||||
<Icon icon={Mail} cls="h-4 w-4 mr-2 animate-spin" />
|
||||
Sending...
|
||||
{:else}
|
||||
<Icon icon={Mail} cls="h-4 w-4 mr-2" />
|
||||
Update Email
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card.Content>
|
||||
<Card.Footer>
|
||||
<p class="text-muted-foreground text-xs">
|
||||
Changing your email will require verification of the new
|
||||
address.
|
||||
</p>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
</div>
|
||||
Reference in New Issue
Block a user