public inbox for [email protected]
 help / color / mirror / Atom feed
* [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs
@ 2025-03-05 14:39 Muhammad Rizki
  2025-03-05 14:40 ` [PATCH v1 01/17] fix(typing): add user_info type prop Muhammad Rizki
                   ` (19 more replies)
  0 siblings, 20 replies; 29+ messages in thread
From: Muhammad Rizki @ 2025-03-05 14:39 UTC (permalink / raw)
  To: Ammar Faizi
  Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List

Hello,
This series of patches was planned only to update profile page feature,
since there is a change in the API structure, I also fixed
the API response type to prevent breaking change.

To prevent more breaking change on production site in the future,
sir Ammar has made a staging API for development stage,
since there is a staging API version, I added an environment variable
`PUBLIC_BASE_URL` for each environment (development or production).
You should set `PUBLIC_BASE_URL` environment in your own server,
there is an example file for development and production environment
in this patch, please see `.env.development.example` and
`.env.production.example` in root directory.

I also added API documentation in Postman workspace that I created
myself, you can find it in this link:
https://gnuweeb.postman.co/workspace/GNU%252FWeeb~2805fbdd-376b-42de-8fe1-2a4cfa22ed3c/overview?ctx=updates

Please give it a test and review, thank you.

Muhammad Rizki (17):
  fix(typing): add user_info type prop
  refactor: optimize icon imports to reduce bundle size
  chore(change-pwd): adjust change password heading styling
  chore(settings/layout): use prose: for heading styling
  fix(profile): fix edit avatar button position
  fix(breadcrumb): Move settingsNav to settings items navigations
  chore(responsive): adjust styling
  chore(navigations): Replace index /settings url
  feat(ui): Add popover and dialog UI component
  feat(http): Use PUBLIC_BASE_URL for each environment
  feat(icons): Add social icons
  feat(typing/enum): add Gender and IsActive enum
  refactor!:feat: update API response structure, update profile page
  chore(meta): rename favicon.png to favicon.ico
  feat(seo): add SEO for site metadata
  chore(login): use $derived() instead of function based
  docs: update README.md

 .env.development.example                      |   2 +
 .env.production.example                       |   2 +
 .gitignore                                    |   2 +-
 README.md                                     |   7 +
 package-lock.json                             |   8 +-
 package.json                                  |   2 +-
 src/app.html                                  |   2 +-
 src/lib/components/customs/app-sidebar.svelte |   4 +-
 src/lib/components/customs/header.svelte      |  39 +-
 src/lib/components/customs/seo.svelte         |  27 +
 src/lib/components/icons/icon-discord.svelte  |  35 ++
 src/lib/components/icons/icon-github.svelte   |  35 ++
 src/lib/components/icons/icon-telegram.svelte |  35 ++
 src/lib/components/icons/icon-twitter.svelte  |  35 ++
 .../components/ui/avatar/avatar-image.svelte  |   2 +-
 src/lib/components/ui/avatar/avatar.svelte    |   2 +-
 .../ui/dialog/dialog-content.svelte           |  38 ++
 .../ui/dialog/dialog-description.svelte       |  16 +
 .../components/ui/dialog/dialog-footer.svelte |  20 +
 .../components/ui/dialog/dialog-header.svelte |  20 +
 .../ui/dialog/dialog-overlay.svelte           |  19 +
 .../components/ui/dialog/dialog-title.svelte  |  16 +
 src/lib/components/ui/dialog/index.ts         |  37 ++
 .../components/ui/input/input-password.svelte |   5 +-
 src/lib/components/ui/input/input.svelte      |   2 +-
 src/lib/components/ui/popover/index.ts        |  17 +
 .../ui/popover/popover-content.svelte         |  28 +
 src/lib/constants/navigations.ts              |  30 +-
 src/lib/hooks/auth.svelte.ts                  |  12 +-
 src/lib/hooks/http.svelte.ts                  |   3 +-
 src/lib/schemas/profile-schema.ts             |  20 +-
 src/lib/typings/common.d.ts                   |  11 +
 src/lib/typings/credential.d.ts               |  11 +-
 src/lib/typings/http.d.ts                     |  11 +-
 src/lib/typings/index.ts                      |  11 +-
 src/lib/utilities/index.ts                    |   3 +-
 src/lib/utilities/navigation.ts               |   5 +
 src/routes/(protected)/+layout.svelte         |  12 +
 src/routes/(protected)/+layout.ts             |   9 +-
 .../(components)/settings-header.svelte       |   3 +-
 .../(protected)/settings/+layout.svelte       |   7 +-
 src/routes/(protected)/settings/+page.ts      |   6 +-
 .../(protected)/settings/account/+page.svelte |   9 +-
 .../(protected)/settings/profile/+page.svelte | 580 +++++++++++++-----
 .../(protected)/settings/profile/+page.ts     |   7 +-
 src/routes/+page.svelte                       |  36 +-
 static/{favicon.png => favicon.ico}           | Bin
 47 files changed, 1024 insertions(+), 219 deletions(-)
 create mode 100644 .env.development.example
 create mode 100644 .env.production.example
 create mode 100644 src/lib/components/customs/seo.svelte
 create mode 100644 src/lib/components/icons/icon-discord.svelte
 create mode 100644 src/lib/components/icons/icon-github.svelte
 create mode 100644 src/lib/components/icons/icon-telegram.svelte
 create mode 100644 src/lib/components/icons/icon-twitter.svelte
 create mode 100644 src/lib/components/ui/dialog/dialog-content.svelte
 create mode 100644 src/lib/components/ui/dialog/dialog-description.svelte
 create mode 100644 src/lib/components/ui/dialog/dialog-footer.svelte
 create mode 100644 src/lib/components/ui/dialog/dialog-header.svelte
 create mode 100644 src/lib/components/ui/dialog/dialog-overlay.svelte
 create mode 100644 src/lib/components/ui/dialog/dialog-title.svelte
 create mode 100644 src/lib/components/ui/dialog/index.ts
 create mode 100644 src/lib/components/ui/popover/index.ts
 create mode 100644 src/lib/components/ui/popover/popover-content.svelte
 create mode 100644 src/lib/utilities/navigation.ts
 rename static/{favicon.png => favicon.ico} (100%)


base-commit: 3c3c00ff4e261aaeab8d91323b99eebf22adc4dd
--
Muhammad Rizki

^ permalink raw reply	[flat|nested] 29+ messages in thread

* [PATCH v1 01/17] fix(typing): add user_info type prop
  2025-03-05 14:39 [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs Muhammad Rizki
@ 2025-03-05 14:40 ` Muhammad Rizki
  2025-03-05 14:40 ` [PATCH v1 02/17] refactor: optimize icon imports to reduce bundle size Muhammad Rizki
                   ` (18 subsequent siblings)
  19 siblings, 0 replies; 29+ messages in thread
From: Muhammad Rizki @ 2025-03-05 14:40 UTC (permalink / raw)
  To: Ammar Faizi
  Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List

Since there is a change on API, we should update the response type.

Signed-off-by: Muhammad Rizki <[email protected]>
---
 src/routes/(protected)/+layout.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/routes/(protected)/+layout.ts b/src/routes/(protected)/+layout.ts
index 64019ba..91dbfc7 100644
--- a/src/routes/(protected)/+layout.ts
+++ b/src/routes/(protected)/+layout.ts
@@ -12,10 +12,10 @@ export const load: LayoutLoad = async () => {
     return redirect(307, "/");
   }
 
-  const { data } = await http<typing.User>({
+  const { data } = await http<{ user_info: typing.User }>({
     params: { action: "get_user_info" }
   });
 
-  localStorage.setItem("gwm_uinfo", JSON.stringify(data.res.user_info));
+  localStorage.setItem("gwm_uinfo", JSON.stringify(data.res?.user_info));
   auth.refresh();
 };
-- 
Muhammad Rizki


^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v1 02/17] refactor: optimize icon imports to reduce bundle size
  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 ` Muhammad Rizki
  2025-03-05 14:40 ` [PATCH v1 03/17] chore(change-pwd): adjust change password heading styling Muhammad Rizki
                   ` (17 subsequent siblings)
  19 siblings, 0 replies; 29+ messages in thread
From: Muhammad Rizki @ 2025-03-05 14:40 UTC (permalink / raw)
  To: Ammar Faizi
  Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List

Previously, all lucide-Svelte icons were imported, increasing the bundle
size. This update changes the import to only include specific icons,
reducing the lucide-svelte-related bundle size.

Signed-off-by: Muhammad Rizki <[email protected]>
---
 src/lib/components/customs/app-sidebar.svelte        | 4 +++-
 src/lib/components/ui/input/input-password.svelte    | 3 ++-
 src/lib/constants/navigations.ts                     | 3 ++-
 src/routes/(protected)/settings/profile/+page.svelte | 4 ++--
 4 files changed, 9 insertions(+), 5 deletions(-)

diff --git a/src/lib/components/customs/app-sidebar.svelte b/src/lib/components/customs/app-sidebar.svelte
index 2fba975..7ccc59c 100644
--- a/src/lib/components/customs/app-sidebar.svelte
+++ b/src/lib/components/customs/app-sidebar.svelte
@@ -3,7 +3,9 @@
   import Separator from "$components/ui/separator/separator.svelte";
   import * as Sidebar from "$lib/components/ui/sidebar";
   import { useAuth } from "$lib/hooks/auth.svelte";
-  import { LogOut, Mails, SquareArrowOutUpRight } from "lucide-svelte";
+  import LogOut from "lucide-svelte/icons/log-out";
+  import Mails from "lucide-svelte/icons/mails";
+  import SquareArrowOutUpRight from "lucide-svelte/icons/square-arrow-out-up-right";
   import type { ComponentProps } from "svelte";
   import { goto } from "$app/navigation";
   import Button from "$components/ui/button/button.svelte";
diff --git a/src/lib/components/ui/input/input-password.svelte b/src/lib/components/ui/input/input-password.svelte
index ac1df87..6e2c1ab 100644
--- a/src/lib/components/ui/input/input-password.svelte
+++ b/src/lib/components/ui/input/input-password.svelte
@@ -3,7 +3,8 @@
   import type { HTMLInputAttributes } from "svelte/elements";
   import Input from "./input.svelte";
   import Button from "$components/ui/button/button.svelte";
-  import { EyeIcon, EyeOffIcon } from "lucide-svelte";
+  import EyeIcon from "lucide-svelte/icons/eye";
+  import EyeOffIcon from "lucide-svelte/icons/eye-off";
 
   let {
     ref = $bindable(null),
diff --git a/src/lib/constants/navigations.ts b/src/lib/constants/navigations.ts
index ada0e40..ca5e77b 100644
--- a/src/lib/constants/navigations.ts
+++ b/src/lib/constants/navigations.ts
@@ -1,5 +1,6 @@
 import * as typing from "$typings";
-import { Home, Settings } from "lucide-svelte";
+import Home from "lucide-svelte/icons/home"
+import Settings from "lucide-svelte/icons/settings"
 
 export const navigations: typing.Navigations[] = [
   {
diff --git a/src/routes/(protected)/settings/profile/+page.svelte b/src/routes/(protected)/settings/profile/+page.svelte
index 442e271..05f62b8 100644
--- a/src/routes/(protected)/settings/profile/+page.svelte
+++ b/src/routes/(protected)/settings/profile/+page.svelte
@@ -3,7 +3,7 @@
   import { superForm } from "sveltekit-superforms";
   import { zodClient } from "sveltekit-superforms/adapters";
   import { profileSchema } from "$lib/schemas/profile-schema";
-  import { Pencil } from "lucide-svelte";
+  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";
@@ -18,7 +18,7 @@
 
   const form = superForm(data.form, {
     SPA: true,
-    validators: zodClient(profileSchema),
+    validators: zodClient(profileSchema)
   });
 
   const getShortName = () => {
-- 
Muhammad Rizki


^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v1 03/17] chore(change-pwd): adjust change password heading styling
  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 ` Muhammad Rizki
  2025-03-05 14:40 ` [PATCH v1 04/17] chore(settings/layout): use prose: for " Muhammad Rizki
                   ` (16 subsequent siblings)
  19 siblings, 0 replies; 29+ messages in thread
From: Muhammad Rizki @ 2025-03-05 14:40 UTC (permalink / raw)
  To: Ammar Faizi
  Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List

Wrap heading element with div and use separator component instead of
<hr/>

Signed-off-by: Muhammad Rizki <[email protected]>
---
 src/routes/(protected)/settings/account/+page.svelte | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/routes/(protected)/settings/account/+page.svelte b/src/routes/(protected)/settings/account/+page.svelte
index 99cfb77..29ccd06 100644
--- a/src/routes/(protected)/settings/account/+page.svelte
+++ b/src/routes/(protected)/settings/account/+page.svelte
@@ -7,6 +7,7 @@
   import InputPassword from "$components/ui/input/input-password.svelte";
   import Button from "$components/ui/button/button.svelte";
   import http from "$lib/hooks/http.svelte";
+  import Separator from "$components/ui/separator/separator.svelte";
 
   let { data } = $props();
 
@@ -49,7 +50,10 @@
   );
 </script>
 
-<h1>Change Password</h1><hr/>
+<div class="space-y-3">
+  <h2 class="text-center font-medium sm:text-start">Change Password</h2>
+  <Separator />
+</div>
 <form use:enhance class="space-y-5">
   <Form.Field {form} name="cur_pass">
     <Form.Control>
-- 
Muhammad Rizki


^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v1 04/17] chore(settings/layout): use prose: for heading styling
  2025-03-05 14:39 [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs Muhammad Rizki
                   ` (2 preceding siblings ...)
  2025-03-05 14:40 ` [PATCH v1 03/17] chore(change-pwd): adjust change password heading styling Muhammad Rizki
@ 2025-03-05 14:40 ` Muhammad Rizki
  2025-03-05 14:40 ` [PATCH v1 05/17] fix(profile): fix edit avatar button position Muhammad Rizki
                   ` (15 subsequent siblings)
  19 siblings, 0 replies; 29+ messages in thread
From: Muhammad Rizki @ 2025-03-05 14:40 UTC (permalink / raw)
  To: Ammar Faizi
  Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List

Using tailwind typography prose: to style heading in settings layout.

Signed-off-by: Muhammad Rizki <[email protected]>
---
 src/routes/(protected)/settings/+layout.svelte | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/routes/(protected)/settings/+layout.svelte b/src/routes/(protected)/settings/+layout.svelte
index 3ef02e4..71e30d8 100644
--- a/src/routes/(protected)/settings/+layout.svelte
+++ b/src/routes/(protected)/settings/+layout.svelte
@@ -8,7 +8,7 @@
   let { children } = $props();
 </script>
 
-<div class="space-y-6 pb-16 prose-h2:text-2xl prose-h3:text-lg">
+<div class="space-y-6 pb-16 prose-h1:text-3xl prose-h2:text-2xl prose-h3:text-lg">
   <div class="space-y-0.5">
     <h2 class="font-bold tracking-tight">Settings</h2>
     <p class="text-muted-foreground">Manage your account settings and set e-mail preferences.</p>
-- 
Muhammad Rizki


^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v1 05/17] fix(profile): fix edit avatar button position
  2025-03-05 14:39 [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs Muhammad Rizki
                   ` (3 preceding siblings ...)
  2025-03-05 14:40 ` [PATCH v1 04/17] chore(settings/layout): use prose: for " Muhammad Rizki
@ 2025-03-05 14:40 ` Muhammad Rizki
  2025-03-05 14:40 ` [PATCH v1 06/17] fix(breadcrumb): Move settingsNav to settings items navigations Muhammad Rizki
                   ` (14 subsequent siblings)
  19 siblings, 0 replies; 29+ messages in thread
From: Muhammad Rizki @ 2025-03-05 14:40 UTC (permalink / raw)
  To: Ammar Faizi
  Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List

Previously, edit button has an issue where the button position is not
correctly positioned.

Signed-off-by: Muhammad Rizki <[email protected]>
---
 src/routes/(protected)/settings/profile/+page.svelte | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/routes/(protected)/settings/profile/+page.svelte b/src/routes/(protected)/settings/profile/+page.svelte
index 05f62b8..73edb8a 100644
--- a/src/routes/(protected)/settings/profile/+page.svelte
+++ b/src/routes/(protected)/settings/profile/+page.svelte
@@ -45,10 +45,10 @@
   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">
+    <Form.Field {form} name="avatar" class="relative">
       <Form.Control>
         {#snippet children({ props })}
-          <Form.Label for="avatar" class="relative cursor-pointer">
+          <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>
@@ -137,10 +137,10 @@
   </div>
 
   <div class="hidden lg:block">
-    <Form.Field {form} name="avatar">
+    <Form.Field {form} name="avatar" class="relative">
       <Form.Control>
         {#snippet children({ props })}
-          <Form.Label for="avatar" class="relative cursor-pointer">
+          <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>
-- 
Muhammad Rizki


^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v1 06/17] fix(breadcrumb): Move settingsNav to settings items navigations
  2025-03-05 14:39 [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs Muhammad Rizki
                   ` (4 preceding siblings ...)
  2025-03-05 14:40 ` [PATCH v1 05/17] fix(profile): fix edit avatar button position Muhammad Rizki
@ 2025-03-05 14:40 ` Muhammad Rizki
  2025-03-05 14:40 ` [PATCH v1 07/17] chore(responsive): adjust styling Muhammad Rizki
                   ` (13 subsequent siblings)
  19 siblings, 0 replies; 29+ messages in thread
From: Muhammad Rizki @ 2025-03-05 14:40 UTC (permalink / raw)
  To: Ammar Faizi
  Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List

Move settingsNav items to navigations settings items to make breadcrumb
works properly.

Signed-off-by: Muhammad Rizki <[email protected]>
---
 src/lib/components/customs/header.svelte      | 39 +++++++++++++++----
 src/lib/constants/navigations.ts              | 31 +++++++--------
 src/lib/typings/common.d.ts                   |  1 +
 src/lib/utilities/index.ts                    |  3 +-
 src/lib/utilities/navigation.ts               |  5 +++
 .../(components)/settings-header.svelte       |  3 +-
 .../(protected)/settings/+layout.svelte       |  5 +--
 src/routes/(protected)/settings/+page.ts      |  6 ++-
 8 files changed, 63 insertions(+), 30 deletions(-)
 create mode 100644 src/lib/utilities/navigation.ts

diff --git a/src/lib/components/customs/header.svelte b/src/lib/components/customs/header.svelte
index 44c681e..6a343d5 100644
--- a/src/lib/components/customs/header.svelte
+++ b/src/lib/components/customs/header.svelte
@@ -4,12 +4,28 @@
   import { page } from "$app/state";
   import { navigations } from "$constants";
   import Separator from "$components/ui/separator/separator.svelte";
+  import * as typing from "$typings";
 
-  const getRouteName = () => {
-    const pathname = page.url.pathname;
-    const found = navigations.find((path) => path.url === pathname);
+  const getParentRoute = (
+    path: string,
+    items: typing.Navigations[],
+    routePath: typing.Navigations[] = []
+  ): typing.Navigations[] => {
+    for (const item of items) {
+      if (path === item.url) {
+        return [...routePath, item];
+      }
 
-    return found?.name ?? "";
+      if (item.items) {
+        const foundPath = getParentRoute(path, item.items, [...routePath, item]);
+        if (foundPath.length) return foundPath;
+      }
+    }
+    return [];
+  };
+
+  const getRouteName = () => {
+    return getParentRoute(page.url.pathname, navigations);
   };
 </script>
 
@@ -23,9 +39,18 @@
           <Breadcrumb.Link href="/">G/W Mail</Breadcrumb.Link>
         </Breadcrumb.Item>
         <Breadcrumb.Separator />
-        <Breadcrumb.Item class="select-none">
-          <Breadcrumb.Page>{getRouteName()}</Breadcrumb.Page>
-        </Breadcrumb.Item>
+        {#each getRouteName() as route, index (route.url)}
+          {#if index === getRouteName().length - 1}
+            <Breadcrumb.Item class="select-none">
+              <Breadcrumb.Page>{route.name}</Breadcrumb.Page>
+            </Breadcrumb.Item>
+          {:else}
+            <Breadcrumb.Item class="hidden md:block">
+              <Breadcrumb.Link href={route.url}>{route.name}</Breadcrumb.Link>
+            </Breadcrumb.Item>
+            <Breadcrumb.Separator />
+          {/if}
+        {/each}
       </Breadcrumb.List>
     </Breadcrumb.Root>
   </div>
diff --git a/src/lib/constants/navigations.ts b/src/lib/constants/navigations.ts
index ca5e77b..4266930 100644
--- a/src/lib/constants/navigations.ts
+++ b/src/lib/constants/navigations.ts
@@ -1,6 +1,6 @@
 import * as typing from "$typings";
-import Home from "lucide-svelte/icons/home"
-import Settings from "lucide-svelte/icons/settings"
+import Home from "lucide-svelte/icons/home";
+import Settings from "lucide-svelte/icons/settings";
 
 export const navigations: typing.Navigations[] = [
   {
@@ -11,19 +11,18 @@ export const navigations: typing.Navigations[] = [
   {
     name: "Settings",
     icon: Settings,
-    url: "/settings"
+    url: "/settings",
+    items: [
+      {
+        name: "Profile",
+        description: "Manage your profile.",
+        url: "/settings/profile"
+      },
+      {
+        name: "Account",
+        description: "Manage your account credentials.",
+        url: "/settings/account"
+      }
+    ]
   }
 ] as const;
-
-export const settingsNav: typing.Navigations[] = [
-  {
-    name: "Profile",
-    description: "Manage your profile.",
-    url: "/settings/profile"
-  },
-  {
-    name: "Account",
-    description: "Manage your account credentials.",
-    url: "/settings/account"
-  }
-];
diff --git a/src/lib/typings/common.d.ts b/src/lib/typings/common.d.ts
index cdbe0d4..1f4a795 100644
--- a/src/lib/typings/common.d.ts
+++ b/src/lib/typings/common.d.ts
@@ -8,6 +8,7 @@ export interface Navigations {
   icon?: typeof IconType;
   url: string;
   disabled?: boolean;
+  items?: Navigations[]
 }
 
 export interface LabelAndValue {
diff --git a/src/lib/utilities/index.ts b/src/lib/utilities/index.ts
index ed0291d..48c622d 100644
--- a/src/lib/utilities/index.ts
+++ b/src/lib/utilities/index.ts
@@ -1,3 +1,4 @@
+import { getSettingsNav } from "./navigation";
 import { cn } from "./styling";
 
-export { cn };
+export { cn, getSettingsNav };
diff --git a/src/lib/utilities/navigation.ts b/src/lib/utilities/navigation.ts
new file mode 100644
index 0000000..4b84eca
--- /dev/null
+++ b/src/lib/utilities/navigation.ts
@@ -0,0 +1,5 @@
+import { navigations } from "$constants";
+
+export const getSettingsNav = () => {
+  return navigations.find((nav) => nav.url.includes("/settings"))?.items ?? [];
+};
diff --git a/src/routes/(protected)/settings/(components)/settings-header.svelte b/src/routes/(protected)/settings/(components)/settings-header.svelte
index cc13950..34e5900 100644
--- a/src/routes/(protected)/settings/(components)/settings-header.svelte
+++ b/src/routes/(protected)/settings/(components)/settings-header.svelte
@@ -1,8 +1,9 @@
 <script lang="ts">
   import { page } from "$app/state";
-  import { settingsNav } from "$constants/navigations";
   import Separator from "$components/ui/separator/separator.svelte";
+  import { getSettingsNav } from "$utils";
 
+  const settingsNav = getSettingsNav();
   const activeNav = $derived(settingsNav.find((e) => page.url.pathname === e.url));
 </script>
 
diff --git a/src/routes/(protected)/settings/+layout.svelte b/src/routes/(protected)/settings/+layout.svelte
index 71e30d8..80d306a 100644
--- a/src/routes/(protected)/settings/+layout.svelte
+++ b/src/routes/(protected)/settings/+layout.svelte
@@ -1,9 +1,8 @@
 <script lang="ts">
   import { Separator } from "$lib/components/ui/separator";
-
-  import { settingsNav } from "$constants/navigations";
   import SettingsNavigation from "./(components)/settings-nav.svelte";
   import SettingsHeader from "./(components)/settings-header.svelte";
+  import { getSettingsNav } from "$utils";
 
   let { children } = $props();
 </script>
@@ -16,7 +15,7 @@
   <Separator class="my-6" />
   <div class="flex flex-col space-y-8 xl:flex-row xl:space-x-8 xl:space-y-0">
     <aside class="xl:w-[12%]">
-      <SettingsNavigation items={settingsNav} />
+      <SettingsNavigation items={getSettingsNav()} />
     </aside>
     <div class="flex-1">
       <div class="space-y-6">
diff --git a/src/routes/(protected)/settings/+page.ts b/src/routes/(protected)/settings/+page.ts
index 3c846ad..c29023a 100644
--- a/src/routes/(protected)/settings/+page.ts
+++ b/src/routes/(protected)/settings/+page.ts
@@ -1,11 +1,13 @@
 import { redirect } from "@sveltejs/kit";
 import type { PageLoad } from "./$types";
-import { settingsNav } from "$constants/navigations";
+import { getSettingsNav } from "$utils";
 
 export const load: PageLoad = async () => {
   // get first page that are not disabled.
+  const settingsNav = getSettingsNav();
   const firstPage = settingsNav.find((e) => !e.disabled);
+  const url = firstPage?.items?.[0].url ?? firstPage?.url;
 
   // if it don't exist, redirect to index page.
-  return redirect(307, firstPage?.url ?? "/");
+  return redirect(307, url ?? "/settings/profile");
 };
-- 
Muhammad Rizki


^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v1 07/17] chore(responsive): adjust styling
  2025-03-05 14:39 [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs Muhammad Rizki
                   ` (5 preceding siblings ...)
  2025-03-05 14:40 ` [PATCH v1 06/17] fix(breadcrumb): Move settingsNav to settings items navigations Muhammad Rizki
@ 2025-03-05 14:40 ` Muhammad Rizki
  2025-03-05 14:40 ` [PATCH v1 08/17] chore(navigations): Replace index /settings url Muhammad Rizki
                   ` (12 subsequent siblings)
  19 siblings, 0 replies; 29+ messages in thread
From: Muhammad Rizki @ 2025-03-05 14:40 UTC (permalink / raw)
  To: Ammar Faizi
  Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List

Previously, some text sizes were too large on mobile devices.
This commit adjusts the text sizes to be more appropriate for mobile
devices and add object-cover on avatar UI.

Signed-off-by: Muhammad Rizki <[email protected]>
---
 src/lib/components/ui/avatar/avatar-image.svelte  | 2 +-
 src/lib/components/ui/avatar/avatar.svelte        | 2 +-
 src/lib/components/ui/input/input-password.svelte | 2 +-
 src/lib/components/ui/input/input.svelte          | 2 +-
 src/routes/+page.svelte                           | 4 ++--
 5 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/lib/components/ui/avatar/avatar-image.svelte b/src/lib/components/ui/avatar/avatar-image.svelte
index 0878ef3..47e9b00 100644
--- a/src/lib/components/ui/avatar/avatar-image.svelte
+++ b/src/lib/components/ui/avatar/avatar-image.svelte
@@ -15,7 +15,7 @@
   bind:ref
   {src}
   {alt}
-  class={cn("aspect-square h-full w-full", className)}
+  class={cn("aspect-square h-full w-full object-cover", className)}
   draggable={false}
   {...restProps}
 />
diff --git a/src/lib/components/ui/avatar/avatar.svelte b/src/lib/components/ui/avatar/avatar.svelte
index 019894e..eaa5692 100644
--- a/src/lib/components/ui/avatar/avatar.svelte
+++ b/src/lib/components/ui/avatar/avatar.svelte
@@ -13,6 +13,6 @@
 <AvatarPrimitive.Root
   bind:loadingStatus
   bind:ref
-  class={cn("relative flex size-10 shrink-0 overflow-hidden rounded-full", className)}
+  class={cn("relative flex size-10 shrink-0 overflow-hidden rounded-full border-2 border-muted", className)}
   {...restProps}
 />
diff --git a/src/lib/components/ui/input/input-password.svelte b/src/lib/components/ui/input/input-password.svelte
index 6e2c1ab..d218e32 100644
--- a/src/lib/components/ui/input/input-password.svelte
+++ b/src/lib/components/ui/input/input-password.svelte
@@ -22,7 +22,7 @@
     type="button"
     variant="ghost"
     size="sm"
-    class="group absolute right-0 top-0 h-full !px-2 transition duration-200 hover:bg-transparent"
+    class="group absolute right-0 top-0 h-full !px-2 hover:bg-transparent"
     onclick={() => (showPassword = !showPassword)}
     disabled={btnDisabled}
   >
diff --git a/src/lib/components/ui/input/input.svelte b/src/lib/components/ui/input/input.svelte
index 0ea1f94..928297c 100644
--- a/src/lib/components/ui/input/input.svelte
+++ b/src/lib/components/ui/input/input.svelte
@@ -14,7 +14,7 @@
 <input
   bind:this={ref}
   class={cn(
-    "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid]:border-destructive md:text-sm",
+    "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid]:border-destructive",
     className
   )}
   bind:value
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 9f82bad..eeadde5 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -57,8 +57,8 @@
   <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-2xl">GNU/Weeb Mail Login</Card.Title>
-        <Card.Description>Proceed login to manager your email account</Card.Description>
+        <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>
 
         {#if isError() && !isCredentialInvalid()}
           <span class="text-sm font-medium text-destructive">
-- 
Muhammad Rizki


^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v1 08/17] chore(navigations): Replace index /settings url
  2025-03-05 14:39 [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs Muhammad Rizki
                   ` (6 preceding siblings ...)
  2025-03-05 14:40 ` [PATCH v1 07/17] chore(responsive): adjust styling Muhammad Rizki
@ 2025-03-05 14:40 ` Muhammad Rizki
  2025-03-05 14:40 ` [PATCH v1 09/17] feat(ui): Add popover and dialog UI component Muhammad Rizki
                   ` (11 subsequent siblings)
  19 siblings, 0 replies; 29+ messages in thread
From: Muhammad Rizki @ 2025-03-05 14:40 UTC (permalink / raw)
  To: Ammar Faizi
  Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List

Replace index /settings url with /settings/profile to ensure the page is
redirected properly.

Signed-off-by: Muhammad Rizki <[email protected]>
---
 src/lib/constants/navigations.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/lib/constants/navigations.ts b/src/lib/constants/navigations.ts
index 4266930..a23db84 100644
--- a/src/lib/constants/navigations.ts
+++ b/src/lib/constants/navigations.ts
@@ -11,7 +11,7 @@ export const navigations: typing.Navigations[] = [
   {
     name: "Settings",
     icon: Settings,
-    url: "/settings",
+    url: "/settings/profile",
     items: [
       {
         name: "Profile",
-- 
Muhammad Rizki


^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v1 09/17] feat(ui): Add popover and dialog UI component
  2025-03-05 14:39 [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs Muhammad Rizki
                   ` (7 preceding siblings ...)
  2025-03-05 14:40 ` [PATCH v1 08/17] chore(navigations): Replace index /settings url Muhammad Rizki
@ 2025-03-05 14:40 ` Muhammad Rizki
  2025-03-05 14:40 ` [PATCH v1 10/17] feat(http): Use PUBLIC_BASE_URL for each environment Muhammad Rizki
                   ` (10 subsequent siblings)
  19 siblings, 0 replies; 29+ messages in thread
From: Muhammad Rizki @ 2025-03-05 14:40 UTC (permalink / raw)
  To: Ammar Faizi
  Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List

Added popover and dialog UI, this commit include updates for bits-ui
version.

Signed-off-by: Muhammad Rizki <[email protected]>
---
 package-lock.json                             |  8 ++--
 package.json                                  |  2 +-
 .../ui/dialog/dialog-content.svelte           | 38 +++++++++++++++++++
 .../ui/dialog/dialog-description.svelte       | 16 ++++++++
 .../components/ui/dialog/dialog-footer.svelte | 20 ++++++++++
 .../components/ui/dialog/dialog-header.svelte | 20 ++++++++++
 .../ui/dialog/dialog-overlay.svelte           | 19 ++++++++++
 .../components/ui/dialog/dialog-title.svelte  | 16 ++++++++
 src/lib/components/ui/dialog/index.ts         | 37 ++++++++++++++++++
 src/lib/components/ui/popover/index.ts        | 17 +++++++++
 .../ui/popover/popover-content.svelte         | 28 ++++++++++++++
 11 files changed, 216 insertions(+), 5 deletions(-)
 create mode 100644 src/lib/components/ui/dialog/dialog-content.svelte
 create mode 100644 src/lib/components/ui/dialog/dialog-description.svelte
 create mode 100644 src/lib/components/ui/dialog/dialog-footer.svelte
 create mode 100644 src/lib/components/ui/dialog/dialog-header.svelte
 create mode 100644 src/lib/components/ui/dialog/dialog-overlay.svelte
 create mode 100644 src/lib/components/ui/dialog/dialog-title.svelte
 create mode 100644 src/lib/components/ui/dialog/index.ts
 create mode 100644 src/lib/components/ui/popover/index.ts
 create mode 100644 src/lib/components/ui/popover/popover-content.svelte

diff --git a/package-lock.json b/package-lock.json
index b9d8ba9..525092e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,7 +20,7 @@
         "@sveltejs/kit": "^2.0.0",
         "@sveltejs/vite-plugin-svelte": "^4.0.0",
         "autoprefixer": "^10.4.20",
-        "bits-ui": "^1.3.2",
+        "bits-ui": "^1.3.5",
         "clsx": "^2.1.1",
         "eslint": "^9.7.0",
         "eslint-config-prettier": "^9.1.0",
@@ -1898,9 +1898,9 @@
       }
     },
     "node_modules/bits-ui": {
-      "version": "1.3.2",
-      "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-1.3.2.tgz",
-      "integrity": "sha512-27fg/O71yqYmDaDf/opInylQYJjBNlFw3Ari0mXKLA6UrQpRvY62EsAid8s+ci/wYrGZMvHpzNJ69t15ycBHTw==",
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-1.3.5.tgz",
+      "integrity": "sha512-pfd8MK5Hp7bOvsW25LJrWVABmBIeAOH3g0pFPJLBIKlcqsaalEdBYejQmlSwrynEDX589aW3hTAWbhmWqRjjrQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
diff --git a/package.json b/package.json
index c323150..52b0e4c 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,7 @@
     "@sveltejs/kit": "^2.0.0",
     "@sveltejs/vite-plugin-svelte": "^4.0.0",
     "autoprefixer": "^10.4.20",
-    "bits-ui": "^1.3.2",
+    "bits-ui": "^1.3.5",
     "clsx": "^2.1.1",
     "eslint": "^9.7.0",
     "eslint-config-prettier": "^9.1.0",
diff --git a/src/lib/components/ui/dialog/dialog-content.svelte b/src/lib/components/ui/dialog/dialog-content.svelte
new file mode 100644
index 0000000..e0f8d9c
--- /dev/null
+++ b/src/lib/components/ui/dialog/dialog-content.svelte
@@ -0,0 +1,38 @@
+<script lang="ts">
+  import { Dialog as DialogPrimitive, type WithoutChildrenOrChild } from "bits-ui";
+  import X from "lucide-svelte/icons/x";
+  import type { Snippet } from "svelte";
+  import * as Dialog from "./index";
+  import { cn } from "$utils";
+
+  let {
+    ref = $bindable(null),
+    class: className,
+    portalProps,
+    children,
+    ...restProps
+  }: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
+    portalProps?: DialogPrimitive.PortalProps;
+    children: Snippet;
+  } = $props();
+</script>
+
+<Dialog.Portal {...portalProps}>
+  <Dialog.Overlay />
+  <DialogPrimitive.Content
+    bind:ref
+    class={cn(
+      "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
+      className
+    )}
+    {...restProps}
+  >
+    {@render children?.()}
+    <DialogPrimitive.Close
+      class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
+    >
+      <X class="size-4" />
+      <span class="sr-only">Close</span>
+    </DialogPrimitive.Close>
+  </DialogPrimitive.Content>
+</Dialog.Portal>
diff --git a/src/lib/components/ui/dialog/dialog-description.svelte b/src/lib/components/ui/dialog/dialog-description.svelte
new file mode 100644
index 0000000..adb4f44
--- /dev/null
+++ b/src/lib/components/ui/dialog/dialog-description.svelte
@@ -0,0 +1,16 @@
+<script lang="ts">
+  import { Dialog as DialogPrimitive } from "bits-ui";
+  import { cn } from "$utils";
+
+  let {
+    ref = $bindable(null),
+    class: className,
+    ...restProps
+  }: DialogPrimitive.DescriptionProps = $props();
+</script>
+
+<DialogPrimitive.Description
+  bind:ref
+  class={cn("text-sm text-muted-foreground", className)}
+  {...restProps}
+/>
diff --git a/src/lib/components/ui/dialog/dialog-footer.svelte b/src/lib/components/ui/dialog/dialog-footer.svelte
new file mode 100644
index 0000000..7695cd2
--- /dev/null
+++ b/src/lib/components/ui/dialog/dialog-footer.svelte
@@ -0,0 +1,20 @@
+<script lang="ts">
+  import type { WithElementRef } from "bits-ui";
+  import type { HTMLAttributes } from "svelte/elements";
+  import { cn } from "$utils";
+
+  let {
+    ref = $bindable(null),
+    class: className,
+    children,
+    ...restProps
+  }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
+</script>
+
+<div
+  bind:this={ref}
+  class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
+  {...restProps}
+>
+  {@render children?.()}
+</div>
diff --git a/src/lib/components/ui/dialog/dialog-header.svelte b/src/lib/components/ui/dialog/dialog-header.svelte
new file mode 100644
index 0000000..ec60291
--- /dev/null
+++ b/src/lib/components/ui/dialog/dialog-header.svelte
@@ -0,0 +1,20 @@
+<script lang="ts">
+  import type { HTMLAttributes } from "svelte/elements";
+  import type { WithElementRef } from "bits-ui";
+  import { cn } from "$utils";
+
+  let {
+    ref = $bindable(null),
+    class: className,
+    children,
+    ...restProps
+  }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
+</script>
+
+<div
+  bind:this={ref}
+  class={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
+  {...restProps}
+>
+  {@render children?.()}
+</div>
diff --git a/src/lib/components/ui/dialog/dialog-overlay.svelte b/src/lib/components/ui/dialog/dialog-overlay.svelte
new file mode 100644
index 0000000..b9b2cae
--- /dev/null
+++ b/src/lib/components/ui/dialog/dialog-overlay.svelte
@@ -0,0 +1,19 @@
+<script lang="ts">
+  import { Dialog as DialogPrimitive } from "bits-ui";
+  import { cn } from "$utils";
+
+  let {
+    ref = $bindable(null),
+    class: className,
+    ...restProps
+  }: DialogPrimitive.OverlayProps = $props();
+</script>
+
+<DialogPrimitive.Overlay
+  bind:ref
+  class={cn(
+    "fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
+    className
+  )}
+  {...restProps}
+/>
diff --git a/src/lib/components/ui/dialog/dialog-title.svelte b/src/lib/components/ui/dialog/dialog-title.svelte
new file mode 100644
index 0000000..15eae58
--- /dev/null
+++ b/src/lib/components/ui/dialog/dialog-title.svelte
@@ -0,0 +1,16 @@
+<script lang="ts">
+  import { Dialog as DialogPrimitive } from "bits-ui";
+  import { cn } from "$utils";
+
+  let {
+    ref = $bindable(null),
+    class: className,
+    ...restProps
+  }: DialogPrimitive.TitleProps = $props();
+</script>
+
+<DialogPrimitive.Title
+  bind:ref
+  class={cn("text-lg font-semibold leading-none tracking-tight", className)}
+  {...restProps}
+/>
diff --git a/src/lib/components/ui/dialog/index.ts b/src/lib/components/ui/dialog/index.ts
new file mode 100644
index 0000000..5126e26
--- /dev/null
+++ b/src/lib/components/ui/dialog/index.ts
@@ -0,0 +1,37 @@
+import { Dialog as DialogPrimitive } from "bits-ui";
+
+import Title from "./dialog-title.svelte";
+import Footer from "./dialog-footer.svelte";
+import Header from "./dialog-header.svelte";
+import Overlay from "./dialog-overlay.svelte";
+import Content from "./dialog-content.svelte";
+import Description from "./dialog-description.svelte";
+
+const Root: typeof DialogPrimitive.Root = DialogPrimitive.Root;
+const Trigger: typeof DialogPrimitive.Trigger = DialogPrimitive.Trigger;
+const Close: typeof DialogPrimitive.Close = DialogPrimitive.Close;
+const Portal: typeof DialogPrimitive.Portal = DialogPrimitive.Portal;
+
+export {
+  Root,
+  Title,
+  Portal,
+  Footer,
+  Header,
+  Trigger,
+  Overlay,
+  Content,
+  Description,
+  Close,
+  //
+  Root as Dialog,
+  Title as DialogTitle,
+  Portal as DialogPortal,
+  Footer as DialogFooter,
+  Header as DialogHeader,
+  Trigger as DialogTrigger,
+  Overlay as DialogOverlay,
+  Content as DialogContent,
+  Description as DialogDescription,
+  Close as DialogClose
+};
diff --git a/src/lib/components/ui/popover/index.ts b/src/lib/components/ui/popover/index.ts
new file mode 100644
index 0000000..e5a8bc6
--- /dev/null
+++ b/src/lib/components/ui/popover/index.ts
@@ -0,0 +1,17 @@
+import { Popover as PopoverPrimitive } from "bits-ui";
+import Content from "./popover-content.svelte";
+const Root = PopoverPrimitive.Root;
+const Trigger = PopoverPrimitive.Trigger;
+const Close = PopoverPrimitive.Close;
+
+export {
+  Root,
+  Content,
+  Trigger,
+  Close,
+  //
+  Root as Popover,
+  Content as PopoverContent,
+  Trigger as PopoverTrigger,
+  Close as PopoverClose
+};
diff --git a/src/lib/components/ui/popover/popover-content.svelte b/src/lib/components/ui/popover/popover-content.svelte
new file mode 100644
index 0000000..11cf914
--- /dev/null
+++ b/src/lib/components/ui/popover/popover-content.svelte
@@ -0,0 +1,28 @@
+<script lang="ts">
+  import { cn } from "$utils";
+  import { Popover as PopoverPrimitive } from "bits-ui";
+
+  let {
+    ref = $bindable(null),
+    class: className,
+    align = "center",
+    sideOffset = 4,
+    portalProps,
+    ...restProps
+  }: PopoverPrimitive.ContentProps & {
+    portalProps?: PopoverPrimitive.PortalProps;
+  } = $props();
+</script>
+
+<PopoverPrimitive.Portal {...portalProps}>
+  <PopoverPrimitive.Content
+    bind:ref
+    {align}
+    {sideOffset}
+    class={cn(
+      "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
+      className
+    )}
+    {...restProps}
+  />
+</PopoverPrimitive.Portal>
-- 
Muhammad Rizki


^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v1 10/17] feat(http): Use PUBLIC_BASE_URL for each environment
  2025-03-05 14:39 [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs Muhammad Rizki
                   ` (8 preceding siblings ...)
  2025-03-05 14:40 ` [PATCH v1 09/17] feat(ui): Add popover and dialog UI component Muhammad Rizki
@ 2025-03-05 14:40 ` Muhammad Rizki
  2025-03-05 14:40 ` [PATCH v1 11/17] feat(icons): Add social icons Muhammad Rizki
                   ` (9 subsequent siblings)
  19 siblings, 0 replies; 29+ messages in thread
From: Muhammad Rizki @ 2025-03-05 14:40 UTC (permalink / raw)
  To: Ammar Faizi
  Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List

Use PUBLIC_BASE_URL to easily control the base URL API for each
environment.

Signed-off-by: Muhammad Rizki <[email protected]>
---
 .env.development.example     | 2 ++
 .env.production.example      | 2 ++
 .gitignore                   | 2 +-
 src/lib/hooks/http.svelte.ts | 3 ++-
 4 files changed, 7 insertions(+), 2 deletions(-)
 create mode 100644 .env.development.example
 create mode 100644 .env.production.example

diff --git a/.env.development.example b/.env.development.example
new file mode 100644
index 0000000..882db7f
--- /dev/null
+++ b/.env.development.example
@@ -0,0 +1,2 @@
+# remove .example filename suffix
+PUBLIC_BASE_URL="https://mail-staging.gnuweeb.org/api.php"
diff --git a/.env.production.example b/.env.production.example
new file mode 100644
index 0000000..882db7f
--- /dev/null
+++ b/.env.production.example
@@ -0,0 +1,2 @@
+# remove .example filename suffix
+PUBLIC_BASE_URL="https://mail-staging.gnuweeb.org/api.php"
diff --git a/.gitignore b/.gitignore
index 1ea8d04..48aea9e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,7 +27,7 @@ Thumbs.db
 # Env
 .env
 .env.*
-!.env.example
+!.env.*.example
 !.env.test
 
 # Vite
diff --git a/src/lib/hooks/http.svelte.ts b/src/lib/hooks/http.svelte.ts
index 78273a1..d3445d9 100644
--- a/src/lib/hooks/http.svelte.ts
+++ b/src/lib/hooks/http.svelte.ts
@@ -1,3 +1,4 @@
+import { PUBLIC_BASE_URL } from "$env/static/public";
 import * as typing from "$typings";
 import axios from "axios";
 import type {
@@ -8,7 +9,7 @@ import type {
 } from "axios";
 
 const client = axios.create({
-  baseURL: "https://mail.gnuweeb.org/api.php"
+  baseURL: PUBLIC_BASE_URL
 });
 
 const http = async <T>(prop: AxiosRequestConfig) => {
-- 
Muhammad Rizki


^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v1 11/17] feat(icons): Add social icons
  2025-03-05 14:39 [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs Muhammad Rizki
                   ` (9 preceding siblings ...)
  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 ` Muhammad Rizki
  2025-03-05 14:40 ` [PATCH v1 12/17] feat(typing/enum): add Gender and IsActive enum Muhammad Rizki
                   ` (8 subsequent siblings)
  19 siblings, 0 replies; 29+ messages in thread
From: Muhammad Rizki @ 2025-03-05 14:40 UTC (permalink / raw)
  To: Ammar Faizi
  Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List

This social icons will be used in the profile form.

Signed-off-by: Muhammad Rizki <[email protected]>
---
 src/lib/components/icons/icon-discord.svelte  | 35 +++++++++++++++++++
 src/lib/components/icons/icon-github.svelte   | 35 +++++++++++++++++++
 src/lib/components/icons/icon-telegram.svelte | 35 +++++++++++++++++++
 src/lib/components/icons/icon-twitter.svelte  | 35 +++++++++++++++++++
 4 files changed, 140 insertions(+)
 create mode 100644 src/lib/components/icons/icon-discord.svelte
 create mode 100644 src/lib/components/icons/icon-github.svelte
 create mode 100644 src/lib/components/icons/icon-telegram.svelte
 create mode 100644 src/lib/components/icons/icon-twitter.svelte

diff --git a/src/lib/components/icons/icon-discord.svelte b/src/lib/components/icons/icon-discord.svelte
new file mode 100644
index 0000000..7f9e955
--- /dev/null
+++ b/src/lib/components/icons/icon-discord.svelte
@@ -0,0 +1,35 @@
+<script lang="ts">
+  import type { HTMLAttributes } from "svelte/elements";
+
+  let { href, class: className }: HTMLAttributes<SVGElement> & { href?: string } = $props();
+</script>
+
+{#if href}
+  <a {href} aria-label="Social Icon" target="_blank" rel="noopener noreferrer">
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      width="24"
+      height="24"
+      viewBox="0 0 24 24"
+      class={className}
+    >
+      <path
+        fill="currentColor"
+        d="M20.317 4.37a19.8 19.8 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.3 18.3 0 0 0-5.487 0a13 13 0 0 0-.617-1.25a.08.08 0 0 0-.079-.037A19.7 19.7 0 0 0 3.677 4.37a.1.1 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.08.08 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.08.08 0 0 0 .084-.028a14 14 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13 13 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10 10 0 0 0 .372-.292a.07.07 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.07.07 0 0 1 .078.01q.181.149.373.292a.077.077 0 0 1-.006.127a12.3 12.3 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.08.08 0 0 0 .084.028a19.8 19.8 0 0 0 6.002-3.03a.08.08 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.06.06 0 0 0-.031-.03M8.02 15.33c-1.182 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418m7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418"
+      />
+    </svg>
+  </a>
+{:else}
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    width="24"
+    height="24"
+    viewBox="0 0 24 24"
+    class={className}
+  >
+    <path
+      fill="currentColor"
+      d="M20.317 4.37a19.8 19.8 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.3 18.3 0 0 0-5.487 0a13 13 0 0 0-.617-1.25a.08.08 0 0 0-.079-.037A19.7 19.7 0 0 0 3.677 4.37a.1.1 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.08.08 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.08.08 0 0 0 .084-.028a14 14 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13 13 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10 10 0 0 0 .372-.292a.07.07 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.07.07 0 0 1 .078.01q.181.149.373.292a.077.077 0 0 1-.006.127a12.3 12.3 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.08.08 0 0 0 .084.028a19.8 19.8 0 0 0 6.002-3.03a.08.08 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.06.06 0 0 0-.031-.03M8.02 15.33c-1.182 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418m7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418"
+    />
+  </svg>
+{/if}
diff --git a/src/lib/components/icons/icon-github.svelte b/src/lib/components/icons/icon-github.svelte
new file mode 100644
index 0000000..9a323d3
--- /dev/null
+++ b/src/lib/components/icons/icon-github.svelte
@@ -0,0 +1,35 @@
+<script lang="ts">
+  import type { HTMLAttributes } from "svelte/elements";
+
+  let { href, class: className }: HTMLAttributes<SVGElement> & { href?: string } = $props();
+</script>
+
+{#if href}
+  <a {href} aria-label="Social Icon" target="_blank" rel="noopener noreferrer">
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      width="24"
+      height="24"
+      viewBox="0 0 24 24"
+      class={className}
+    >
+      <path
+        fill="currentColor"
+        d="M12 .297c-6.63 0-12 5.373-12 12c0 5.303 3.438 9.8 8.205 11.385c.6.113.82-.258.82-.577c0-.285-.01-1.04-.015-2.04c-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729c1.205.084 1.838 1.236 1.838 1.236c1.07 1.835 2.809 1.305 3.495.998c.108-.776.417-1.305.76-1.605c-2.665-.3-5.466-1.332-5.466-5.93c0-1.31.465-2.38 1.235-3.22c-.135-.303-.54-1.523.105-3.176c0 0 1.005-.322 3.3 1.23c.96-.267 1.98-.399 3-.405c1.02.006 2.04.138 3 .405c2.28-1.552 3.285-1.23 3.285-1.23c.645 1.653.24 2.873.12 3.176c.765.84 1.23 1.91 1.23 3.22c0 4.61-2.805 5.625-5.475 5.92c.42.36.81 1.096.81 2.22c0 1.606-.015 2.896-.015 3.286c0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
+      />
+    </svg>
+  </a>
+{:else}
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    width="24"
+    height="24"
+    viewBox="0 0 24 24"
+    class={className}
+  >
+    <path
+      fill="currentColor"
+      d="M12 .297c-6.63 0-12 5.373-12 12c0 5.303 3.438 9.8 8.205 11.385c.6.113.82-.258.82-.577c0-.285-.01-1.04-.015-2.04c-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729c1.205.084 1.838 1.236 1.838 1.236c1.07 1.835 2.809 1.305 3.495.998c.108-.776.417-1.305.76-1.605c-2.665-.3-5.466-1.332-5.466-5.93c0-1.31.465-2.38 1.235-3.22c-.135-.303-.54-1.523.105-3.176c0 0 1.005-.322 3.3 1.23c.96-.267 1.98-.399 3-.405c1.02.006 2.04.138 3 .405c2.28-1.552 3.285-1.23 3.285-1.23c.645 1.653.24 2.873.12 3.176c.765.84 1.23 1.91 1.23 3.22c0 4.61-2.805 5.625-5.475 5.92c.42.36.81 1.096.81 2.22c0 1.606-.015 2.896-.015 3.286c0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
+    />
+  </svg>
+{/if}
diff --git a/src/lib/components/icons/icon-telegram.svelte b/src/lib/components/icons/icon-telegram.svelte
new file mode 100644
index 0000000..7e9982d
--- /dev/null
+++ b/src/lib/components/icons/icon-telegram.svelte
@@ -0,0 +1,35 @@
+<script lang="ts">
+  import type { HTMLAttributes } from "svelte/elements";
+
+  let { href, class: className }: HTMLAttributes<SVGElement> & { href?: string } = $props();
+</script>
+
+{#if href}
+  <a {href} aria-label="Social Icon" target="_blank" rel="noopener noreferrer">
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      width="24"
+      height="24"
+      viewBox="0 0 24 24"
+      class={className}
+    >
+      <path
+        fill="currentColor"
+        d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12a12 12 0 0 0 12-12A12 12 0 0 0 12 0zm4.962 7.224c.1-.002.321.023.465.14a.5.5 0 0 1 .171.325c.016.093.036.306.02.472c-.18 1.898-.962 6.502-1.36 8.627c-.168.9-.499 1.201-.82 1.23c-.696.065-1.225-.46-1.9-.902c-1.056-.693-1.653-1.124-2.678-1.8c-1.185-.78-.417-1.21.258-1.91c.177-.184 3.247-2.977 3.307-3.23c.007-.032.014-.15-.056-.212s-.174-.041-.249-.024q-.159.037-5.061 3.345q-.72.495-1.302.48c-.428-.008-1.252-.241-1.865-.44c-.752-.245-1.349-.374-1.297-.789q.04-.324.893-.663q5.247-2.286 6.998-3.014c3.332-1.386 4.025-1.627 4.476-1.635"
+      />
+    </svg>
+  </a>
+{:else}
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    width="24"
+    height="24"
+    viewBox="0 0 24 24"
+    class={className}
+  >
+    <path
+      fill="currentColor"
+      d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12a12 12 0 0 0 12-12A12 12 0 0 0 12 0zm4.962 7.224c.1-.002.321.023.465.14a.5.5 0 0 1 .171.325c.016.093.036.306.02.472c-.18 1.898-.962 6.502-1.36 8.627c-.168.9-.499 1.201-.82 1.23c-.696.065-1.225-.46-1.9-.902c-1.056-.693-1.653-1.124-2.678-1.8c-1.185-.78-.417-1.21.258-1.91c.177-.184 3.247-2.977 3.307-3.23c.007-.032.014-.15-.056-.212s-.174-.041-.249-.024q-.159.037-5.061 3.345q-.72.495-1.302.48c-.428-.008-1.252-.241-1.865-.44c-.752-.245-1.349-.374-1.297-.789q.04-.324.893-.663q5.247-2.286 6.998-3.014c3.332-1.386 4.025-1.627 4.476-1.635"
+    />
+  </svg>
+{/if}
diff --git a/src/lib/components/icons/icon-twitter.svelte b/src/lib/components/icons/icon-twitter.svelte
new file mode 100644
index 0000000..a2162ec
--- /dev/null
+++ b/src/lib/components/icons/icon-twitter.svelte
@@ -0,0 +1,35 @@
+<script lang="ts">
+  import type { HTMLAttributes } from "svelte/elements";
+
+  let { href, class: className }: HTMLAttributes<SVGElement> & { href?: string } = $props();
+</script>
+
+{#if href}
+  <a {href} aria-label="Social Icon" target="_blank" rel="noopener noreferrer">
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      width="24"
+      height="24"
+      viewBox="0 0 24 24"
+      class={className}
+    >
+      <path
+        fill="currentColor"
+        d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584l-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
+      />
+    </svg>
+  </a>
+{:else}
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    width="24"
+    height="24"
+    viewBox="0 0 24 24"
+    class={className}
+  >
+    <path
+      fill="currentColor"
+      d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584l-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
+    />
+  </svg>
+{/if}
-- 
Muhammad Rizki


^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v1 12/17] feat(typing/enum): add Gender and IsActive enum
  2025-03-05 14:39 [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs Muhammad Rizki
                   ` (10 preceding siblings ...)
  2025-03-05 14:40 ` [PATCH v1 11/17] feat(icons): Add social icons Muhammad Rizki
@ 2025-03-05 14:40 ` Muhammad Rizki
  2025-03-05 14:40 ` [PATCH v1 13/17] refactor!:feat: update API response structure, update profile page Muhammad Rizki
                   ` (7 subsequent siblings)
  19 siblings, 0 replies; 29+ messages in thread
From: Muhammad Rizki @ 2025-03-05 14:40 UTC (permalink / raw)
  To: Ammar Faizi
  Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List

Added Gender and IsActive enum to make it readable and increase
developer experience.

Signed-off-by: Muhammad Rizki <[email protected]>
---
 src/lib/typings/common.d.ts | 10 ++++++++++
 src/lib/typings/index.ts    | 11 ++++++++++-
 2 files changed, 20 insertions(+), 1 deletion(-)

diff --git a/src/lib/typings/common.d.ts b/src/lib/typings/common.d.ts
index 1f4a795..6aaf661 100644
--- a/src/lib/typings/common.d.ts
+++ b/src/lib/typings/common.d.ts
@@ -2,6 +2,16 @@ import type { Icon as IconType } from "lucide-svelte";
 
 export type RecordString = Record<string, string>;
 
+export enum Gender {
+  MALE = "m",
+  FEMALE = "f"
+}
+
+export enum IsActive {
+  ACTIVE = "1",
+  INACTIVE = "0"
+}
+
 export interface Navigations {
   name: string;
   description?: string;
diff --git a/src/lib/typings/index.ts b/src/lib/typings/index.ts
index 1855c2b..7f0c04d 100644
--- a/src/lib/typings/index.ts
+++ b/src/lib/typings/index.ts
@@ -1,8 +1,17 @@
-import type { RecordString, Navigations, LabelAndValue, MailConfig } from "./common";
+import type {
+  RecordString,
+  Navigations,
+  LabelAndValue,
+  MailConfig,
+  Gender,
+  IsActive
+} from "./common";
 import type { ResponseAPI, LoginResponse, RenewTokenResponse } from "./http";
 import type { User } from "./credential";
 
 export type {
+  Gender,
+  IsActive,
   RecordString,
   Navigations,
   LabelAndValue,
-- 
Muhammad Rizki


^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v1 13/17] refactor!:feat: update API response structure, update profile page
  2025-03-05 14:39 [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs Muhammad Rizki
                   ` (11 preceding siblings ...)
  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
  2025-03-05 14:40 ` [PATCH v1 14/17] chore(meta): rename favicon.png to favicon.ico Muhammad Rizki
                   ` (6 subsequent siblings)
  19 siblings, 0 replies; 29+ messages in thread
From: Muhammad Rizki @ 2025-03-05 14:40 UTC (permalink / raw)
  To: Ammar Faizi
  Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List

- 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


^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v1 14/17] chore(meta): rename favicon.png to favicon.ico
  2025-03-05 14:39 [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs Muhammad Rizki
                   ` (12 preceding siblings ...)
  2025-03-05 14:40 ` [PATCH v1 13/17] refactor!:feat: update API response structure, update profile page Muhammad Rizki
@ 2025-03-05 14:40 ` Muhammad Rizki
  2025-03-05 14:40 ` [PATCH v1 15/17] feat(seo): add SEO for site metadata Muhammad Rizki
                   ` (5 subsequent siblings)
  19 siblings, 0 replies; 29+ messages in thread
From: Muhammad Rizki @ 2025-03-05 14:40 UTC (permalink / raw)
  To: Ammar Faizi
  Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List

use .ico extension instead of .png for standard favicon file format.

Signed-off-by: Muhammad Rizki <[email protected]>
---
 src/app.html                        |   2 +-
 static/{favicon.png => favicon.ico} | Bin
 2 files changed, 1 insertion(+), 1 deletion(-)
 rename static/{favicon.png => favicon.ico} (100%)

diff --git a/src/app.html b/src/app.html
index 84ffad1..564d2da 100644
--- a/src/app.html
+++ b/src/app.html
@@ -2,7 +2,7 @@
 <html lang="en">
   <head>
     <meta charset="utf-8" />
-    <link rel="icon" href="%sveltekit.assets%/favicon.png" />
+    <link rel="icon" href="%sveltekit.assets%/favicon.ico" />
     <meta name="viewport" content="width=device-width, initial-scale=1" />
     %sveltekit.head%
   </head>
diff --git a/static/favicon.png b/static/favicon.ico
similarity index 100%
rename from static/favicon.png
rename to static/favicon.ico
-- 
Muhammad Rizki


^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v1 15/17] feat(seo): add SEO for site metadata
  2025-03-05 14:39 [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs Muhammad Rizki
                   ` (13 preceding siblings ...)
  2025-03-05 14:40 ` [PATCH v1 14/17] chore(meta): rename favicon.png to favicon.ico Muhammad Rizki
@ 2025-03-05 14:40 ` Muhammad Rizki
  2025-03-05 14:40 ` [PATCH v1 16/17] chore(login): use $derived() instead of function based Muhammad Rizki
                   ` (4 subsequent siblings)
  19 siblings, 0 replies; 29+ messages in thread
From: Muhammad Rizki @ 2025-03-05 14:40 UTC (permalink / raw)
  To: Ammar Faizi
  Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List

Added SEO component to handle site metadata for better SEO.

Signed-off-by: Muhammad Rizki <[email protected]>
---
 src/lib/components/customs/seo.svelte         | 27 +++++++++++++++++++
 src/routes/(protected)/+layout.svelte         | 12 +++++++++
 .../(protected)/settings/account/+page.svelte |  3 +++
 .../(protected)/settings/profile/+page.svelte |  3 +++
 src/routes/+page.svelte                       |  3 +++
 5 files changed, 48 insertions(+)
 create mode 100644 src/lib/components/customs/seo.svelte

diff --git a/src/lib/components/customs/seo.svelte b/src/lib/components/customs/seo.svelte
new file mode 100644
index 0000000..34f77ac
--- /dev/null
+++ b/src/lib/components/customs/seo.svelte
@@ -0,0 +1,27 @@
+<script lang="ts">
+  interface Props {
+    title: string;
+    description: string;
+    image?: string;
+  }
+
+  let { title, description, image }: Props = $props();
+
+  const META_IMG = image ?? "/favicon.ico";
+</script>
+
+<svelte:head>
+  <title>{title}</title>
+  <meta name="description" content={description} />
+  <meta property="og:type" content="website" />
+  <meta property="og:title" content={title} />
+  <meta property="og:description" content={description} />
+  <meta property="og:image" content={META_IMG} />
+  <meta property="og:image:alt" content={META_IMG} />
+  <meta name="twitter:card" content="summary_large_image" />
+  <meta name="twitter:title" content={title} />
+  <meta name="twitter:description" content={description} />
+  <meta name="twitter:image" content={META_IMG} />
+  <meta name="twitter:image:alt" content={META_IMG} />
+  <meta name="robots" content="index, follow" />
+</svelte:head>
diff --git a/src/routes/(protected)/+layout.svelte b/src/routes/(protected)/+layout.svelte
index e88b3fb..ec934be 100644
--- a/src/routes/(protected)/+layout.svelte
+++ b/src/routes/(protected)/+layout.svelte
@@ -3,10 +3,22 @@
   import AppSidebar from "$components/customs/app-sidebar.svelte";
   import Header from "$components/customs/header.svelte";
   import Separator from "$components/ui/separator/separator.svelte";
+  import Seo from "$components/customs/seo.svelte";
+  import { useAuth } from "$lib/hooks/auth.svelte";
 
   let { children } = $props();
+
+  const auth = useAuth();
 </script>
 
+{#if auth.user && auth.token}
+  <Seo
+    title="{auth.user?.full_name} ({auth.user?.username}) - GNU/Weeb Mail"
+    description="Configure your email client using this config"
+    image={auth.user?.photo}
+  />
+{/if}
+
 <Sidebar.Provider class="light">
   <AppSidebar />
 
diff --git a/src/routes/(protected)/settings/account/+page.svelte b/src/routes/(protected)/settings/account/+page.svelte
index 29ccd06..e1ba4c6 100644
--- a/src/routes/(protected)/settings/account/+page.svelte
+++ b/src/routes/(protected)/settings/account/+page.svelte
@@ -8,6 +8,7 @@
   import Button from "$components/ui/button/button.svelte";
   import http from "$lib/hooks/http.svelte";
   import Separator from "$components/ui/separator/separator.svelte";
+  import Seo from "$components/customs/seo.svelte";
 
   let { data } = $props();
 
@@ -50,6 +51,8 @@
   );
 </script>
 
+<Seo title="Account settings - GNU/Weeb Mail" description="Update your account." />
+
 <div class="space-y-3">
   <h2 class="text-center font-medium sm:text-start">Change Password</h2>
   <Separator />
diff --git a/src/routes/(protected)/settings/profile/+page.svelte b/src/routes/(protected)/settings/profile/+page.svelte
index b91382a..b61601b 100644
--- a/src/routes/(protected)/settings/profile/+page.svelte
+++ b/src/routes/(protected)/settings/profile/+page.svelte
@@ -21,6 +21,7 @@
   import * as typing from "$typings";
   import { toast } from "svelte-sonner";
   import InputPassword from "$components/ui/input/input-password.svelte";
+  import Seo from "$components/customs/seo.svelte";
 
   let { data } = $props();
   let showModalConfirmation = $state(false);
@@ -143,6 +144,8 @@
   const isError = $derived(Boolean($errors.full_name || $errors.ext_email || $errors.gender));
 </script>
 
+<Seo title="Your profile - GNU/Weeb Mail" description="Update your profile." />
+
 <Dialog.Root open={showModalConfirmation} onOpenChange={handleOpenModal}>
   <form use:enhance class="flex flex-col gap-5" enctype="multipart/form-data">
     <div
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index cb4f673..f6820bc 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -1,5 +1,6 @@
 <script lang="ts">
   import Loading from "$components/customs/loading.svelte";
+  import Seo from "$components/customs/seo.svelte";
   import { Button } from "$components/ui/button";
   import * as Card from "$components/ui/card";
   import * as Form from "$components/ui/form";
@@ -56,6 +57,8 @@
   });
 </script>
 
+<Seo title="Login - GNU/Weeb Mail" description="Update your profile." />
+
 <div class="mx-auto flex min-h-screen w-full items-center justify-center px-3 py-2">
   <Card.Root class="w-full max-w-lg">
     <form method="POST" use:enhance>
-- 
Muhammad Rizki


^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v1 16/17] chore(login): use $derived() instead of function based
  2025-03-05 14:39 [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs Muhammad Rizki
                   ` (14 preceding siblings ...)
  2025-03-05 14:40 ` [PATCH v1 15/17] feat(seo): add SEO for site metadata Muhammad Rizki
@ 2025-03-05 14:40 ` Muhammad Rizki
  2025-03-05 14:40 ` [PATCH v1 17/17] docs: update README.md Muhammad Rizki
                   ` (3 subsequent siblings)
  19 siblings, 0 replies; 29+ messages in thread
From: Muhammad Rizki @ 2025-03-05 14:40 UTC (permalink / raw)
  To: Ammar Faizi
  Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List

this usage is the same as previous method.

Signed-off-by: Muhammad Rizki <[email protected]>
---
 src/routes/+page.svelte | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index f6820bc..b2c2874 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -45,14 +45,14 @@
     }
   });
 
-  const isError = () => Boolean($errors.username_or_email && $errors.password);
-  const isValid = () => Boolean($formData.username_or_email && $formData.password);
-  const isCredentialInvalid = () => Boolean(data.isInvalidCreds && +data.isInvalidCreds);
-
   const { form: formData, errors, message, submitting, constraints, enhance } = form;
 
+  const isError = $derived(Boolean($errors.username_or_email && $errors.password));
+  const isValid = $derived(Boolean($formData.username_or_email && $formData.password));
+  const isCredentialInvalid = $derived(Boolean(data.isInvalidCreds && +data.isInvalidCreds));
+
   onMount(() => {
-    if (!isCredentialInvalid()) return;
+    if (!isCredentialInvalid) return;
     localStorage.removeItem("gwm_invalid_creds");
   });
 </script>
@@ -68,13 +68,13 @@
           Proceed login to manage your email account
         </Card.Description>
 
-        {#if isError() && !isCredentialInvalid()}
+        {#if isError && !isCredentialInvalid}
           <span class="text-sm font-medium text-destructive">
             {$message}
           </span>
         {/if}
 
-        {#if !isError() && isCredentialInvalid()}
+        {#if !isError && isCredentialInvalid}
           <span class="text-sm font-medium text-destructive">
             Invalid credential, please login again.
           </span>
@@ -121,7 +121,7 @@
         <Button
           type="submit"
           class="mt-3 flex w-full gap-x-2"
-          disabled={$submitting || !isValid() || isError()}
+          disabled={$submitting || !isValid || isError}
         >
           <span>Login</span>
           {#if $submitting}
-- 
Muhammad Rizki


^ permalink raw reply related	[flat|nested] 29+ messages in thread

* [PATCH v1 17/17] docs: update README.md
  2025-03-05 14:39 [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs Muhammad Rizki
                   ` (15 preceding siblings ...)
  2025-03-05 14:40 ` [PATCH v1 16/17] chore(login): use $derived() instead of function based Muhammad Rizki
@ 2025-03-05 14:40 ` Muhammad Rizki
  2025-03-05 16:54 ` [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs Ammar Faizi
                   ` (2 subsequent siblings)
  19 siblings, 0 replies; 29+ messages in thread
From: Muhammad Rizki @ 2025-03-05 14:40 UTC (permalink / raw)
  To: Ammar Faizi
  Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List

add API examples section to README.md

Signed-off-by: Muhammad Rizki <[email protected]>
---
 README.md | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/README.md b/README.md
index 7b63918..9215a35 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,7 @@
 - [How to Build](#how-to-build)
 - [Code Standards](#code-standards)
 - [How to Develop or Contribute](#how-to-develop)
+- [API examples](#api-examples)
 
 <h2 id="requirements">
   Requirements
@@ -109,3 +110,9 @@ if you want to create styles or CSS that affects globally you can write it in [.
 <h3>Aliases</h3>
 
 You may add some import aliases, you can edit it in [svelte.config.js](./svelte.config.js), we used `$` as import alias, you must follow our standards to remain consistency.
+
+<h2 id="api-examples">
+API Examples
+</h2>
+
+You can view all API examples in our Postman workspace [here](https://gnuweeb.postman.co/workspace/GNU%252FWeeb~2805fbdd-376b-42de-8fe1-2a4cfa22ed3c/overview?ctx=updates).
-- 
Muhammad Rizki


^ permalink raw reply related	[flat|nested] 29+ messages in thread

* Re: [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs
  2025-03-05 14:39 [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs Muhammad Rizki
                   ` (16 preceding siblings ...)
  2025-03-05 14:40 ` [PATCH v1 17/17] docs: update README.md Muhammad Rizki
@ 2025-03-05 16:54 ` Ammar Faizi
  2025-03-05 16:57   ` Alviro Iskandar Setiawan
  2025-03-06  2:02   ` Muhammad Rizki
  2025-03-05 17:04 ` Ammar Faizi
  2025-03-05 18:14 ` Alviro Iskandar Setiawan
  19 siblings, 2 replies; 29+ messages in thread
From: Ammar Faizi @ 2025-03-05 16:54 UTC (permalink / raw)
  To: Muhammad Rizki; +Cc: Alviro Iskandar Setiawan, GNU/Weeb Mailing List

On Wed, Mar 05, 2025 at 09:39:59PM +0700, Muhammad Rizki wrote:
> This series of patches was planned only to update profile page feature,
> since there is a change in the API structure, I also fixed
> the API response type to prevent breaking change.
> 
> To prevent more breaking change on production site in the future,
> sir Ammar has made a staging API for development stage,
> since there is a staging API version, I added an environment variable
> `PUBLIC_BASE_URL` for each environment (development or production).
> You should set `PUBLIC_BASE_URL` environment in your own server,
> there is an example file for development and production environment
> in this patch, please see `.env.development.example` and
> `.env.production.example` in root directory.

So, in my testing, the assets loading process is still broken. I had to
reapply this patch manually to get it working:

diff --git a/svelte.config.js b/svelte.config.js
index 5a6890a..40dc905 100644
--- a/svelte.config.js
+++ b/svelte.config.js
@@ -5,6 +5,7 @@ import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
 const config = {
   preprocess: vitePreprocess(),
   kit: {
+    paths: { relative: false },
     adapter: adapter({
       pages: "dist",
       assets: "dist",

That's fine. Also, I found that the delete profile photo button does
not work.

Please send follow up patches for these two problems:

1) Use 'paths: { relative: false }' to make it work on nginx.

2) Fix the 'delete' button on photo profile.

Don't send a v2 revision, just two new patches. This series is good to
be applied, just a bit incomplete...

-- 
Ammar Faizi


^ permalink raw reply related	[flat|nested] 29+ messages in thread

* Re: [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs
  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  2:02   ` Muhammad Rizki
  1 sibling, 1 reply; 29+ messages in thread
From: Alviro Iskandar Setiawan @ 2025-03-05 16:57 UTC (permalink / raw)
  To: Ammar Faizi; +Cc: Muhammad Rizki, GNU/Weeb Mailing List

On Wed, Mar 5, 2025 at 11:54 PM Ammar Faizi wrote:
> Don't send a v2 revision, just two new patches. This series is good to
> be applied, just a bit incomplete...

good then:

Acked-by: Alviro Iskandar Setiawan <[email protected]>

Sorry I did not review, just an ack should be ok. Tq

-- Viro

^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs
  2025-03-05 14:39 [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs Muhammad Rizki
                   ` (17 preceding siblings ...)
  2025-03-05 16:54 ` [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs Ammar Faizi
@ 2025-03-05 17:04 ` Ammar Faizi
  2025-03-05 18:14 ` Alviro Iskandar Setiawan
  19 siblings, 0 replies; 29+ messages in thread
From: Ammar Faizi @ 2025-03-05 17:04 UTC (permalink / raw)
  To: Muhammad Rizki
  Cc: Ammar Faizi, GNU/Weeb Mailing List, Alviro Iskandar Setiawan

On Wed, 5 Mar 2025 21:39:59 +0700, Muhammad Rizki wrote:
> This series of patches was planned only to update profile page feature,
> since there is a change in the API structure, I also fixed
> the API response type to prevent breaking change.
> 
> To prevent more breaking change on production site in the future,
> sir Ammar has made a staging API for development stage,
> since there is a staging API version, I added an environment variable
> `PUBLIC_BASE_URL` for each environment (development or production).
> You should set `PUBLIC_BASE_URL` environment in your own server,
> there is an example file for development and production environment
> in this patch, please see `.env.development.example` and
> `.env.production.example` in root directory.
> 
> [...]

Applied, thanks!

[01/17] fix(typing): add user_info type prop
        commit: b418f03c8549d70b31eb8c40b3cd1063d7173f7b
[02/17] refactor: optimize icon imports to reduce bundle size
        commit: a98e3b42c132562071f68ca8b6324e18e4576746
[03/17] chore(change-pwd): adjust change password heading styling
        commit: a9d7e99ef66e443611e6a92227f44aec461fd1ba
[04/17] chore(settings/layout): use prose: for heading styling
        commit: 3d886a3f1422957f68564c5757662a371962a8f2
[05/17] fix(profile): fix edit avatar button position
        commit: b646cd12f50fde6b3e2eb56764846fd72cf24dc6
[06/17] fix(breadcrumb): Move settingsNav to settings items navigations
        commit: 060dac3c1e49c31d27f30b685c03e8d5718bdde0
[07/17] chore(responsive): adjust styling
        commit: 3ea26074d3ba4e8402d3d1982108abf91a310fb9
[08/17] chore(navigations): Replace index /settings url
        commit: c756763c9244970037ab9fd8f5d2b6d4fcc0724c
[09/17] feat(ui): Add popover and dialog UI component
        commit: a669ffb112fb1c9eefaecd3dcb362e226695f0c6
[10/17] feat(http): Use PUBLIC_BASE_URL for each environment
        commit: 3a9b86820fd28d892514c16bb0cd1afea1120143
[11/17] feat(icons): Add social icons
        commit: fdcf495a34f38a00d5dd22afcd53d876978ebd78
[12/17] feat(typing/enum): add Gender and IsActive enum
        commit: ed2f96695766e972848eff984885b0ebcc93ab93
[13/17] refactor!:feat: update API response structure, update profile page
        commit: 5e434a1f8e2203c6118ed5c6a4aa1407edaae775
[14/17] chore(meta): rename favicon.png to favicon.ico
        commit: f244cdb7fa8b3f880787e63358067d895a7224f9
[15/17] feat(seo): add SEO for site metadata
        commit: e535723845b7b9b788c000ad51b1918256aea75b
[16/17] chore(login): use $derived() instead of function based
        commit: f16c4482d0c851c0fd82ea1e5de21015c51ec914
[17/17] docs: update README.md
        commit: 2d08530366126cc141c7df02bcd2ff847cde264f

Best regards,
-- 
Ammar Faizi


^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs
  2025-03-05 14:39 [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs Muhammad Rizki
                   ` (18 preceding siblings ...)
  2025-03-05 17:04 ` Ammar Faizi
@ 2025-03-05 18:14 ` Alviro Iskandar Setiawan
  2025-03-06  1:59   ` Muhammad Rizki
  19 siblings, 1 reply; 29+ messages in thread
From: Alviro Iskandar Setiawan @ 2025-03-05 18:14 UTC (permalink / raw)
  To: Muhammad Rizki; +Cc: Ammar Faizi, GNU/Weeb Mailing List

btw, social media fields can't be left empty now. They should be nullable

-- Viro

^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs
  2025-03-05 18:14 ` Alviro Iskandar Setiawan
@ 2025-03-06  1:59   ` Muhammad Rizki
  2025-03-06  3:35     ` Ammar Faizi
  0 siblings, 1 reply; 29+ messages in thread
From: Muhammad Rizki @ 2025-03-06  1:59 UTC (permalink / raw)
  To: Alviro Iskandar Setiawan; +Cc: Ammar Faizi, GNU/Weeb Mailing List

On 06/03/2025 01:14, Alviro Iskandar Setiawan wrote:
> btw, social media fields can't be left empty now. They should be nullable
> 
> -- Viro

I created them as optional, but the API returns error field when one of 
them is empty.

^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs
  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  2:02   ` Muhammad Rizki
  2025-03-06  3:37     ` Ammar Faizi
  1 sibling, 1 reply; 29+ messages in thread
From: Muhammad Rizki @ 2025-03-06  2:02 UTC (permalink / raw)
  To: Ammar Faizi; +Cc: Alviro Iskandar Setiawan, GNU/Weeb Mailing List

On 05/03/2025 23:54, Ammar Faizi wrote:
> On Wed, Mar 05, 2025 at 09:39:59PM +0700, Muhammad Rizki wrote:
>> This series of patches was planned only to update profile page feature,
>> since there is a change in the API structure, I also fixed
>> the API response type to prevent breaking change.
>>
>> To prevent more breaking change on production site in the future,
>> sir Ammar has made a staging API for development stage,
>> since there is a staging API version, I added an environment variable
>> `PUBLIC_BASE_URL` for each environment (development or production).
>> You should set `PUBLIC_BASE_URL` environment in your own server,
>> there is an example file for development and production environment
>> in this patch, please see `.env.development.example` and
>> `.env.production.example` in root directory.
> 
> So, in my testing, the assets loading process is still broken. I had to
> reapply this patch manually to get it working:
> 
> diff --git a/svelte.config.js b/svelte.config.js
> index 5a6890a..40dc905 100644
> --- a/svelte.config.js
> +++ b/svelte.config.js
> @@ -5,6 +5,7 @@ import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
>   const config = {
>     preprocess: vitePreprocess(),
>     kit: {
> +    paths: { relative: false },
>       adapter: adapter({
>         pages: "dist",
>         assets: "dist",
> 
> That's fine. Also, I found that the delete profile photo button does
> not work.
> 
> Please send follow up patches for these two problems:
> 
> 1) Use 'paths: { relative: false }' to make it work on nginx.

Oh, I forgot to add it.

> 
> 2) Fix the 'delete' button on photo profile.

Also, forgot this too.

> 
> Don't send a v2 revision, just two new patches. This series is good to
> be applied, just a bit incomplete...
> 

Make a new revision?


^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs
  2025-03-06  1:59   ` Muhammad Rizki
@ 2025-03-06  3:35     ` Ammar Faizi
  0 siblings, 0 replies; 29+ messages in thread
From: Ammar Faizi @ 2025-03-06  3:35 UTC (permalink / raw)
  To: Muhammad Rizki; +Cc: Alviro Iskandar Setiawan, GNU/Weeb Mailing List

On Thu, Mar 06, 2025 at 08:59:57AM +0700, Muhammad Rizki wrote:
> On 06/03/2025 01:14, Alviro Iskandar Setiawan wrote:
> > btw, social media fields can't be left empty now. They should be nullable
> > 
> > -- Viro
> 
> I created them as optional, but the API returns error field when one of
> them is empty.

Actually, no.

You left the fields missing, not empty. You still need to send empty
fields of socials[xxx]. Try the old version for comparison.

-- 
Ammar Faizi


^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs
  2025-03-06  2:02   ` Muhammad Rizki
@ 2025-03-06  3:37     ` Ammar Faizi
  0 siblings, 0 replies; 29+ messages in thread
From: Ammar Faizi @ 2025-03-06  3:37 UTC (permalink / raw)
  To: Muhammad Rizki; +Cc: Alviro Iskandar Setiawan, GNU/Weeb Mailing List

On Thu, Mar 06, 2025 at 09:02:44AM +0700, Muhammad Rizki wrote: 
> Make a new revision?

This series is already applied. Send a new patch series.

-- 
Ammar Faizi


^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs
  2025-03-05 16:57   ` Alviro Iskandar Setiawan
@ 2025-03-06  7:01     ` Muhammad Rizki
  2025-03-06  7:02       ` Alviro Iskandar Setiawan
  0 siblings, 1 reply; 29+ messages in thread
From: Muhammad Rizki @ 2025-03-06  7:01 UTC (permalink / raw)
  To: Alviro Iskandar Setiawan, Ammar Faizi; +Cc: GNU/Weeb Mailing List

On 05/03/2025 23:57, Alviro Iskandar Setiawan wrote:
> On Wed, Mar 5, 2025 at 11:54 PM Ammar Faizi wrote:
>> Don't send a v2 revision, just two new patches. This series is good to
>> be applied, just a bit incomplete...
> 
> good then:
> 
> Acked-by: Alviro Iskandar Setiawan <[email protected]>
> 
> Sorry I did not review, just an ack should be ok. Tq
> 
> -- Viro

did you ask me to add it on all commits for the next series?

^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs
  2025-03-06  7:01     ` Muhammad Rizki
@ 2025-03-06  7:02       ` Alviro Iskandar Setiawan
  2025-03-06  7:04         ` Muhammad Rizki
  0 siblings, 1 reply; 29+ messages in thread
From: Alviro Iskandar Setiawan @ 2025-03-06  7:02 UTC (permalink / raw)
  To: Muhammad Rizki; +Cc: Ammar Faizi, GNU/Weeb Mailing List

On Thu, Mar 6, 2025 at 2:01 PM Muhammad Rizki wrote:
> did you ask me to add it on all commits for the next series?

No, it's already applied. My ack is already there.

-- Viro

^ permalink raw reply	[flat|nested] 29+ messages in thread

* Re: [PATCH v1 00/17] Profile Page, SEO, Fixed API structure, Docs
  2025-03-06  7:02       ` Alviro Iskandar Setiawan
@ 2025-03-06  7:04         ` Muhammad Rizki
  0 siblings, 0 replies; 29+ messages in thread
From: Muhammad Rizki @ 2025-03-06  7:04 UTC (permalink / raw)
  To: Alviro Iskandar Setiawan; +Cc: Ammar Faizi, GNU/Weeb Mailing List

On 06/03/2025 14:02, Alviro Iskandar Setiawan wrote:
> On Thu, Mar 6, 2025 at 2:01 PM Muhammad Rizki wrote:
>> did you ask me to add it on all commits for the next series?
> 
> No, it's already applied. My ack is already there.
> 
> -- Viro

Oh, I see.

^ permalink raw reply	[flat|nested] 29+ messages in thread

end of thread, other threads:[~2025-03-06  7:04 UTC | newest]

Thread overview: 29+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [PATCH v1 13/17] refactor!:feat: update API response structure, update profile page Muhammad Rizki
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

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox