* [PATCH v2 00/12] Fix Auth Guard, Move SEO
@ 2025-03-08 19:26 Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 01/12] fix(svelte): use relative false Muhammad Rizki
` (12 more replies)
0 siblings, 13 replies; 15+ messages in thread
From: Muhammad Rizki @ 2025-03-08 19:26 UTC (permalink / raw)
To: Ammar Faizi
Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List
Hello,
This series fixes auth guard to prevent users calling API with invalid
credentials, they will redirected to the login page if invalid
credentials occurs.
Please give it a test again if this bug still occurs, thanks.
## Changelog
v1 -> v2
- Move SEO from +layout.svelte to +page.svelte of /home page.
- Fix auth guard to prevent users calling API with invalid credentials.
Muhammad Rizki (12):
fix(svelte): use relative false
fix(avatar): change avatarImg state to use from auth.user.photo state
chore(profile): add toUpperCase() on getShortName()
fix(profile): make social fields default to empty string
chore(toaster): change toast message position and use richColors
chore(profile): reset password value on success
fix(profile-avatar): add delete avatar method
chore(profile): add space for password confirmation form
feat(ui): add dropdown-menu and update bits-ui version
chore(sidebar-menu): change sidebar menu look
chore(seo): move seo from layout to /home page
fix(auth): fix auth guard when credentials is invalid
package-lock.json | 8 +-
package.json | 2 +-
src/lib/components/customs/app-sidebar.svelte | 230 ++++++++++++------
.../dropdown-menu-checkbox-item.svelte | 40 +++
.../dropdown-menu-content.svelte | 27 ++
.../dropdown-menu-group-heading.svelte | 19 ++
.../dropdown-menu/dropdown-menu-item.svelte | 23 ++
.../dropdown-menu/dropdown-menu-label.svelte | 23 ++
.../dropdown-menu-radio-item.svelte | 30 +++
.../dropdown-menu-separator.svelte | 16 ++
.../dropdown-menu-shortcut.svelte | 20 ++
.../dropdown-menu-sub-content.svelte | 19 ++
.../dropdown-menu-sub-trigger.svelte | 28 +++
src/lib/components/ui/dropdown-menu/index.ts | 50 ++++
src/lib/hooks/auth.svelte.ts | 26 +-
src/lib/hooks/http.svelte.ts | 4 +-
src/lib/schemas/profile-schema.ts | 8 +-
src/routes/(protected)/+layout.svelte | 15 +-
src/routes/(protected)/+layout.ts | 9 +-
src/routes/(protected)/home/+page.svelte | 10 +
.../(protected)/settings/profile/+page.svelte | 120 +++++----
.../(protected)/settings/profile/+page.ts | 2 +-
src/routes/+layout.svelte | 2 +-
src/routes/+page.ts | 2 +
svelte.config.js | 1 +
25 files changed, 590 insertions(+), 144 deletions(-)
create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte
create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte
create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte
create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte
create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte
create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte
create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte
create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte
create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte
create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte
create mode 100644 src/lib/components/ui/dropdown-menu/index.ts
base-commit: 063f5f0deac9844c05e8439dc0cc0d1e1d5063dd
--
Muhammad Rizki
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH v2 01/12] fix(svelte): use relative false
2025-03-08 19:26 [PATCH v2 00/12] Fix Auth Guard, Move SEO Muhammad Rizki
@ 2025-03-08 19:26 ` Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 02/12] fix(avatar): change avatarImg state to use from auth.user.photo state Muhammad Rizki
` (11 subsequent siblings)
12 siblings, 0 replies; 15+ messages in thread
From: Muhammad Rizki @ 2025-03-08 19:26 UTC (permalink / raw)
To: Ammar Faizi
Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List
Added `paths: { relative: false }` on svelte.config.js to make it work
on nginx.
Signed-off-by: Muhammad Rizki <[email protected]>
---
svelte.config.js | 1 +
1 file changed, 1 insertion(+)
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",
--
Muhammad Rizki
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH v2 02/12] fix(avatar): change avatarImg state to use from auth.user.photo state
2025-03-08 19:26 [PATCH v2 00/12] Fix Auth Guard, Move SEO Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 01/12] fix(svelte): use relative false Muhammad Rizki
@ 2025-03-08 19:26 ` Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 03/12] chore(profile): add toUpperCase() on getShortName() Muhammad Rizki
` (10 subsequent siblings)
12 siblings, 0 replies; 15+ messages in thread
From: Muhammad Rizki @ 2025-03-08 19:26 UTC (permalink / raw)
To: Ammar Faizi
Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List
Previously, avatar image is kinda buggy when a user has an avatar photo
and then deleted the avatar photo; the user avatar is updated
asynchronously in sveltekit load() function which makes the
/settings/profile page is loaded firstly before trying to get_user_info
in /settings layout and make the avatar value uses the
previous value (avatar value with an avatar image url exists). To fix
this issue, we need to pass the auth.user.photo state to the avatarImg
state in the /settings/profile +page.svelte file instead on load().
Signed-off-by: Muhammad Rizki <[email protected]>
---
src/routes/(protected)/settings/profile/+page.svelte | 2 +-
src/routes/(protected)/settings/profile/+page.ts | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/routes/(protected)/settings/profile/+page.svelte b/src/routes/(protected)/settings/profile/+page.svelte
index b61601b..fc1d641 100644
--- a/src/routes/(protected)/settings/profile/+page.svelte
+++ b/src/routes/(protected)/settings/profile/+page.svelte
@@ -105,7 +105,7 @@
const auth = useAuth();
- let avatarImg = $state(data.avatar);
+ let avatarImg = $state(auth.user?.photo);
const avatar = $derived(avatarImg);
const getShortName = () => {
diff --git a/src/routes/(protected)/settings/profile/+page.ts b/src/routes/(protected)/settings/profile/+page.ts
index 7b25a4f..c3f9bd3 100644
--- a/src/routes/(protected)/settings/profile/+page.ts
+++ b/src/routes/(protected)/settings/profile/+page.ts
@@ -17,5 +17,5 @@ export const load: PageLoad = async () => {
};
const form = await superValidate(data, zod(profileSchema));
- return { form, avatar: auth.user?.photo };
+ return { form };
};
--
Muhammad Rizki
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH v2 03/12] chore(profile): add toUpperCase() on getShortName()
2025-03-08 19:26 [PATCH v2 00/12] Fix Auth Guard, Move SEO Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 01/12] fix(svelte): use relative false Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 02/12] fix(avatar): change avatarImg state to use from auth.user.photo state Muhammad Rizki
@ 2025-03-08 19:26 ` Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 04/12] fix(profile): make social fields default to empty string Muhammad Rizki
` (9 subsequent siblings)
12 siblings, 0 replies; 15+ messages in thread
From: Muhammad Rizki @ 2025-03-08 19:26 UTC (permalink / raw)
To: Ammar Faizi
Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List
Ensure it is uppercase.
Signed-off-by: Muhammad Rizki <[email protected]>
---
src/routes/(protected)/settings/profile/+page.svelte | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/routes/(protected)/settings/profile/+page.svelte b/src/routes/(protected)/settings/profile/+page.svelte
index fc1d641..5ddac3b 100644
--- a/src/routes/(protected)/settings/profile/+page.svelte
+++ b/src/routes/(protected)/settings/profile/+page.svelte
@@ -111,7 +111,7 @@
const getShortName = () => {
const fullName = auth.user?.full_name ?? "";
const match = fullName.match(/\b(\w)/g) ?? [];
- return match.slice(0, 2).join("");
+ return match.slice(0, 2).join("").toUpperCase();
};
const handleOpenModal = (e: boolean) => {
--
Muhammad Rizki
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH v2 04/12] fix(profile): make social fields default to empty string
2025-03-08 19:26 [PATCH v2 00/12] Fix Auth Guard, Move SEO Muhammad Rizki
` (2 preceding siblings ...)
2025-03-08 19:26 ` [PATCH v2 03/12] chore(profile): add toUpperCase() on getShortName() Muhammad Rizki
@ 2025-03-08 19:26 ` Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 05/12] chore(toaster): change toast message position and use richColors Muhammad Rizki
` (8 subsequent siblings)
12 siblings, 0 replies; 15+ messages in thread
From: Muhammad Rizki @ 2025-03-08 19:26 UTC (permalink / raw)
To: Ammar Faizi
Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List
Previously, is optional but doesn't have default value for it. This
commit adds default value for social fields to empty string to prevent
null value.
Signed-off-by: Muhammad Rizki <[email protected]>
---
src/lib/schemas/profile-schema.ts | 8 ++--
.../(protected)/settings/profile/+page.svelte | 38 ++++++++-----------
2 files changed, 20 insertions(+), 26 deletions(-)
diff --git a/src/lib/schemas/profile-schema.ts b/src/lib/schemas/profile-schema.ts
index c4c7228..a9981f1 100644
--- a/src/lib/schemas/profile-schema.ts
+++ b/src/lib/schemas/profile-schema.ts
@@ -13,10 +13,10 @@ export const profileSchema = z.object({
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()
+ github_username: z.string().optional().default(""),
+ telegram_username: z.string().optional().default(""),
+ twitter_username: z.string().optional().default(""),
+ discord_username: z.string().optional().default("")
}),
password: z.string()
});
diff --git a/src/routes/(protected)/settings/profile/+page.svelte b/src/routes/(protected)/settings/profile/+page.svelte
index 5ddac3b..2e9b76f 100644
--- a/src/routes/(protected)/settings/profile/+page.svelte
+++ b/src/routes/(protected)/settings/profile/+page.svelte
@@ -41,18 +41,10 @@
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);
- }
+ formData.append("socials[github_username]", form.data.socials.github_username);
+ formData.append("socials[telegram_username]", form.data.socials.telegram_username);
+ formData.append("socials[twitter_username]", form.data.socials.twitter_username);
+ formData.append("socials[discord_username]", form.data.socials.discord_username);
const {
data: { res },
@@ -434,17 +426,18 @@
disabled={$submitting || !isSubmittable || isError}
{...props}
>
- Update profile {isSubmittable}
+ Update profile
</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 use:enhance enctype="multipart/form-data">
+ <Dialog.Header>
+ <Dialog.Title>Update Profile Confirmation</Dialog.Title>
+ <Dialog.Description>Confirm changes to your profile here.</Dialog.Description>
+ </Dialog.Header>
+
<Form.Field {form} name="password" class="w-full">
<Form.Control>
{#snippet children({ props })}
@@ -463,10 +456,11 @@
{/snippet}
</Form.Control>
</Form.Field>
- </div>
- <Dialog.Footer>
- <Button type="submit" onclick={handleSubmit}>Confirm</Button>
- </Dialog.Footer>
+
+ <Dialog.Footer>
+ <Button type="submit" onclick={handleSubmit}>Confirm</Button>
+ </Dialog.Footer>
+ </form>
</Dialog.Content>
</form>
</Dialog.Root>
--
Muhammad Rizki
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH v2 05/12] chore(toaster): change toast message position and use richColors
2025-03-08 19:26 [PATCH v2 00/12] Fix Auth Guard, Move SEO Muhammad Rizki
` (3 preceding siblings ...)
2025-03-08 19:26 ` [PATCH v2 04/12] fix(profile): make social fields default to empty string Muhammad Rizki
@ 2025-03-08 19:26 ` Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 06/12] chore(profile): reset password value on success Muhammad Rizki
` (7 subsequent siblings)
12 siblings, 0 replies; 15+ messages in thread
From: Muhammad Rizki @ 2025-03-08 19:26 UTC (permalink / raw)
To: Ammar Faizi
Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List
change toast message position to `bottom-center` and use `richColors`
to increase UX.
Signed-off-by: Muhammad Rizki <[email protected]>
---
src/routes/+layout.svelte | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index e0aab7c..f24d888 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -7,4 +7,4 @@
{@render children()}
-<Toaster />
+<Toaster position="bottom-center" richColors />
--
Muhammad Rizki
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH v2 06/12] chore(profile): reset password value on success
2025-03-08 19:26 [PATCH v2 00/12] Fix Auth Guard, Move SEO Muhammad Rizki
` (4 preceding siblings ...)
2025-03-08 19:26 ` [PATCH v2 05/12] chore(toaster): change toast message position and use richColors Muhammad Rizki
@ 2025-03-08 19:26 ` Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 07/12] fix(profile-avatar): add delete avatar method Muhammad Rizki
` (6 subsequent siblings)
12 siblings, 0 replies; 15+ messages in thread
From: Muhammad Rizki @ 2025-03-08 19:26 UTC (permalink / raw)
To: Ammar Faizi
Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List
reset password value to prevent user spamming the update button.
Signed-off-by: Muhammad Rizki <[email protected]>
---
src/routes/(protected)/settings/profile/+page.svelte | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/routes/(protected)/settings/profile/+page.svelte b/src/routes/(protected)/settings/profile/+page.svelte
index 2e9b76f..664894e 100644
--- a/src/routes/(protected)/settings/profile/+page.svelte
+++ b/src/routes/(protected)/settings/profile/+page.svelte
@@ -66,6 +66,9 @@
user_info: data.res?.user_info
});
+ // reset password input onSuccess.
+ form.data.password = "";
+
toast.info("Success update profile", {
description: data.res?.msg ?? "Invalid credential, please login again."
});
--
Muhammad Rizki
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH v2 07/12] fix(profile-avatar): add delete avatar method
2025-03-08 19:26 [PATCH v2 00/12] Fix Auth Guard, Move SEO Muhammad Rizki
` (5 preceding siblings ...)
2025-03-08 19:26 ` [PATCH v2 06/12] chore(profile): reset password value on success Muhammad Rizki
@ 2025-03-08 19:26 ` Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 08/12] chore(profile): add space for password confirmation form Muhammad Rizki
` (5 subsequent siblings)
12 siblings, 0 replies; 15+ messages in thread
From: Muhammad Rizki @ 2025-03-08 19:26 UTC (permalink / raw)
To: Ammar Faizi
Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List
Added delete avatar method to remove user's avatar from the server.
Signed-off-by: Muhammad Rizki <[email protected]>
---
.../(protected)/settings/profile/+page.svelte | 73 +++++++++++++++----
1 file changed, 57 insertions(+), 16 deletions(-)
diff --git a/src/routes/(protected)/settings/profile/+page.svelte b/src/routes/(protected)/settings/profile/+page.svelte
index 664894e..43ad382 100644
--- a/src/routes/(protected)/settings/profile/+page.svelte
+++ b/src/routes/(protected)/settings/profile/+page.svelte
@@ -24,7 +24,6 @@
import Seo from "$components/customs/seo.svelte";
let { data } = $props();
- let showModalConfirmation = $state(false);
const form = superForm(data.form, {
SPA: true,
@@ -101,6 +100,9 @@
const auth = useAuth();
let avatarImg = $state(auth.user?.photo);
+ let openEditAvatar = $state(false);
+ let showModalConfirmation = $state(false);
+
const avatar = $derived(avatarImg);
const getShortName = () => {
@@ -132,6 +134,34 @@
handleOpenModal(false);
};
+ const deleteAvatar = async () => {
+ openEditAvatar = false;
+
+ if (!auth.user?.photo) {
+ // delete draft avatar
+ avatarImg = "";
+ $formData.photo = null;
+ return;
+ }
+
+ const {
+ data: { res },
+ status
+ } = await http<typing.ResponseAPI<{}>>({
+ params: { action: "delete_user_photo" },
+ method: "GET"
+ });
+
+ if (status === 200) {
+ avatarImg = "";
+ $formData.photo = null;
+
+ toast.info("Success delete profile picture", {
+ description: res?.msg
+ });
+ }
+ };
+
const isSubmittable = $derived(
Boolean($formData.full_name && $formData.ext_email && $formData.gender)
);
@@ -162,7 +192,7 @@
{/if}
</Avatar.Fallback>
</Avatar.Root>
- <Popover.Root>
+ <Popover.Root open={openEditAvatar} onOpenChange={(e) => (openEditAvatar = e)}>
<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"
>
@@ -174,16 +204,20 @@
<Form.Label
for="photo"
class="w-full cursor-pointer rounded-md px-2 py-1.5 text-start text-xs"
+ onclick={() => (openEditAvatar = false)}
>
Upload...
</Form.Label>
</Button>
- <Button
- variant="ghost"
- class="flex h-max w-full justify-start px-2 py-1.5 text-xs"
- >
- Delete
- </Button>
+ {#if avatar || $formData.photo}
+ <Button
+ onclick={deleteAvatar}
+ variant="ghost"
+ class="flex h-max w-full justify-start px-2 py-1.5 text-xs text-destructive hover:text-destructive"
+ >
+ Delete
+ </Button>
+ {/if}
</Popover.Content>
</Popover.Root>
</Form.Label>
@@ -380,7 +414,7 @@
{/if}
</Avatar.Fallback>
</Avatar.Root>
- <Popover.Root>
+ <Popover.Root open={openEditAvatar} onOpenChange={(e) => (openEditAvatar = e)}>
<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"
>
@@ -388,7 +422,11 @@
<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">
+ <Button
+ variant="ghost"
+ class="h-max w-full px-0 py-0"
+ onclick={() => (openEditAvatar = false)}
+ >
<Form.Label
for="photo"
class="w-full cursor-pointer rounded-md px-2 py-1.5 text-start text-xs"
@@ -396,12 +434,15 @@
Upload...
</Form.Label>
</Button>
- <Button
- variant="ghost"
- class="flex h-max w-full justify-start px-2 py-1.5 text-xs"
- >
- Delete
- </Button>
+ {#if avatar || $formData.photo}
+ <Button
+ onclick={deleteAvatar}
+ variant="ghost"
+ class="flex h-max w-full justify-start px-2 py-1.5 text-xs text-destructive hover:text-destructive"
+ >
+ Delete
+ </Button>
+ {/if}
</Popover.Content>
</Popover.Root>
</Form.Label>
--
Muhammad Rizki
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH v2 08/12] chore(profile): add space for password confirmation form
2025-03-08 19:26 [PATCH v2 00/12] Fix Auth Guard, Move SEO Muhammad Rizki
` (6 preceding siblings ...)
2025-03-08 19:26 ` [PATCH v2 07/12] fix(profile-avatar): add delete avatar method Muhammad Rizki
@ 2025-03-08 19:26 ` Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 09/12] feat(ui): add dropdown-menu and update bits-ui version Muhammad Rizki
` (4 subsequent siblings)
12 siblings, 0 replies; 15+ messages in thread
From: Muhammad Rizki @ 2025-03-08 19:26 UTC (permalink / raw)
To: Ammar Faizi
Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List
Added space to make it more easier to read the form.
Signed-off-by: Muhammad Rizki <[email protected]>
---
src/routes/(protected)/settings/profile/+page.svelte | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/routes/(protected)/settings/profile/+page.svelte b/src/routes/(protected)/settings/profile/+page.svelte
index 43ad382..9812ff2 100644
--- a/src/routes/(protected)/settings/profile/+page.svelte
+++ b/src/routes/(protected)/settings/profile/+page.svelte
@@ -482,7 +482,7 @@
<Dialog.Description>Confirm changes to your profile here.</Dialog.Description>
</Dialog.Header>
- <Form.Field {form} name="password" class="w-full">
+ <Form.Field {form} name="password" class="w-full pb-8 pt-5">
<Form.Control>
{#snippet children({ props })}
<Form.Label for="password">Password</Form.Label>
--
Muhammad Rizki
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH v2 09/12] feat(ui): add dropdown-menu and update bits-ui version
2025-03-08 19:26 [PATCH v2 00/12] Fix Auth Guard, Move SEO Muhammad Rizki
` (7 preceding siblings ...)
2025-03-08 19:26 ` [PATCH v2 08/12] chore(profile): add space for password confirmation form Muhammad Rizki
@ 2025-03-08 19:26 ` Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 10/12] chore(sidebar-menu): change sidebar menu look Muhammad Rizki
` (3 subsequent siblings)
12 siblings, 0 replies; 15+ messages in thread
From: Muhammad Rizki @ 2025-03-08 19:26 UTC (permalink / raw)
To: Ammar Faizi
Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List
Added dropdown-menu UI component and updated bits-ui version.
Signed-off-by: Muhammad Rizki <[email protected]>
---
package-lock.json | 8 +--
package.json | 2 +-
| 40 +++++++++++++++
| 27 ++++++++++
| 19 +++++++
| 23 +++++++++
| 23 +++++++++
| 30 +++++++++++
| 16 ++++++
| 20 ++++++++
| 19 +++++++
| 28 +++++++++++
| 50 +++++++++++++++++++
13 files changed, 300 insertions(+), 5 deletions(-)
create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte
create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte
create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte
create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte
create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte
create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte
create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte
create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte
create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte
create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte
create mode 100644 src/lib/components/ui/dropdown-menu/index.ts
diff --git a/package-lock.json b/package-lock.json
index 525092e..36dc7aa 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.5",
+ "bits-ui": "^1.3.6",
"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.5",
- "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-1.3.5.tgz",
- "integrity": "sha512-pfd8MK5Hp7bOvsW25LJrWVABmBIeAOH3g0pFPJLBIKlcqsaalEdBYejQmlSwrynEDX589aW3hTAWbhmWqRjjrQ==",
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-1.3.6.tgz",
+ "integrity": "sha512-0Ee7Ox5KqIBdio/+TG387Xlj6QJ0S7tHVS2K4DYiBHxBRbm6ni13i/pOoNDjeME6NOGUZxbSe4dNZIW3pGlxZA==",
"dev": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 52b0e4c..4d084e5 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.5",
+ "bits-ui": "^1.3.6",
"clsx": "^2.1.1",
"eslint": "^9.7.0",
"eslint-config-prettier": "^9.1.0",
--git a/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte
new file mode 100644
index 0000000..660e574
--- /dev/null
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte
@@ -0,0 +1,40 @@
+<script lang="ts">
+ import { DropdownMenu as DropdownMenuPrimitive, type WithoutChildrenOrChild } from "bits-ui";
+ import Check from "lucide-svelte/icons/check";
+ import Minus from "lucide-svelte/icons/minus";
+ import { cn } from "$utils";
+ import type { Snippet } from "svelte";
+
+ let {
+ ref = $bindable(null),
+ class: className,
+ children: childrenProp,
+ checked = $bindable(false),
+ indeterminate = $bindable(false),
+ ...restProps
+ }: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
+ children?: Snippet;
+ } = $props();
+</script>
+
+<DropdownMenuPrimitive.CheckboxItem
+ bind:ref
+ bind:checked
+ bind:indeterminate
+ class={cn(
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
+ className
+ )}
+ {...restProps}
+>
+ {#snippet children({ checked, indeterminate })}
+ <span class="absolute left-2 flex size-3.5 items-center justify-center">
+ {#if indeterminate}
+ <Minus class="size-4" />
+ {:else}
+ <Check class={cn("size-4", !checked && "text-transparent")} />
+ {/if}
+ </span>
+ {@render childrenProp?.()}
+ {/snippet}
+</DropdownMenuPrimitive.CheckboxItem>
--git a/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte
new file mode 100644
index 0000000..aada59c
--- /dev/null
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte
@@ -0,0 +1,27 @@
+<script lang="ts">
+ import { cn } from "$utils";
+ import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
+
+ let {
+ ref = $bindable(null),
+ class: className,
+ sideOffset = 4,
+ portalProps,
+ ...restProps
+ }: DropdownMenuPrimitive.ContentProps & {
+ portalProps?: DropdownMenuPrimitive.PortalProps;
+ } = $props();
+</script>
+
+<DropdownMenuPrimitive.Portal {...portalProps}>
+ <DropdownMenuPrimitive.Content
+ bind:ref
+ {sideOffset}
+ class={cn(
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 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}
+ />
+</DropdownMenuPrimitive.Portal>
--git a/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte
new file mode 100644
index 0000000..123869c
--- /dev/null
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte
@@ -0,0 +1,19 @@
+<script lang="ts">
+ import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
+ import { cn } from "$utils";
+
+ let {
+ ref = $bindable(null),
+ class: className,
+ inset,
+ ...restProps
+ }: DropdownMenuPrimitive.GroupHeadingProps & {
+ inset?: boolean;
+ } = $props();
+</script>
+
+<DropdownMenuPrimitive.GroupHeading
+ bind:ref
+ class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
+ {...restProps}
+/>
--git a/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte
new file mode 100644
index 0000000..cb92909
--- /dev/null
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte
@@ -0,0 +1,23 @@
+<script lang="ts">
+ import { cn } from "$utils";
+ import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
+
+ let {
+ ref = $bindable(null),
+ class: className,
+ inset,
+ ...restProps
+ }: DropdownMenuPrimitive.ItemProps & {
+ inset?: boolean;
+ } = $props();
+</script>
+
+<DropdownMenuPrimitive.Item
+ bind:ref
+ class={cn(
+ "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
+ inset && "pl-8",
+ className
+ )}
+ {...restProps}
+/>
--git a/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte
new file mode 100644
index 0000000..4a0b460
--- /dev/null
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte
@@ -0,0 +1,23 @@
+<script lang="ts">
+ import { cn } from "$utils";
+ import { type WithElementRef } from "bits-ui";
+ import type { HTMLAttributes } from "svelte/elements";
+
+ let {
+ ref = $bindable(null),
+ class: className,
+ inset,
+ children,
+ ...restProps
+ }: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
+ inset?: boolean;
+ } = $props();
+</script>
+
+<div
+ bind:this={ref}
+ class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
+ {...restProps}
+>
+ {@render children?.()}
+</div>
--git a/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte
new file mode 100644
index 0000000..676f467
--- /dev/null
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte
@@ -0,0 +1,30 @@
+<script lang="ts">
+ import { DropdownMenu as DropdownMenuPrimitive, type WithoutChild } from "bits-ui";
+ import Circle from "lucide-svelte/icons/circle";
+ import { cn } from "$utils";
+
+ let {
+ ref = $bindable(null),
+ class: className,
+ children: childrenProp,
+ ...restProps
+ }: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
+</script>
+
+<DropdownMenuPrimitive.RadioItem
+ bind:ref
+ class={cn(
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
+ className
+ )}
+ {...restProps}
+>
+ {#snippet children({ checked })}
+ <span class="absolute left-2 flex size-3.5 items-center justify-center">
+ {#if checked}
+ <Circle class="size-2 fill-current" />
+ {/if}
+ </span>
+ {@render childrenProp?.({ checked })}
+ {/snippet}
+</DropdownMenuPrimitive.RadioItem>
--git a/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte
new file mode 100644
index 0000000..da3d7b0
--- /dev/null
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte
@@ -0,0 +1,16 @@
+<script lang="ts">
+ import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
+ import { cn } from "$utils";
+
+ let {
+ ref = $bindable(null),
+ class: className,
+ ...restProps
+ }: DropdownMenuPrimitive.SeparatorProps = $props();
+</script>
+
+<DropdownMenuPrimitive.Separator
+ bind:ref
+ class={cn("-mx-1 my-1 h-px bg-muted", className)}
+ {...restProps}
+/>
--git a/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte
new file mode 100644
index 0000000..8de03e7
--- /dev/null
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.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<HTMLSpanElement>> = $props();
+</script>
+
+<span
+ bind:this={ref}
+ class={cn("ml-auto text-xs tracking-widest opacity-60", className)}
+ {...restProps}
+>
+ {@render children?.()}
+</span>
--git a/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte
new file mode 100644
index 0000000..d999904
--- /dev/null
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte
@@ -0,0 +1,19 @@
+<script lang="ts">
+ import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
+ import { cn } from "$utils";
+
+ let {
+ ref = $bindable(null),
+ class: className,
+ ...restProps
+ }: DropdownMenuPrimitive.SubContentProps = $props();
+</script>
+
+<DropdownMenuPrimitive.SubContent
+ bind:ref
+ class={cn(
+ "z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-lg focus:outline-none",
+ className
+ )}
+ {...restProps}
+/>
--git a/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte
new file mode 100644
index 0000000..cd0a621
--- /dev/null
+++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte
@@ -0,0 +1,28 @@
+<script lang="ts">
+ import { DropdownMenu as DropdownMenuPrimitive, type WithoutChild } from "bits-ui";
+ import ChevronRight from "lucide-svelte/icons/chevron-right";
+ import { cn } from "$utils";
+
+ let {
+ ref = $bindable(null),
+ class: className,
+ inset,
+ children,
+ ...restProps
+ }: WithoutChild<DropdownMenuPrimitive.SubTriggerProps> & {
+ inset?: boolean;
+ } = $props();
+</script>
+
+<DropdownMenuPrimitive.SubTrigger
+ bind:ref
+ class={cn(
+ "flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ inset && "pl-8",
+ className
+ )}
+ {...restProps}
+>
+ {@render children?.()}
+ <ChevronRight class="ml-auto" />
+</DropdownMenuPrimitive.SubTrigger>
--git a/src/lib/components/ui/dropdown-menu/index.ts b/src/lib/components/ui/dropdown-menu/index.ts
new file mode 100644
index 0000000..2ea0e8c
--- /dev/null
+++ b/src/lib/components/ui/dropdown-menu/index.ts
@@ -0,0 +1,50 @@
+import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
+import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
+import Content from "./dropdown-menu-content.svelte";
+import GroupHeading from "./dropdown-menu-group-heading.svelte";
+import Item from "./dropdown-menu-item.svelte";
+import Label from "./dropdown-menu-label.svelte";
+import RadioItem from "./dropdown-menu-radio-item.svelte";
+import Separator from "./dropdown-menu-separator.svelte";
+import Shortcut from "./dropdown-menu-shortcut.svelte";
+import SubContent from "./dropdown-menu-sub-content.svelte";
+import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
+
+const Sub = DropdownMenuPrimitive.Sub;
+const Root = DropdownMenuPrimitive.Root;
+const Trigger = DropdownMenuPrimitive.Trigger;
+const Group = DropdownMenuPrimitive.Group;
+const RadioGroup = DropdownMenuPrimitive.RadioGroup;
+
+export {
+ CheckboxItem,
+ Content,
+ Root as DropdownMenu,
+ CheckboxItem as DropdownMenuCheckboxItem,
+ Content as DropdownMenuContent,
+ Group as DropdownMenuGroup,
+ GroupHeading as DropdownMenuGroupHeading,
+ Item as DropdownMenuItem,
+ Label as DropdownMenuLabel,
+ RadioGroup as DropdownMenuRadioGroup,
+ RadioItem as DropdownMenuRadioItem,
+ Separator as DropdownMenuSeparator,
+ Shortcut as DropdownMenuShortcut,
+ Sub as DropdownMenuSub,
+ SubContent as DropdownMenuSubContent,
+ SubTrigger as DropdownMenuSubTrigger,
+ Trigger as DropdownMenuTrigger,
+ Group,
+ GroupHeading,
+ Item,
+ Label,
+ RadioGroup,
+ RadioItem,
+ Root,
+ Separator,
+ Shortcut,
+ Sub,
+ SubContent,
+ SubTrigger,
+ Trigger
+};
--
Muhammad Rizki
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH v2 10/12] chore(sidebar-menu): change sidebar menu look
2025-03-08 19:26 [PATCH v2 00/12] Fix Auth Guard, Move SEO Muhammad Rizki
` (8 preceding siblings ...)
2025-03-08 19:26 ` [PATCH v2 09/12] feat(ui): add dropdown-menu and update bits-ui version Muhammad Rizki
@ 2025-03-08 19:26 ` Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 11/12] chore(seo): move seo from layout to /home page Muhammad Rizki
` (2 subsequent siblings)
12 siblings, 0 replies; 15+ messages in thread
From: Muhammad Rizki @ 2025-03-08 19:26 UTC (permalink / raw)
To: Ammar Faizi
Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List
This commit fixes the logout button in the sidebar menu which
overflowed to main content, with this fix, I change the sidebar footer
with account info and use dropdown menu to display the logout button and
account info.
Signed-off-by: Muhammad Rizki <[email protected]>
---
| 230 ++++++++++++------
1 file changed, 154 insertions(+), 76 deletions(-)
--git a/src/lib/components/customs/app-sidebar.svelte b/src/lib/components/customs/app-sidebar.svelte
index 7ccc59c..2132214 100644
--- a/src/lib/components/customs/app-sidebar.svelte
+++ b/src/lib/components/customs/app-sidebar.svelte
@@ -2,6 +2,9 @@
import { navigations } from "$constants";
import Separator from "$components/ui/separator/separator.svelte";
import * as Sidebar from "$lib/components/ui/sidebar";
+ import * as Avatar from "$lib/components/ui/avatar";
+ import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
+ import * as Dialog from "$lib/components/ui/dialog";
import { useAuth } from "$lib/hooks/auth.svelte";
import LogOut from "lucide-svelte/icons/log-out";
import Mails from "lucide-svelte/icons/mails";
@@ -14,17 +17,27 @@
import { crossfade } from "svelte/transition";
import { cubicInOut } from "svelte/easing";
import IconRoundcube from "$components/icons/icon-roundcube.svelte";
+ import Loading from "./loading.svelte";
+ import ChevronsUpDown from "lucide-svelte/icons/chevrons-up-down";
let { ref = $bindable(null), ...restProps }: ComponentProps<typeof Sidebar.Root> = $props();
const auth = useAuth();
const sidebar = Sidebar.useSidebar();
+ let showModalConfirmation = $state(false);
+
const [send, receive] = crossfade({
duration: 250,
easing: cubicInOut
});
+ const getShortName = () => {
+ const fullName = auth.user?.full_name ?? "";
+ const match = fullName.match(/\b(\w)/g) ?? [];
+ return match.slice(0, 2).join("").toUpperCase();
+ };
+
const handleNavigationMobile = () => {
if (!sidebar.isMobile) return;
sidebar.toggle();
@@ -36,93 +49,158 @@
};
</script>
-<Sidebar.Root bind:ref variant="inset" collapsible="icon" {...restProps}>
- <Sidebar.Content>
- <Sidebar.Group>
- <Sidebar.Header>
- <Sidebar.Menu>
- <Sidebar.MenuItem>
- <Sidebar.MenuButton onclick={() => sidebar.toggle()} size="lg">
- <div
- class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"
- >
- <Mails class="size-4" />
- </div>
- <div class="grid flex-1 text-left text-sm leading-tight">
- <span class="truncate font-semibold">{auth.user?.full_name}</span>
- <span class="truncate text-xs">@{auth.user?.username}</span>
- </div>
- </Sidebar.MenuButton>
- </Sidebar.MenuItem>
- </Sidebar.Menu>
- </Sidebar.Header>
- <Separator class="mb-3" />
- <Sidebar.GroupContent>
- <Sidebar.Menu>
- {#each navigations as item (item.name)}
+<Dialog.Root open={showModalConfirmation} onOpenChange={(e) => (showModalConfirmation = e)}>
+ <Sidebar.Root bind:ref variant="inset" collapsible="icon" {...restProps}>
+ <Sidebar.Content>
+ <Sidebar.Group>
+ <Sidebar.Header>
+ <Sidebar.Menu>
<Sidebar.MenuItem>
- {@const isActive = page.url.pathname.startsWith(item.url)}
- <Sidebar.MenuButton {isActive} class="relative">
+ <Sidebar.MenuButton onclick={() => sidebar.toggle()} size="lg">
+ <div
+ class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"
+ >
+ <Mails class="size-4" />
+ </div>
+ <h2 class="shrink-0 text-lg font-semibold">G/W Mail</h2>
+ </Sidebar.MenuButton>
+ </Sidebar.MenuItem>
+ </Sidebar.Menu>
+ </Sidebar.Header>
+ <Separator class="mb-3" />
+ <Sidebar.GroupContent>
+ <Sidebar.Menu>
+ {#each navigations as item (item.name)}
+ <Sidebar.MenuItem>
+ {@const isActive = page.url.pathname.startsWith(item.url)}
+ <Sidebar.MenuButton {isActive} class="relative">
+ {#snippet child({ props })}
+ {@const className = props.class as string}
+ <a
+ href={item.url}
+ onclick={handleNavigationMobile}
+ {...props}
+ class={cn("relative z-10", className)}
+ >
+ <item.icon />
+ <span>{item.name}</span>
+ </a>
+ {#if isActive}
+ <!-- svelte-ignore element_invalid_self_closing_tag -->
+ <div
+ class={cn(
+ "absolute inset-0 rounded-md bg-sidebar-accent",
+ isActive &&
+ "overflow-hidden before:absolute before:left-0 before:h-full before:w-0.5 before:bg-foreground"
+ )}
+ in:send={{ key: "active-sidebar-tab" }}
+ out:receive={{ key: "active-sidebar-tab" }}
+ />
+ {/if}
+ {/snippet}
+ </Sidebar.MenuButton>
+ </Sidebar.MenuItem>
+ {/each}
+ <Sidebar.MenuItem>
+ <Sidebar.MenuButton>
{#snippet child({ props })}
{@const className = props.class as string}
<a
- href={item.url}
- onclick={handleNavigationMobile}
+ href="https://mail.gnuweeb.org/roundcube/"
{...props}
- class={cn("relative z-10", className)}
+ class={cn("group/roundcube", className)}
+ target="_blank"
>
- <item.icon />
- <span>{item.name}</span>
- </a>
- {#if isActive}
- <!-- svelte-ignore element_invalid_self_closing_tag -->
- <div
- class={cn(
- "absolute inset-0 rounded-md bg-sidebar-accent",
- isActive &&
- "overflow-hidden before:absolute before:left-0 before:h-full before:w-0.5 before:bg-foreground"
- )}
- in:send={{ key: "active-sidebar-tab" }}
- out:receive={{ key: "active-sidebar-tab" }}
+ <IconRoundcube />
+ <span>Roundcube</span>
+ <SquareArrowOutUpRight
+ class="!size-3 transition-transform group-hover/roundcube:!scale-125"
/>
- {/if}
+ </a>
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
- {/each}
- <Sidebar.MenuItem>
- <Sidebar.MenuButton>
+ </Sidebar.Menu>
+ </Sidebar.GroupContent>
+ </Sidebar.Group>
+ </Sidebar.Content>
+ <Sidebar.Footer>
+ <Sidebar.Menu>
+ <Sidebar.MenuItem>
+ <DropdownMenu.Root>
+ <DropdownMenu.Trigger>
{#snippet child({ props })}
- {@const className = props.class as string}
- <a
- href="https://mail.gnuweeb.org/roundcube/"
+ <Sidebar.MenuButton
+ size="lg"
+ class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
{...props}
- class={cn("group/roundcube", className)}
- target="_blank"
>
- <IconRoundcube />
- <span>Roundcube</span>
- <SquareArrowOutUpRight
- class="!size-3 transition-transform group-hover/roundcube:!scale-125"
- />
- </a>
+ <Avatar.Root class="h-8 w-8 rounded-lg">
+ <Avatar.Image src={auth.user?.photo} alt="{auth.user?.username}@gnuweeb.org" />
+ <Avatar.Fallback class="rounded-lg text-xs">
+ {#if !Boolean(auth.user?.photo)}
+ {getShortName()}
+ {:else}
+ <Loading class="size-3 text-black" />
+ {/if}
+ </Avatar.Fallback>
+ </Avatar.Root>
+ <div class="grid flex-1 text-left text-sm leading-tight">
+ <span class="truncate font-semibold">{auth.user?.full_name}</span>
+ <span class="truncate text-xs">{auth.user?.username}@gnuweeb.org</span>
+ </div>
+ <ChevronsUpDown class="ml-auto size-4" />
+ </Sidebar.MenuButton>
{/snippet}
- </Sidebar.MenuButton>
- </Sidebar.MenuItem>
- </Sidebar.Menu>
- </Sidebar.GroupContent>
- </Sidebar.Group>
- </Sidebar.Content>
- <Sidebar.Footer>
- <Sidebar.Menu>
- <Button
- variant="destructive"
- onclick={handleLogout}
- class="flex w-full items-center justify-start"
- >
- <LogOut />
- <span>Logout</span>
- </Button>
- </Sidebar.Menu>
- </Sidebar.Footer>
-</Sidebar.Root>
+ </DropdownMenu.Trigger>
+ <DropdownMenu.Content
+ class="w-[var(--bits-dropdown-menu-anchor-width)] min-w-56 rounded-lg"
+ side={sidebar.isMobile ? "bottom" : "right"}
+ align="end"
+ sideOffset={4}
+ >
+ <DropdownMenu.Label class="p-0 font-normal">
+ <div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
+ <Avatar.Root class="h-8 w-8 rounded-lg">
+ <Avatar.Image src={auth.user?.photo} alt="{auth.user?.username}@gnuweeb.org" />
+ <Avatar.Fallback class="rounded-lg text-xs">
+ {#if !Boolean(auth.user?.photo)}
+ {getShortName()}
+ {:else}
+ <Loading class="size-3 text-black" />
+ {/if}
+ </Avatar.Fallback>
+ </Avatar.Root>
+ <div class="grid flex-1 text-left text-sm leading-tight">
+ <span class="truncate font-semibold">{auth.user?.full_name}</span>
+ <span class="truncate text-xs">{auth.user?.username}@gnuweeb.org</span>
+ </div>
+ </div>
+ </DropdownMenu.Label>
+ <DropdownMenu.Separator />
+ <DropdownMenu.Item
+ onclick={() => (showModalConfirmation = true)}
+ class="text-destructive hover:!text-destructive"
+ >
+ <LogOut />
+ Log out
+ </DropdownMenu.Item>
+ </DropdownMenu.Content>
+ </DropdownMenu.Root>
+ </Sidebar.MenuItem>
+ </Sidebar.Menu>
+ </Sidebar.Footer>
+ </Sidebar.Root>
+ <Dialog.Content class="sm:max-w-[425px]">
+ <Dialog.Header>
+ <Dialog.Title>Logout Confirmation</Dialog.Title>
+ <Dialog.Description>Confirm logout from your profile.</Dialog.Description>
+ </Dialog.Header>
+ <p class="text-sm font-medium">
+ You are about to logout from your profile, click logout button below to proceed.
+ </p>
+ <Dialog.Footer>
+ <Button type="submit" variant="destructive" onclick={handleLogout}>Logout</Button>
+ </Dialog.Footer>
+ </Dialog.Content>
+</Dialog.Root>
--
Muhammad Rizki
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH v2 11/12] chore(seo): move seo from layout to /home page
2025-03-08 19:26 [PATCH v2 00/12] Fix Auth Guard, Move SEO Muhammad Rizki
` (9 preceding siblings ...)
2025-03-08 19:26 ` [PATCH v2 10/12] chore(sidebar-menu): change sidebar menu look Muhammad Rizki
@ 2025-03-08 19:26 ` Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 12/12] fix(auth): fix auth guard when credentials is invalid Muhammad Rizki
2025-03-08 19:38 ` [PATCH v2 00/12] Fix Auth Guard, Move SEO Ammar Faizi
12 siblings, 0 replies; 15+ messages in thread
From: Muhammad Rizki @ 2025-03-08 19:26 UTC (permalink / raw)
To: Ammar Faizi
Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List
Move SEO from +layout.svelte to +page.svelte in /home page.
Signed-off-by: Muhammad Rizki <[email protected]>
---
src/routes/(protected)/+layout.svelte | 12 ------------
src/routes/(protected)/home/+page.svelte | 10 ++++++++++
2 files changed, 10 insertions(+), 12 deletions(-)
diff --git a/src/routes/(protected)/+layout.svelte b/src/routes/(protected)/+layout.svelte
index ec934be..e88b3fb 100644
--- a/src/routes/(protected)/+layout.svelte
+++ b/src/routes/(protected)/+layout.svelte
@@ -3,22 +3,10 @@
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)/home/+page.svelte b/src/routes/(protected)/home/+page.svelte
index ef2be4b..4d35704 100644
--- a/src/routes/(protected)/home/+page.svelte
+++ b/src/routes/(protected)/home/+page.svelte
@@ -5,6 +5,10 @@
import { mailConfig } from "$constants";
import { toast } from "svelte-sonner";
import { copyText } from "svelte-copy";
+ import { useAuth } from "$lib/hooks/auth.svelte";
+ import Seo from "$components/customs/seo.svelte";
+
+ const auth = useAuth();
const copy = async (text: string) => {
try {
@@ -18,6 +22,12 @@
};
</script>
+<Seo
+ title="{auth.user?.full_name} ({auth.user?.username}) - GNU/Weeb Mail"
+ description="Configure your email client using this config"
+ image={auth.user?.photo}
+/>
+
<div class="flex h-full w-full justify-center">
<Card.Root>
<Card.Header class="py-5">
--
Muhammad Rizki
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH v2 12/12] fix(auth): fix auth guard when credentials is invalid
2025-03-08 19:26 [PATCH v2 00/12] Fix Auth Guard, Move SEO Muhammad Rizki
` (10 preceding siblings ...)
2025-03-08 19:26 ` [PATCH v2 11/12] chore(seo): move seo from layout to /home page Muhammad Rizki
@ 2025-03-08 19:26 ` Muhammad Rizki
2025-03-08 19:38 ` [PATCH v2 00/12] Fix Auth Guard, Move SEO Ammar Faizi
12 siblings, 0 replies; 15+ messages in thread
From: Muhammad Rizki @ 2025-03-08 19:26 UTC (permalink / raw)
To: Ammar Faizi
Cc: Muhammad Rizki, Alviro Iskandar Setiawan, GNU/Weeb Mailing List
This commit fixes auth guard to prevent invalid credentials to call API,
it will redirect user to the login page if credentials is invalid.
Signed-off-by: Muhammad Rizki <[email protected]>
---
src/lib/hooks/auth.svelte.ts | 26 +++++++++++++++++++++-----
src/lib/hooks/http.svelte.ts | 4 +++-
src/routes/(protected)/+layout.svelte | 9 +++++++++
src/routes/(protected)/+layout.ts | 9 ++++++++-
src/routes/+page.ts | 2 ++
5 files changed, 43 insertions(+), 7 deletions(-)
diff --git a/src/lib/hooks/auth.svelte.ts b/src/lib/hooks/auth.svelte.ts
index 7bd2d3b..aa4beda 100644
--- a/src/lib/hooks/auth.svelte.ts
+++ b/src/lib/hooks/auth.svelte.ts
@@ -5,6 +5,17 @@ let data = $state<LoginResponse>({
token_exp_at: 0
});
+const getUserFromLocalStorage = () => {
+ const user = localStorage.getItem("gwm_uinfo");
+ if (!user) return undefined;
+
+ try {
+ return JSON.parse(user) as User;
+ } catch {
+ return undefined;
+ }
+};
+
export function useAuth() {
return {
get token() {
@@ -24,8 +35,12 @@ export function useAuth() {
},
refresh() {
- const user = localStorage.getItem("gwm_uinfo");
- data.user_info = JSON.parse(user!) as User;
+ const token = localStorage.getItem("gwm_token");
+ const token_exp_at = Number(localStorage.getItem("gwm_token_exp_at"));
+
+ data.user_info = getUserFromLocalStorage();
+ data.token = token!;
+ data.token_exp_at = token_exp_at;
},
save({ user_info, token, token_exp_at }: LoginResponse) {
@@ -42,15 +57,16 @@ export function useAuth() {
},
isValid() {
+ const user = getUserFromLocalStorage();
const token = localStorage.getItem("gwm_token");
- const user = localStorage.getItem("gwm_uinfo");
+ const expLs = localStorage.getItem("gwm_token_exp_at");
- if (!token || !user) {
+ if (!token || !user || !expLs) {
this.clear();
return false;
}
- const exp = Number(localStorage.getItem("gwm_token_exp_at"));
+ const exp = Number(expLs);
const unix = Math.round(new Date().getTime() / 1000);
if (unix >= exp) {
diff --git a/src/lib/hooks/http.svelte.ts b/src/lib/hooks/http.svelte.ts
index d3445d9..f30687d 100644
--- a/src/lib/hooks/http.svelte.ts
+++ b/src/lib/hooks/http.svelte.ts
@@ -1,3 +1,4 @@
+import { goto } from "$app/navigation";
import { PUBLIC_BASE_URL } from "$env/static/public";
import * as typing from "$typings";
import axios from "axios";
@@ -53,10 +54,11 @@ client.interceptors.response.use(
const response = err.response as AxiosResponse<typing.ResponseAPI<typing.RenewTokenResponse>>;
const status = response ? response.status : null;
- if (status === 403 && response?.data) {
+ if (status !== 200) {
localStorage.removeItem("gwm_token");
localStorage.removeItem("gwm_token_exp_at");
localStorage.removeItem("gwm_uinfo");
+ goto("/");
}
return response;
diff --git a/src/routes/(protected)/+layout.svelte b/src/routes/(protected)/+layout.svelte
index e88b3fb..f5998a7 100644
--- a/src/routes/(protected)/+layout.svelte
+++ b/src/routes/(protected)/+layout.svelte
@@ -3,8 +3,17 @@
import AppSidebar from "$components/customs/app-sidebar.svelte";
import Header from "$components/customs/header.svelte";
import Separator from "$components/ui/separator/separator.svelte";
+ import { useAuth } from "$lib/hooks/auth.svelte";
+ import { goto, onNavigate } from "$app/navigation";
let { children } = $props();
+
+ const auth = useAuth();
+
+ onNavigate(() => {
+ if (auth.isValid()) return;
+ goto("/");
+ });
</script>
<Sidebar.Provider class="light">
diff --git a/src/routes/(protected)/+layout.ts b/src/routes/(protected)/+layout.ts
index 06ff2db..951766f 100644
--- a/src/routes/(protected)/+layout.ts
+++ b/src/routes/(protected)/+layout.ts
@@ -9,13 +9,20 @@ export const load: LayoutLoad = async () => {
if (!auth.isValid()) {
localStorage.setItem("gwm_invalid_creds", String(1));
+ auth.clear();
return redirect(307, "/");
}
- const { data } = await http<{ user_info: typing.User }>({
+ const { status, data } = await http<{ user_info: typing.User }>({
params: { action: "get_user_info" }
});
+ if (status !== 200) {
+ localStorage.setItem("gwm_invalid_creds", String(1));
+ auth.clear();
+ return redirect(307, "/");
+ }
+
auth.save({
token: data.res?.renew_token?.token,
token_exp_at: data.res?.renew_token?.token_exp_at,
diff --git a/src/routes/+page.ts b/src/routes/+page.ts
index 8cbe162..604e967 100644
--- a/src/routes/+page.ts
+++ b/src/routes/+page.ts
@@ -11,6 +11,8 @@ export const load: PageLoad = async () => {
if (auth.isValid()) return redirect(307, "/home");
+ auth.refresh();
+
const form = await superValidate(zod(loginSchema));
return { form, isInvalidCreds };
};
--
Muhammad Rizki
^ permalink raw reply related [flat|nested] 15+ messages in thread
* Re: [PATCH v2 00/12] Fix Auth Guard, Move SEO
2025-03-08 19:26 [PATCH v2 00/12] Fix Auth Guard, Move SEO Muhammad Rizki
` (11 preceding siblings ...)
2025-03-08 19:26 ` [PATCH v2 12/12] fix(auth): fix auth guard when credentials is invalid Muhammad Rizki
@ 2025-03-08 19:38 ` Ammar Faizi
2025-03-08 19:51 ` Alviro Iskandar Setiawan
12 siblings, 1 reply; 15+ messages in thread
From: Ammar Faizi @ 2025-03-08 19:38 UTC (permalink / raw)
To: Muhammad Rizki
Cc: Ammar Faizi, GNU/Weeb Mailing List, Alviro Iskandar Setiawan
On Sun, 9 Mar 2025 02:26:42 +0700, Muhammad Rizki wrote:
> This series fixes auth guard to prevent users calling API with invalid
> credentials, they will redirected to the login page if invalid
> credentials occurs.
>
> [...]
Applied, thanks!
It's now live at https://mail-staging.gnuweeb.org/
The reset password feature is coming to the API soon...
[01/12] fix(svelte): use relative false
commit: 37c7412d6743bb7615d784094a137e2522426938
[02/12] fix(avatar): change avatarImg state to use from auth.user.photo state
commit: 65c5bf1c0aebcdf239bd0eb608802d0f9fcca152
[03/12] chore(profile): add toUpperCase() on getShortName()
commit: 6c87dd3ad9fda0b4cdb658ca6e1f435eb6f4439e
[04/12] fix(profile): make social fields default to empty string
commit: b4f1e97598731c8ecb44c556e850afdd58e07fde
[05/12] chore(toaster): change toast message position and use richColors
commit: 8ec7d55700ec2a7dd33e7ecca4144f491a0566b1
[06/12] chore(profile): reset password value on success
commit: 5291b29be4e9ec6b156f79f97081d3a05bddad82
[07/12] fix(profile-avatar): add delete avatar method
commit: 2fa88a5eb99ce0eb21e25c921c39b35776ba75b2
[08/12] chore(profile): add space for password confirmation form
commit: 2986ecdcb0981360562c90f6e126082e22797258
[09/12] feat(ui): add dropdown-menu and update bits-ui version
commit: 77da4b1ba04df74fbb41c4f0baf50160be1344ef
[10/12] chore(sidebar-menu): change sidebar menu look
commit: 6c19cd4f056bd80b17803ff6544276a04695a8ff
[11/12] chore(seo): move seo from layout to /home page
commit: b21826cdba3fec36a7e5aff20adf9820d17e0c24
[12/12] fix(auth): fix auth guard when credentials is invalid
commit: 6e9649aaa51a21e2de9e03ea07ea3b2429fdcba5
Best regards,
--
Ammar Faizi
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v2 00/12] Fix Auth Guard, Move SEO
2025-03-08 19:38 ` [PATCH v2 00/12] Fix Auth Guard, Move SEO Ammar Faizi
@ 2025-03-08 19:51 ` Alviro Iskandar Setiawan
0 siblings, 0 replies; 15+ messages in thread
From: Alviro Iskandar Setiawan @ 2025-03-08 19:51 UTC (permalink / raw)
To: Ammar Faizi; +Cc: Muhammad Rizki, GNU/Weeb Mailing List
On Sun, Mar 9, 2025 at 2:38 AM Ammar Faizi wrote:
> It's now live at https://mail-staging.gnuweeb.org/
Looks good.
-- Viro
^ permalink raw reply [flat|nested] 15+ messages in thread
end of thread, other threads:[~2025-03-08 19:51 UTC | newest]
Thread overview: 15+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-03-08 19:26 [PATCH v2 00/12] Fix Auth Guard, Move SEO Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 01/12] fix(svelte): use relative false Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 02/12] fix(avatar): change avatarImg state to use from auth.user.photo state Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 03/12] chore(profile): add toUpperCase() on getShortName() Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 04/12] fix(profile): make social fields default to empty string Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 05/12] chore(toaster): change toast message position and use richColors Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 06/12] chore(profile): reset password value on success Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 07/12] fix(profile-avatar): add delete avatar method Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 08/12] chore(profile): add space for password confirmation form Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 09/12] feat(ui): add dropdown-menu and update bits-ui version Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 10/12] chore(sidebar-menu): change sidebar menu look Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 11/12] chore(seo): move seo from layout to /home page Muhammad Rizki
2025-03-08 19:26 ` [PATCH v2 12/12] fix(auth): fix auth guard when credentials is invalid Muhammad Rizki
2025-03-08 19:38 ` [PATCH v2 00/12] Fix Auth Guard, Move SEO Ammar Faizi
2025-03-08 19:51 ` Alviro Iskandar Setiawan
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox