From: Muhammad Rizki <[email protected]>
To: Ammar Faizi <[email protected]>
Cc: Muhammad Rizki <[email protected]>,
Alviro Iskandar Setiawan <[email protected]>,
GNU/Weeb Mailing List <[email protected]>
Subject: [PATCH v1 13/17] refactor!:feat: update API response structure, update profile page
Date: Wed, 5 Mar 2025 21:40:12 +0700 [thread overview]
Message-ID: <[email protected]> (raw)
In-Reply-To: <[email protected]>
- Update API response structure to match the new API response structure.
- Update profile page to follow the user profile schema and its types.
- Update login page to use the new API response structure.
- Update the profile page to use the new API response structure.
Signed-off-by: Muhammad Rizki <[email protected]>
---
src/lib/hooks/auth.svelte.ts | 12 +-
src/lib/schemas/profile-schema.ts | 20 +-
src/lib/typings/credential.d.ts | 11 +-
src/lib/typings/http.d.ts | 11 +-
src/routes/(protected)/+layout.ts | 7 +-
.../(protected)/settings/profile/+page.svelte | 577 +++++++++++++-----
.../(protected)/settings/profile/+page.ts | 7 +-
src/routes/+page.svelte | 17 +-
8 files changed, 498 insertions(+), 164 deletions(-)
diff --git a/src/lib/hooks/auth.svelte.ts b/src/lib/hooks/auth.svelte.ts
index ba357f7..7bd2d3b 100644
--- a/src/lib/hooks/auth.svelte.ts
+++ b/src/lib/hooks/auth.svelte.ts
@@ -19,7 +19,7 @@ export function useAuth() {
return data.user_info;
},
- set token(newValue: string) {
+ set token(newValue) {
data.token = newValue;
},
@@ -28,11 +28,11 @@ export function useAuth() {
data.user_info = JSON.parse(user!) as User;
},
- save(newData: LoginResponse) {
- data = newData;
- localStorage.setItem("gwm_token", newData.token);
- localStorage.setItem("gwm_token_exp_at", newData.token_exp_at.toString());
- localStorage.setItem("gwm_uinfo", JSON.stringify(newData.user_info));
+ save({ user_info, token, token_exp_at }: LoginResponse) {
+ data = { user_info, token, token_exp_at };
+ localStorage.setItem("gwm_token", token || "null");
+ localStorage.setItem("gwm_token_exp_at", token_exp_at!.toString() ?? "0");
+ localStorage.setItem("gwm_uinfo", JSON.stringify(user_info));
},
clear() {
diff --git a/src/lib/schemas/profile-schema.ts b/src/lib/schemas/profile-schema.ts
index fa67c17..c4c7228 100644
--- a/src/lib/schemas/profile-schema.ts
+++ b/src/lib/schemas/profile-schema.ts
@@ -1,8 +1,22 @@
import { z } from "zod";
+const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
+
+const photoSchema = z.instanceof(File).refine((file) => file.size <= MAX_FILE_SIZE, {
+ message: "File size must be less than 10MB"
+});
+
export const profileSchema = z.object({
- avatar: z.instanceof(File).optional(),
+ photo: photoSchema.nullable(),
username: z.string().optional(),
- full_name: z.string().optional(),
- gender: z.string().optional()
+ full_name: z.string().min(1, "Full name is required"),
+ ext_email: z.string().email("Invalid email format"),
+ gender: z.enum(["m", "f"]),
+ socials: z.object({
+ github_username: z.string().optional(),
+ telegram_username: z.string().optional(),
+ twitter_username: z.string().optional(),
+ discord_username: z.string().optional()
+ }),
+ password: z.string()
});
diff --git a/src/lib/typings/credential.d.ts b/src/lib/typings/credential.d.ts
index df02c86..beabe5e 100644
--- a/src/lib/typings/credential.d.ts
+++ b/src/lib/typings/credential.d.ts
@@ -1,7 +1,16 @@
export interface User {
user_id: number;
full_name: string;
- gender: string;
+ gender: Gender;
username: string;
+ ext_email: string;
role: string;
+ is_active: IsActive | boolean;
+ socials: {
+ github_username: string;
+ telegram_username: string;
+ twitter_username: string;
+ discord_username: string;
+ };
+ photo: string;
}
diff --git a/src/lib/typings/http.d.ts b/src/lib/typings/http.d.ts
index 98d64f5..1d390a3 100644
--- a/src/lib/typings/http.d.ts
+++ b/src/lib/typings/http.d.ts
@@ -2,7 +2,10 @@ import type { User } from "./credential";
export interface ResponseAPI<Data> {
code: number;
- res?: Data & { renew_token?: RenewTokenResponse };
+ res?: Data & {
+ msg: string;
+ renew_token?: RenewTokenResponse;
+ };
}
export interface RenewTokenResponse {
@@ -11,7 +14,7 @@ export interface RenewTokenResponse {
}
export interface LoginResponse {
- token: string;
- token_exp_at: number;
- user_info?: User | null;
+ token?: string;
+ token_exp_at?: number;
+ user_info?: User;
}
diff --git a/src/routes/(protected)/+layout.ts b/src/routes/(protected)/+layout.ts
index 91dbfc7..06ff2db 100644
--- a/src/routes/(protected)/+layout.ts
+++ b/src/routes/(protected)/+layout.ts
@@ -16,6 +16,9 @@ export const load: LayoutLoad = async () => {
params: { action: "get_user_info" }
});
- localStorage.setItem("gwm_uinfo", JSON.stringify(data.res?.user_info));
- auth.refresh();
+ auth.save({
+ token: data.res?.renew_token?.token,
+ token_exp_at: data.res?.renew_token?.token_exp_at,
+ user_info: data.res?.user_info
+ });
};
diff --git a/src/routes/(protected)/settings/profile/+page.svelte b/src/routes/(protected)/settings/profile/+page.svelte
index 73edb8a..b91382a 100644
--- a/src/routes/(protected)/settings/profile/+page.svelte
+++ b/src/routes/(protected)/settings/profile/+page.svelte
@@ -1,170 +1,469 @@
<script lang="ts">
import { useAuth } from "$lib/hooks/auth.svelte";
- import { superForm } from "sveltekit-superforms";
+ import { setError, setMessage, superForm } from "sveltekit-superforms";
import { zodClient } from "sveltekit-superforms/adapters";
import { profileSchema } from "$lib/schemas/profile-schema";
import Pencil from "lucide-svelte/icons/pencil";
import * as Avatar from "$lib/components/ui/avatar";
import * as Form from "$lib/components/ui/form";
import * as RadioGroup from "$lib/components/ui/radio-group";
+ import * as Popover from "$lib/components/ui/popover";
+ import * as Dialog from "$lib/components/ui/dialog";
import Input from "$components/ui/input/input.svelte";
import Label from "$components/ui/label/label.svelte";
+ import IconGithub from "$components/icons/icon-github.svelte";
+ import IconTelegram from "$components/icons/icon-telegram.svelte";
+ import IconTwitter from "$components/icons/icon-twitter.svelte";
+ import IconDiscord from "$components/icons/icon-discord.svelte";
+ import Loading from "$components/customs/loading.svelte";
+ import Button from "$components/ui/button/button.svelte";
+ import http from "$lib/hooks/http.svelte.js";
+ import * as typing from "$typings";
+ import { toast } from "svelte-sonner";
+ import InputPassword from "$components/ui/input/input-password.svelte";
let { data } = $props();
-
- const auth = useAuth();
-
- let avatarImage = $state<string>();
+ let showModalConfirmation = $state(false);
const form = superForm(data.form, {
SPA: true,
- validators: zodClient(profileSchema)
+ resetForm: false,
+ validators: zodClient(profileSchema),
+ dataType: "json",
+
+ async onUpdate({ form }) {
+ const formData = new FormData();
+ if (form.data.photo) {
+ formData.append("photo", form.data.photo);
+ }
+ formData.append("full_name", form.data.full_name);
+ formData.append("ext_email", form.data.ext_email);
+ formData.append("gender", form.data.gender);
+ formData.append("password", form.data.password);
+ if (form.data.socials.github_username) {
+ formData.append("socials[github_username]", form.data.socials.github_username);
+ }
+ if (form.data.socials.telegram_username) {
+ formData.append("socials[telegram_username]", form.data.socials.telegram_username);
+ }
+ if (form.data.socials.twitter_username) {
+ formData.append("socials[twitter_username]", form.data.socials.twitter_username);
+ }
+ if (form.data.socials.discord_username) {
+ formData.append("socials[discord_username]", form.data.socials.discord_username);
+ }
+
+ const {
+ data: { res },
+ status
+ } = await http<typing.ResponseAPI<{}>>({
+ params: { action: "set_user_info" },
+ method: "POST",
+ data: formData
+ });
+
+ if (status === 200) {
+ const { data } = await http<{ user_info: typing.User }>({
+ params: { action: "get_user_info" }
+ });
+
+ auth.save({
+ token: data.res?.renew_token?.token,
+ token_exp_at: data.res?.renew_token?.token_exp_at,
+ user_info: data.res?.user_info
+ });
+
+ toast.info("Success update profile", {
+ description: data.res?.msg ?? "Invalid credential, please login again."
+ });
+ } else {
+ if (res?.msg.includes("full_name")) {
+ setError(form, "full_name", res?.msg);
+ }
+
+ if (res?.msg.includes("ext_email")) {
+ setError(form, "ext_email", res?.msg);
+ }
+
+ if (res?.msg.includes("gender")) {
+ setError(form, "gender", res?.msg);
+ }
+
+ if (res?.msg.includes("password")) {
+ setError(form, "password", res?.msg);
+ }
+
+ toast.error("Failed to update profile", {
+ description: res?.msg ?? "Invalid credential, please login again."
+ });
+ }
+ }
});
+ const { form: formData, errors, submitting, constraints, enhance, submit } = form;
+
+ const auth = useAuth();
+
+ let avatarImg = $state(data.avatar);
+ const avatar = $derived(avatarImg);
+
const getShortName = () => {
const fullName = auth.user?.full_name ?? "";
const match = fullName.match(/\b(\w)/g) ?? [];
return match.slice(0, 2).join("");
};
- const handleAvatar = (e: any) => {
- const file = e.srcElement.files[0];
+ const handleOpenModal = (e: boolean) => {
+ showModalConfirmation = e;
+ };
+
+ const handleAvatar = (event: Event) => {
+ const input = event.target as HTMLInputElement;
+ const file = input.files?.[0];
+ if (!file) return;
+
+ $formData.photo = file;
+
const reader = new FileReader();
+ reader.onload = () => {
+ avatarImg = reader.result as string;
+ };
reader.readAsDataURL(file);
+ };
- reader.onload = function () {
- avatarImage = reader.result as string;
- };
+ const handleSubmit = () => {
+ submit();
+ handleOpenModal(false);
};
- const { form: formData, errors, submitting, constraints, enhance } = form;
+ const isSubmittable = $derived(
+ Boolean($formData.full_name && $formData.ext_email && $formData.gender)
+ );
+
+ const isError = $derived(Boolean($errors.full_name || $errors.ext_email || $errors.gender));
</script>
-<form
- use:enhance
- class="flex w-full max-w-5xl flex-col gap-y-8 lg:flex-row lg:justify-between lg:gap-x-8 lg:gap-y-0"
->
- <div class="flex w-full justify-center lg:hidden">
- <Form.Field {form} name="avatar" class="relative">
- <Form.Control>
- {#snippet children({ props })}
- <Form.Label for="avatar" class="cursor-pointer">
- <Avatar.Root class="size-40">
- <Avatar.Image src={avatarImage} alt="@{auth.user?.username}" />
- <Avatar.Fallback class="text-xl">{getShortName()}</Avatar.Fallback>
- </Avatar.Root>
- <div
- class="absolute bottom-3 left-0 flex items-center gap-x-1 rounded-lg bg-foreground px-2 py-1 text-primary-foreground"
- >
- <Pencil class="size-4" />
- <span class="text-xs font-medium">Edit</span>
- </div>
- </Form.Label>
- <Input
- type="file"
- accept="image/png,image/jpeg"
- {...props}
- aria-invalid={$errors.avatar ? "true" : undefined}
- bind:value={$formData.avatar}
- disabled={$submitting}
- {...$constraints.avatar}
- class="hidden"
- onchange={handleAvatar}
- />
- {/snippet}
- </Form.Control>
- </Form.Field>
- </div>
-
- <div class="flex w-full max-w-3xl flex-col space-y-5">
- <Form.Field {form} name="username">
- <Form.Control>
- {#snippet children({ props })}
- <Form.Label>Username</Form.Label>
- <Input
- {...props}
- aria-invalid={$errors.username ? "true" : undefined}
- bind:value={$formData.username}
- placeholder="@username"
- {...$constraints.username}
- disabled
- />
- <Form.Description>This is your GNU/Weeb email username.</Form.Description>
- {/snippet}
- </Form.Control>
- </Form.Field>
-
- <Form.Field {form} name="full_name">
- <Form.Control>
- {#snippet children({ props })}
- <Form.Label>Full Name</Form.Label>
- <Input
- {...props}
- aria-invalid={$errors.full_name ? "true" : undefined}
- bind:value={$formData.full_name}
- placeholder="Your full name"
- {...$constraints.full_name}
- disabled
- />
- {/snippet}
- </Form.Control>
- </Form.Field>
-
- <Form.Field {form} name="gender">
- <Form.Control>
- {#snippet children({ props })}
- <Form.Label>Gender</Form.Label>
- <RadioGroup.Root
- {...props}
- aria-invalid={$errors.gender ? "true" : undefined}
- bind:value={$formData.gender}
- placeholder="Your full name"
- {...$constraints.gender}
- disabled
+<Dialog.Root open={showModalConfirmation} onOpenChange={handleOpenModal}>
+ <form use:enhance class="flex flex-col gap-5" enctype="multipart/form-data">
+ <div
+ class="flex w-full max-w-5xl flex-col gap-y-8 lg:flex-row lg:justify-between lg:gap-x-8 lg:gap-y-0"
+ >
+ <div class="flex w-full justify-center lg:hidden">
+ <Form.Field {form} name="photo" class="relative text-center">
+ <Form.Control>
+ {#snippet children({ props })}
+ <Form.Label for="photo" class="cursor-pointer space-y-2">
+ <span>Profile picture</span>
+ <Avatar.Root class="size-40">
+ <Avatar.Image src={avatar} alt="@{auth.user?.username}" />
+ <Avatar.Fallback class="text-xl">
+ {#if !Boolean(avatar)}
+ {getShortName()}
+ {:else}
+ <Loading class="size-7 text-black" />
+ {/if}
+ </Avatar.Fallback>
+ </Avatar.Root>
+ <Popover.Root>
+ <Popover.Trigger
+ class="absolute bottom-3 left-0 flex h-max w-max items-center gap-x-1 rounded-lg border border-input bg-background px-2 py-1 hover:bg-accent hover:text-accent-foreground"
+ >
+ <Pencil class="size-4" />
+ <span class="text-xs font-medium">Edit</span>
+ </Popover.Trigger>
+ <Popover.Content class="flex max-w-[8rem] flex-col gap-y-1 p-1 text-sm">
+ <Button variant="ghost" class="h-max w-full px-0 py-0">
+ <Form.Label
+ for="photo"
+ class="w-full cursor-pointer rounded-md px-2 py-1.5 text-start text-xs"
+ >
+ Upload...
+ </Form.Label>
+ </Button>
+ <Button
+ variant="ghost"
+ class="flex h-max w-full justify-start px-2 py-1.5 text-xs"
+ >
+ Delete
+ </Button>
+ </Popover.Content>
+ </Popover.Root>
+ </Form.Label>
+ <Input
+ type="file"
+ accept="image/png,image/jpeg"
+ {...props}
+ aria-invalid={$errors.photo ? "true" : undefined}
+ disabled={$submitting}
+ onchange={handleAvatar}
+ {...$constraints.photo}
+ class="hidden"
+ />
+ {/snippet}
+ </Form.Control>
+ </Form.Field>
+ </div>
+
+ <div class="flex w-full max-w-3xl flex-col space-y-5">
+ <Form.Field {form} name="username">
+ <Form.Control>
+ {#snippet children({ props })}
+ <Form.Label for="username">Username</Form.Label>
+ <Input
+ {...props}
+ aria-invalid={$errors.username ? "true" : undefined}
+ bind:value={$formData.username}
+ placeholder="@username"
+ {...$constraints.username}
+ disabled
+ />
+ <Form.Description>This is your GNU/Weeb email username.</Form.Description>
+ {/snippet}
+ </Form.Control>
+ </Form.Field>
+
+ <Form.Field {form} name="ext_email">
+ <Form.Control>
+ {#snippet children({ props })}
+ <Form.Label for="ext_email">External Email</Form.Label>
+ <Input
+ {...props}
+ aria-invalid={$errors.ext_email ? "true" : undefined}
+ bind:value={$formData.ext_email}
+ placeholder="Your external email address"
+ disabled={$submitting}
+ {...$constraints.ext_email}
+ />
+ <Form.FieldErrors />
+ <Form.Description>
+ This is your external email address e.g [email protected]
+ </Form.Description>
+ {/snippet}
+ </Form.Control>
+ </Form.Field>
+
+ <Form.Field {form} name="full_name">
+ <Form.Control>
+ {#snippet children({ props })}
+ <Form.Label for="full_name">Full Name</Form.Label>
+ <Input
+ {...props}
+ aria-invalid={$errors.full_name ? "true" : undefined}
+ bind:value={$formData.full_name}
+ placeholder="Your full name"
+ disabled={$submitting}
+ {...$constraints.full_name}
+ />
+ <Form.FieldErrors />
+ <Form.Description>You full name must be your real name.</Form.Description>
+ {/snippet}
+ </Form.Control>
+ </Form.Field>
+
+ <Form.Field {form} name="gender">
+ <Form.Control>
+ {#snippet children({ props })}
+ <Form.Label for="gender">Gender</Form.Label>
+ <RadioGroup.Root
+ {...props}
+ aria-invalid={$errors.gender ? "true" : undefined}
+ bind:value={$formData.gender}
+ placeholder="Your full name"
+ disabled={$submitting}
+ {...$constraints.gender}
+ >
+ <div class="flex items-center space-x-2">
+ <RadioGroup.Item value="m" id="m" />
+ <Label for="m">Male</Label>
+ </div>
+ <div class="flex items-center space-x-2">
+ <RadioGroup.Item value="f" id="f" />
+ <Label for="f">Female</Label>
+ </div>
+ </RadioGroup.Root>
+ <Form.FieldErrors />
+ <Form.Description>
+ Your real gender, <b><u><i>NO LGBT</i></u></b>.
+ </Form.Description>
+ {/snippet}
+ </Form.Control>
+ </Form.Field>
+
+ <div class="space-y-1">
+ <span
+ class="select-none text-sm font-medium {Boolean($errors.socials) && 'text-destructive'}"
>
- <div class="flex items-center space-x-2">
- <RadioGroup.Item value="m" id="m" />
- <Label for="m">Male</Label>
- </div>
- <div class="flex items-center space-x-2">
- <RadioGroup.Item value="f" id="f" />
- <Label for="f">Female</Label>
- </div>
- </RadioGroup.Root>
- {/snippet}
- </Form.Control>
- </Form.Field>
- </div>
-
- <div class="hidden lg:block">
- <Form.Field {form} name="avatar" class="relative">
- <Form.Control>
- {#snippet children({ props })}
- <Form.Label for="avatar" class="cursor-pointer">
- <Avatar.Root class="lg:size-40 xl:size-52">
- <Avatar.Image src={avatarImage} alt="@{auth.user?.username}" />
- <Avatar.Fallback class="lg:text-xl xl:text-3xl">{getShortName()}</Avatar.Fallback>
- </Avatar.Root>
- <div
- class="absolute bottom-3 left-0 flex items-center gap-x-1 rounded-lg bg-foreground px-2 py-1 text-primary-foreground xl:left-2.5"
- >
- <Pencil class="size-4" />
- <span class="text-xs font-medium">Edit</span>
- </div>
- </Form.Label>
- <Input
- type="file"
- accept="image/png,image/jpeg"
- {...props}
- aria-invalid={$errors.avatar ? "true" : undefined}
- bind:value={$formData.avatar}
- disabled={$submitting}
- {...$constraints.avatar}
- class="hidden"
- onchange={handleAvatar}
- />
- {/snippet}
- </Form.Control>
- </Form.Field>
- </div>
-</form>
+ Social Accounts
+ </span>
+ <div class="flex items-center gap-x-2">
+ <IconGithub href="https://github.com/{$formData.socials.github_username}" />
+ <Form.Field {form} name="socials.github_username" class="w-full">
+ <Form.Control>
+ {#snippet children({ props })}
+ <Input
+ {...props}
+ aria-invalid={$errors.socials?.github_username ? "true" : undefined}
+ bind:value={$formData.socials.github_username}
+ placeholder="Your github username"
+ disabled={$submitting}
+ {...$constraints.socials?.github_username}
+ />
+ {/snippet}
+ </Form.Control>
+ </Form.Field>
+ </div>
+ <div class="flex items-center gap-x-2">
+ <IconTelegram href="https://{$formData.socials.telegram_username}.t.me" />
+ <Form.Field {form} name="socials.telegram_username" class="w-full">
+ <Form.Control>
+ {#snippet children({ props })}
+ <Input
+ {...props}
+ aria-invalid={$errors.socials?.telegram_username ? "true" : undefined}
+ bind:value={$formData.socials.telegram_username}
+ placeholder="Your telegram username"
+ disabled={$submitting}
+ {...$constraints.socials?.telegram_username}
+ />
+ {/snippet}
+ </Form.Control>
+ </Form.Field>
+ </div>
+ <div class="flex items-center gap-x-2">
+ <IconTwitter href="https://x.com/{$formData.socials.twitter_username}" />
+ <Form.Field {form} name="socials.twitter_username" class="w-full">
+ <Form.Control>
+ {#snippet children({ props })}
+ <Input
+ {...props}
+ aria-invalid={$errors.socials?.twitter_username ? "true" : undefined}
+ bind:value={$formData.socials.twitter_username}
+ placeholder="Your twitter username"
+ disabled={$submitting}
+ {...$constraints.socials?.twitter_username}
+ />
+ {/snippet}
+ </Form.Control>
+ </Form.Field>
+ </div>
+ <div class="flex items-center gap-x-2">
+ <IconDiscord />
+ <Form.Field {form} name="socials.discord_username" class="w-full">
+ <Form.Control>
+ {#snippet children({ props })}
+ <Input
+ {...props}
+ aria-invalid={$errors.socials?.discord_username ? "true" : undefined}
+ bind:value={$formData.socials.discord_username}
+ placeholder="Your discord username"
+ disabled={$submitting}
+ {...$constraints.socials?.discord_username}
+ />
+ {/snippet}
+ </Form.Control>
+ </Form.Field>
+ </div>
+ </div>
+ </div>
+
+ <div class="hidden lg:block">
+ <Form.Field {form} name="photo" class="relative text-center">
+ <Form.Control>
+ {#snippet children({ props })}
+ <Form.Label for="photo" class="cursor-pointer space-y-2">
+ <span>Profile picture</span>
+ <Avatar.Root class="lg:size-40 xl:size-52">
+ <Avatar.Image src={avatar} alt="@{auth.user?.username}" />
+ <Avatar.Fallback class="lg:text-xl xl:text-3xl">
+ {#if !Boolean(avatar)}
+ {getShortName()}
+ {:else}
+ <Loading class="size-10 text-black" />
+ {/if}
+ </Avatar.Fallback>
+ </Avatar.Root>
+ <Popover.Root>
+ <Popover.Trigger
+ class="absolute bottom-3 left-0 flex h-max w-max items-center gap-x-1 rounded-lg border border-input bg-background px-2 py-1 hover:bg-accent hover:text-accent-foreground xl:left-2.5"
+ >
+ <Pencil class="size-4" />
+ <span class="text-xs font-medium">Edit</span>
+ </Popover.Trigger>
+ <Popover.Content class="flex max-w-[8rem] flex-col gap-y-1 p-1 text-sm">
+ <Button variant="ghost" class="h-max w-full px-0 py-0">
+ <Form.Label
+ for="photo"
+ class="w-full cursor-pointer rounded-md px-2 py-1.5 text-start text-xs"
+ >
+ Upload...
+ </Form.Label>
+ </Button>
+ <Button
+ variant="ghost"
+ class="flex h-max w-full justify-start px-2 py-1.5 text-xs"
+ >
+ Delete
+ </Button>
+ </Popover.Content>
+ </Popover.Root>
+ </Form.Label>
+ <Input
+ type="file"
+ accept="image/png,image/jpeg"
+ {...props}
+ aria-invalid={$errors.photo ? "true" : undefined}
+ disabled={$submitting}
+ onchange={handleAvatar}
+ {...$constraints.photo}
+ class="hidden"
+ />
+ {/snippet}
+ </Form.Control>
+ </Form.Field>
+ </div>
+ </div>
+
+ <Dialog.Trigger>
+ {#snippet child({ props })}
+ <Button
+ type="button"
+ class="w-max px-5"
+ disabled={$submitting || !isSubmittable || isError}
+ {...props}
+ >
+ Update profile {isSubmittable}
+ </Button>
+ {/snippet}
+ </Dialog.Trigger>
+
+ <Dialog.Content class="sm:max-w-[425px]">
+ <Dialog.Header>
+ <Dialog.Title>Update Profile Confirmation</Dialog.Title>
+ <Dialog.Description>Confirm changes to your profile here.</Dialog.Description>
+ </Dialog.Header>
+ <div>
+ <Form.Field {form} name="password" class="w-full">
+ <Form.Control>
+ {#snippet children({ props })}
+ <Form.Label for="password">Password</Form.Label>
+ <InputPassword
+ {...props}
+ aria-invalid={$errors.password ? "true" : undefined}
+ bind:value={$formData.password}
+ placeholder="Enter password"
+ disabled={$submitting}
+ {...$constraints.password}
+ />
+ <Form.Description>
+ Your password is required to make changes to your profile.
+ </Form.Description>
+ {/snippet}
+ </Form.Control>
+ </Form.Field>
+ </div>
+ <Dialog.Footer>
+ <Button type="submit" onclick={handleSubmit}>Confirm</Button>
+ </Dialog.Footer>
+ </Dialog.Content>
+ </form>
+</Dialog.Root>
diff --git a/src/routes/(protected)/settings/profile/+page.ts b/src/routes/(protected)/settings/profile/+page.ts
index 9e036c8..7b25a4f 100644
--- a/src/routes/(protected)/settings/profile/+page.ts
+++ b/src/routes/(protected)/settings/profile/+page.ts
@@ -11,10 +11,11 @@ export const load: PageLoad = async () => {
const data = {
username: auth.user?.username,
full_name: auth.user?.full_name,
- gender: auth.user?.gender
+ ext_email: auth.user?.ext_email,
+ gender: auth.user?.gender,
+ socials: auth.user?.socials
};
const form = await superValidate(data, zod(profileSchema));
-
- return { form };
+ return { form, avatar: auth.user?.photo };
};
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index eeadde5..cb4f673 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -22,7 +22,10 @@
validators: zod(loginSchema),
async onUpdate({ form }) {
- const res = await http<typing.LoginResponse>({
+ const {
+ status,
+ data: { res }
+ } = await http<typing.LoginResponse>({
params: { action: "login" },
method: "POST",
data: {
@@ -31,12 +34,12 @@
}
});
- if (res.status === 200) {
- auth.save(res.data.res!);
+ if (status === 200) {
+ auth.save(res as typing.LoginResponse);
} else {
setError(form, "username_or_email", "");
setError(form, "password", "");
- setMessage(form, res.data.res);
+ setMessage(form, res?.msg ?? "Invalid credential, please login again.");
}
}
});
@@ -57,8 +60,10 @@
<Card.Root class="w-full max-w-lg">
<form method="POST" use:enhance>
<Card.Header class="flex items-center justify-center space-y-1">
- <Card.Title class="text-xl text-center lg:text-2xl">GNU/Weeb Mail Login</Card.Title>
- <Card.Description class="text-center">Proceed login to manage your email account</Card.Description>
+ <Card.Title class="text-center text-xl lg:text-2xl">GNU/Weeb Mail Login</Card.Title>
+ <Card.Description class="text-center">
+ Proceed login to manage your email account
+ </Card.Description>
{#if isError() && !isCredentialInvalid()}
<span class="text-sm font-medium text-destructive">
--
Muhammad Rizki
next prev parent reply other threads:[~2025-03-05 14:41 UTC|newest]
Thread overview: 29+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-03-05 14:39 [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs Muhammad Rizki
2025-03-05 14:40 ` [PATCH v1 01/17] fix(typing): add user_info type prop Muhammad Rizki
2025-03-05 14:40 ` [PATCH v1 02/17] refactor: optimize icon imports to reduce bundle size Muhammad Rizki
2025-03-05 14:40 ` [PATCH v1 03/17] chore(change-pwd): adjust change password heading styling Muhammad Rizki
2025-03-05 14:40 ` [PATCH v1 04/17] chore(settings/layout): use prose: for " Muhammad Rizki
2025-03-05 14:40 ` [PATCH v1 05/17] fix(profile): fix edit avatar button position Muhammad Rizki
2025-03-05 14:40 ` [PATCH v1 06/17] fix(breadcrumb): Move settingsNav to settings items navigations Muhammad Rizki
2025-03-05 14:40 ` [PATCH v1 07/17] chore(responsive): adjust styling Muhammad Rizki
2025-03-05 14:40 ` [PATCH v1 08/17] chore(navigations): Replace index /settings url Muhammad Rizki
2025-03-05 14:40 ` [PATCH v1 09/17] feat(ui): Add popover and dialog UI component Muhammad Rizki
2025-03-05 14:40 ` [PATCH v1 10/17] feat(http): Use PUBLIC_BASE_URL for each environment Muhammad Rizki
2025-03-05 14:40 ` [PATCH v1 11/17] feat(icons): Add social icons Muhammad Rizki
2025-03-05 14:40 ` [PATCH v1 12/17] feat(typing/enum): add Gender and IsActive enum Muhammad Rizki
2025-03-05 14:40 ` Muhammad Rizki [this message]
2025-03-05 14:40 ` [PATCH v1 14/17] chore(meta): rename favicon.png to favicon.ico Muhammad Rizki
2025-03-05 14:40 ` [PATCH v1 15/17] feat(seo): add SEO for site metadata Muhammad Rizki
2025-03-05 14:40 ` [PATCH v1 16/17] chore(login): use $derived() instead of function based Muhammad Rizki
2025-03-05 14:40 ` [PATCH v1 17/17] docs: update README.md Muhammad Rizki
2025-03-05 16:54 ` [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs Ammar Faizi
2025-03-05 16:57 ` Alviro Iskandar Setiawan
2025-03-06 7:01 ` Muhammad Rizki
2025-03-06 7:02 ` Alviro Iskandar Setiawan
2025-03-06 7:04 ` Muhammad Rizki
2025-03-06 2:02 ` Muhammad Rizki
2025-03-06 3:37 ` Ammar Faizi
2025-03-05 17:04 ` Ammar Faizi
2025-03-05 18:14 ` Alviro Iskandar Setiawan
2025-03-06 1:59 ` Muhammad Rizki
2025-03-06 3:35 ` Ammar Faizi
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
[email protected] \
[email protected] \
[email protected] \
[email protected] \
[email protected] \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox