* [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]>
---
| 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(-)
--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]>
---
| 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 +++
| 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
--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 ?? [];
+};
--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 ++++++++
| 20 ++++++++++
| 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}
+/>
--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>
--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 +++++++++++++++++++
| 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}
--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 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
* 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-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-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