* [PATCH v1 01/10] fix(svelte): use relative false
2025-03-07 19:26 [PATCH v1 00/10] Relative Path, Delete Avatar, Sidebar, Toast Muhammad Rizki
@ 2025-03-07 19:26 ` Muhammad Rizki
2025-03-07 19:26 ` [PATCH v1 02/10] fix(avatar): change avatarImg state to use from auth.user.photo state Muhammad Rizki
` (9 subsequent siblings)
10 siblings, 0 replies; 15+ messages in thread
From: Muhammad Rizki @ 2025-03-07 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 v1 02/10] fix(avatar): change avatarImg state to use from auth.user.photo state
2025-03-07 19:26 [PATCH v1 00/10] Relative Path, Delete Avatar, Sidebar, Toast Muhammad Rizki
2025-03-07 19:26 ` [PATCH v1 01/10] fix(svelte): use relative false Muhammad Rizki
@ 2025-03-07 19:26 ` Muhammad Rizki
2025-03-07 19:26 ` [PATCH v1 03/10] chore(profile): add toUpperCase() on getShortName() Muhammad Rizki
` (8 subsequent siblings)
10 siblings, 0 replies; 15+ messages in thread
From: Muhammad Rizki @ 2025-03-07 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 v1 03/10] chore(profile): add toUpperCase() on getShortName()
2025-03-07 19:26 [PATCH v1 00/10] Relative Path, Delete Avatar, Sidebar, Toast Muhammad Rizki
2025-03-07 19:26 ` [PATCH v1 01/10] fix(svelte): use relative false Muhammad Rizki
2025-03-07 19:26 ` [PATCH v1 02/10] fix(avatar): change avatarImg state to use from auth.user.photo state Muhammad Rizki
@ 2025-03-07 19:26 ` Muhammad Rizki
2025-03-07 19:26 ` [PATCH v1 04/10] fix(profile): make social fields default to empty string Muhammad Rizki
` (7 subsequent siblings)
10 siblings, 0 replies; 15+ messages in thread
From: Muhammad Rizki @ 2025-03-07 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 v1 04/10] fix(profile): make social fields default to empty string
2025-03-07 19:26 [PATCH v1 00/10] Relative Path, Delete Avatar, Sidebar, Toast Muhammad Rizki
` (2 preceding siblings ...)
2025-03-07 19:26 ` [PATCH v1 03/10] chore(profile): add toUpperCase() on getShortName() Muhammad Rizki
@ 2025-03-07 19:26 ` Muhammad Rizki
2025-03-07 19:26 ` [PATCH v1 05/10] chore(toaster): change toast message position and use richColors Muhammad Rizki
` (6 subsequent siblings)
10 siblings, 0 replies; 15+ messages in thread
From: Muhammad Rizki @ 2025-03-07 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 v1 05/10] chore(toaster): change toast message position and use richColors
2025-03-07 19:26 [PATCH v1 00/10] Relative Path, Delete Avatar, Sidebar, Toast Muhammad Rizki
` (3 preceding siblings ...)
2025-03-07 19:26 ` [PATCH v1 04/10] fix(profile): make social fields default to empty string Muhammad Rizki
@ 2025-03-07 19:26 ` Muhammad Rizki
2025-03-07 19:26 ` [PATCH v1 06/10] chore(profile): reset password value on success Muhammad Rizki
` (5 subsequent siblings)
10 siblings, 0 replies; 15+ messages in thread
From: Muhammad Rizki @ 2025-03-07 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 v1 06/10] chore(profile): reset password value on success
2025-03-07 19:26 [PATCH v1 00/10] Relative Path, Delete Avatar, Sidebar, Toast Muhammad Rizki
` (4 preceding siblings ...)
2025-03-07 19:26 ` [PATCH v1 05/10] chore(toaster): change toast message position and use richColors Muhammad Rizki
@ 2025-03-07 19:26 ` Muhammad Rizki
2025-03-07 19:26 ` [PATCH v1 07/10] fix(profile-avatar): add delete avatar method Muhammad Rizki
` (4 subsequent siblings)
10 siblings, 0 replies; 15+ messages in thread
From: Muhammad Rizki @ 2025-03-07 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 v1 07/10] fix(profile-avatar): add delete avatar method
2025-03-07 19:26 [PATCH v1 00/10] Relative Path, Delete Avatar, Sidebar, Toast Muhammad Rizki
` (5 preceding siblings ...)
2025-03-07 19:26 ` [PATCH v1 06/10] chore(profile): reset password value on success Muhammad Rizki
@ 2025-03-07 19:26 ` Muhammad Rizki
2025-03-07 19:26 ` [PATCH v1 08/10] chore(profile): add space for password confirmation form Muhammad Rizki
` (3 subsequent siblings)
10 siblings, 0 replies; 15+ messages in thread
From: Muhammad Rizki @ 2025-03-07 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 v1 08/10] chore(profile): add space for password confirmation form
2025-03-07 19:26 [PATCH v1 00/10] Relative Path, Delete Avatar, Sidebar, Toast Muhammad Rizki
` (6 preceding siblings ...)
2025-03-07 19:26 ` [PATCH v1 07/10] fix(profile-avatar): add delete avatar method Muhammad Rizki
@ 2025-03-07 19:26 ` Muhammad Rizki
2025-03-07 19:26 ` [PATCH v1 09/10] feat(ui): add dropdown-menu and update bits-ui version Muhammad Rizki
` (2 subsequent siblings)
10 siblings, 0 replies; 15+ messages in thread
From: Muhammad Rizki @ 2025-03-07 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 v1 09/10] feat(ui): add dropdown-menu and update bits-ui version
2025-03-07 19:26 [PATCH v1 00/10] Relative Path, Delete Avatar, Sidebar, Toast Muhammad Rizki
` (7 preceding siblings ...)
2025-03-07 19:26 ` [PATCH v1 08/10] chore(profile): add space for password confirmation form Muhammad Rizki
@ 2025-03-07 19:26 ` Muhammad Rizki
2025-03-07 19:26 ` [PATCH v1 10/10] chore(sidebar-menu): change sidebar menu look Muhammad Rizki
2025-03-08 15:18 ` [PATCH v1 00/10] Relative Path, Delete Avatar, Sidebar, Toast Ammar Faizi
10 siblings, 0 replies; 15+ messages in thread
From: Muhammad Rizki @ 2025-03-07 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 v1 10/10] chore(sidebar-menu): change sidebar menu look
2025-03-07 19:26 [PATCH v1 00/10] Relative Path, Delete Avatar, Sidebar, Toast Muhammad Rizki
` (8 preceding siblings ...)
2025-03-07 19:26 ` [PATCH v1 09/10] feat(ui): add dropdown-menu and update bits-ui version Muhammad Rizki
@ 2025-03-07 19:26 ` Muhammad Rizki
2025-03-08 15:18 ` [PATCH v1 00/10] Relative Path, Delete Avatar, Sidebar, Toast Ammar Faizi
10 siblings, 0 replies; 15+ messages in thread
From: Muhammad Rizki @ 2025-03-07 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
* Re: [PATCH v1 00/10] Relative Path, Delete Avatar, Sidebar, Toast
2025-03-07 19:26 [PATCH v1 00/10] Relative Path, Delete Avatar, Sidebar, Toast Muhammad Rizki
` (9 preceding siblings ...)
2025-03-07 19:26 ` [PATCH v1 10/10] chore(sidebar-menu): change sidebar menu look Muhammad Rizki
@ 2025-03-08 15:18 ` Ammar Faizi
2025-03-08 15:21 ` Muhammad Rizki
10 siblings, 1 reply; 15+ messages in thread
From: Ammar Faizi @ 2025-03-08 15:18 UTC (permalink / raw)
To: Muhammad Rizki; +Cc: Alviro Iskandar Setiawan, GNU/Weeb Mailing List
[-- Attachment #1: Type: text/plain, Size: 1685 bytes --]
On 3/8/25 2:26 AM, Muhammad Rizki wrote:
> Hello,
> This series fixes relative path, fixes social fields, add delete avatar
> method, change sidebar menu look, change toast message position and
> use richColors.
>
> Give it a test and let me know if there's any issue, thanks.
I tried to apply it on mail-staging.gnuweeb.org. And it makes the
current active session no longer works. It shows "500 Internal Error".
Checking on network tab, the header it sends is:
Authorization: Bearer null
Local storage inspection:
JSON.stringify(localStorage)
'{"gwm_token":"null","gwm_token_exp_at":"1742402878","gwm_uinfo":"{\\"id\\":2,\\"full_name\\":\\"Ammar Faizi\\",\\"gender\\":\\"m\\",\\"username\\":\\"ammarfaizi2\\",\\"ext_email\\":\\"[email protected]\\",\\"role\\":\\"admin\\",\\"is_active\\":\\"1\\",\\"socials\\":{\\"github_username\\":\\"ammarfaizi2\\",\\"telegram_username\\":\\"ammarfaizi2\\",\\"twitter_username\\":\\"ammarfaizi2\\",\\"discord_username\\":\\"ammarfaizi2\\"},\\"photo\\":\\"https://mail.gnuweeb.org/api.php?action=fetch_photo&f=5oGoBUc31xCxK%2FFoBUk4B8Nt9ZcZ3f%2B7dwfDIPPf9vYTyh2ziSDL%2Bt1ZJ3lmjnHlQBBsTkAMkQsEGZ2n9sEdnREmFgolrSfQImYLIzLmQ%2Bo%3D\\"}"}'
Take a look at the "gwm_token" here, it's set to "null" as a string.
That seems off. How do you determine if "gwm_token" is empty?
Testing in incognito mode works fine, but for active sessions, it
breaks and doesn't recover automatically. Instead, it continuously
throws a 500 Internal Server Error.
The only way to fix it is by manually clearing localStorage, which
is far from ideal.
The app should immediately log the user out upon receiving a
"401 Unauthorized" response.
--
Ammar Faizi
[-- Attachment #2: Screenshot from 2025-03-08 22-05-33.png --]
[-- Type: image/png, Size: 92802 bytes --]
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v1 00/10] Relative Path, Delete Avatar, Sidebar, Toast
2025-03-08 15:18 ` [PATCH v1 00/10] Relative Path, Delete Avatar, Sidebar, Toast Ammar Faizi
@ 2025-03-08 15:21 ` Muhammad Rizki
2025-03-08 15:28 ` Ammar Faizi
0 siblings, 1 reply; 15+ messages in thread
From: Muhammad Rizki @ 2025-03-08 15:21 UTC (permalink / raw)
To: Ammar Faizi; +Cc: Alviro Iskandar Setiawan, GNU/Weeb Mailing List
On 08/03/2025 22:18, Ammar Faizi wrote:
> On 3/8/25 2:26 AM, Muhammad Rizki wrote:
>> Hello,
>> This series fixes relative path, fixes social fields, add delete avatar
>> method, change sidebar menu look, change toast message position and
>> use richColors.
>>
>> Give it a test and let me know if there's any issue, thanks.
>
> I tried to apply it on mail-staging.gnuweeb.org. And it makes the
> current active session no longer works. It shows "500 Internal Error".
>
> Checking on network tab, the header it sends is:
>
> Authorization: Bearer null
>
> Local storage inspection:
>
> JSON.stringify(localStorage)
> '{"gwm_token":"null","gwm_token_exp_at":"1742402878","gwm_uinfo":"{\
> \"id\\":2,\\"full_name\\":\\"Ammar Faizi\\",\\"gender\\":\\"m\\",\
> \"username\\":\\"ammarfaizi2\\",\\"ext_email\\":\
> \"[email protected]\\",\\"role\\":\\"admin\\",\\"is_active\\":\\"1\
> \",\\"socials\\":{\\"github_username\\":\\"ammarfaizi2\\",\
> \"telegram_username\\":\\"ammarfaizi2\\",\\"twitter_username\\":\
> \"ammarfaizi2\\",\\"discord_username\\":\\"ammarfaizi2\\"},\\"photo\\":\
> \"https://mail.gnuweeb.org/api.php?
> action=fetch_photo&f=5oGoBUc31xCxK%2FFoBUk4B8Nt9ZcZ3f%2B7dwfDIPPf9vYTyh2ziSDL%2Bt1ZJ3lmjnHlQBBsTkAMkQsEGZ2n9sEdnREmFgolrSfQImYLIzLmQ%2Bo%3D\\"}"}'
>
> Take a look at the "gwm_token" here, it's set to "null" as a string.
> That seems off. How do you determine if "gwm_token" is empty?
>
> Testing in incognito mode works fine, but for active sessions, it
> breaks and doesn't recover automatically. Instead, it continuously
> throws a 500 Internal Server Error.
>
> The only way to fix it is by manually clearing localStorage, which
> is far from ideal.
>
> The app should immediately log the user out upon receiving a
> "401 Unauthorized" response.
>
can you try to clear the localStorage? it could be because of previous
state of user_info is not synchronized with current API structure.
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v1 00/10] Relative Path, Delete Avatar, Sidebar, Toast
2025-03-08 15:21 ` Muhammad Rizki
@ 2025-03-08 15:28 ` Ammar Faizi
2025-03-08 15:46 ` Muhammad Rizki
0 siblings, 1 reply; 15+ messages in thread
From: Ammar Faizi @ 2025-03-08 15:28 UTC (permalink / raw)
To: Muhammad Rizki; +Cc: Alviro Iskandar Setiawan, GNU/Weeb Mailing List
On Sat, Mar 08, 2025 at 10:21:09PM +0700, Muhammad Rizki wrote:
> can you try to clear the localStorage?
As already explained, it works if I clear the localStorage manually.
> it could be because of previous state
> of user_info is not synchronized with current API structure.
No. After clearing the localStorage, I tried to relogin and purposely
invalidate gwm_token by doing:
localStorage.setItem("gwm_token", "aaaaaa")
I expect I get logged out immediately after reload. But no, it throws
"500 Internal Error" on invalid token.
Invalid or expired token should not trap user in "500 Internal Error"
page.
And whatever the user throws at you, you are not allowed to crash. You
must recover from unexpected scenario.
--
Ammar Faizi
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v1 00/10] Relative Path, Delete Avatar, Sidebar, Toast
2025-03-08 15:28 ` Ammar Faizi
@ 2025-03-08 15:46 ` Muhammad Rizki
0 siblings, 0 replies; 15+ messages in thread
From: Muhammad Rizki @ 2025-03-08 15:46 UTC (permalink / raw)
To: Ammar Faizi
Cc: Alviro Iskandar Setiawan, GNU/Weeb Mailing List, Muhammad Rizki
On 08/03/2025 22:28, Ammar Faizi wrote:
> On Sat, Mar 08, 2025 at 10:21:09PM +0700, Muhammad Rizki wrote:
>> can you try to clear the localStorage?
>
> As already explained, it works if I clear the localStorage manually.
>
>> it could be because of previous state
>> of user_info is not synchronized with current API structure.
>
> No. After clearing the localStorage, I tried to relogin and purposely
> invalidate gwm_token by doing:
>
> localStorage.setItem("gwm_token", "aaaaaa")
>
> I expect I get logged out immediately after reload. But no, it throws
> "500 Internal Error" on invalid token.
>
> Invalid or expired token should not trap user in "500 Internal Error"
> page.
>
> And whatever the user throws at you, you are not allowed to crash. You
> must recover from unexpected scenario.
>
what I'm trying to say is:
the `null` value is because of API structure changes, but yeah good
catch there, if user manually set the state it should throw user to
login page, I will try to patch that issue, thanks.
^ permalink raw reply [flat|nested] 15+ messages in thread