Skip to content

Commit fd26671

Browse files
committed
Merge branch 'kiizuha' (patches from Muhammad Rizki)
"This series fixes auth guard to prevent users calling API with invalid credentials, they will redirected to the login page if invalid credentials occurs." * kiizuha: fix(auth): fix auth guard when credentials is invalid chore(seo): move seo from layout to /home page chore(sidebar-menu): change sidebar menu look feat(ui): add dropdown-menu and update bits-ui version chore(profile): add space for password confirmation form fix(profile-avatar): add delete avatar method chore(profile): reset password value on success chore(toaster): change toast message position and use richColors fix(profile): make social fields default to empty string chore(profile): add toUpperCase() on getShortName() fix(avatar): change avatarImg state to use from auth.user.photo state fix(svelte): use relative false Link: https://lore.gnuweeb.org/gwml/[email protected] Signed-off-by: Ammar Faizi <[email protected]>
2 parents d84bcea + 6e9649a commit fd26671

25 files changed

+590
-144
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"@sveltejs/kit": "^2.0.0",
1919
"@sveltejs/vite-plugin-svelte": "^4.0.0",
2020
"autoprefixer": "^10.4.20",
21-
"bits-ui": "^1.3.5",
21+
"bits-ui": "^1.3.6",
2222
"clsx": "^2.1.1",
2323
"eslint": "^9.7.0",
2424
"eslint-config-prettier": "^9.1.0",

src/lib/components/customs/app-sidebar.svelte

Lines changed: 154 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
import { navigations } from "$constants";
33
import Separator from "$components/ui/separator/separator.svelte";
44
import * as Sidebar from "$lib/components/ui/sidebar";
5+
import * as Avatar from "$lib/components/ui/avatar";
6+
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
7+
import * as Dialog from "$lib/components/ui/dialog";
58
import { useAuth } from "$lib/hooks/auth.svelte";
69
import LogOut from "lucide-svelte/icons/log-out";
710
import Mails from "lucide-svelte/icons/mails";
@@ -14,17 +17,27 @@
1417
import { crossfade } from "svelte/transition";
1518
import { cubicInOut } from "svelte/easing";
1619
import IconRoundcube from "$components/icons/icon-roundcube.svelte";
20+
import Loading from "./loading.svelte";
21+
import ChevronsUpDown from "lucide-svelte/icons/chevrons-up-down";
1722
1823
let { ref = $bindable(null), ...restProps }: ComponentProps<typeof Sidebar.Root> = $props();
1924
2025
const auth = useAuth();
2126
const sidebar = Sidebar.useSidebar();
2227
28+
let showModalConfirmation = $state(false);
29+
2330
const [send, receive] = crossfade({
2431
duration: 250,
2532
easing: cubicInOut
2633
});
2734
35+
const getShortName = () => {
36+
const fullName = auth.user?.full_name ?? "";
37+
const match = fullName.match(/\b(\w)/g) ?? [];
38+
return match.slice(0, 2).join("").toUpperCase();
39+
};
40+
2841
const handleNavigationMobile = () => {
2942
if (!sidebar.isMobile) return;
3043
sidebar.toggle();
@@ -36,93 +49,158 @@
3649
};
3750
</script>
3851

39-
<Sidebar.Root bind:ref variant="inset" collapsible="icon" {...restProps}>
40-
<Sidebar.Content>
41-
<Sidebar.Group>
42-
<Sidebar.Header>
43-
<Sidebar.Menu>
44-
<Sidebar.MenuItem>
45-
<Sidebar.MenuButton onclick={() => sidebar.toggle()} size="lg">
46-
<div
47-
class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"
48-
>
49-
<Mails class="size-4" />
50-
</div>
51-
<div class="grid flex-1 text-left text-sm leading-tight">
52-
<span class="truncate font-semibold">{auth.user?.full_name}</span>
53-
<span class="truncate text-xs">@{auth.user?.username}</span>
54-
</div>
55-
</Sidebar.MenuButton>
56-
</Sidebar.MenuItem>
57-
</Sidebar.Menu>
58-
</Sidebar.Header>
59-
<Separator class="mb-3" />
60-
<Sidebar.GroupContent>
61-
<Sidebar.Menu>
62-
{#each navigations as item (item.name)}
52+
<Dialog.Root open={showModalConfirmation} onOpenChange={(e) => (showModalConfirmation = e)}>
53+
<Sidebar.Root bind:ref variant="inset" collapsible="icon" {...restProps}>
54+
<Sidebar.Content>
55+
<Sidebar.Group>
56+
<Sidebar.Header>
57+
<Sidebar.Menu>
6358
<Sidebar.MenuItem>
64-
{@const isActive = page.url.pathname.startsWith(item.url)}
65-
<Sidebar.MenuButton {isActive} class="relative">
59+
<Sidebar.MenuButton onclick={() => sidebar.toggle()} size="lg">
60+
<div
61+
class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"
62+
>
63+
<Mails class="size-4" />
64+
</div>
65+
<h2 class="shrink-0 text-lg font-semibold">G/W Mail</h2>
66+
</Sidebar.MenuButton>
67+
</Sidebar.MenuItem>
68+
</Sidebar.Menu>
69+
</Sidebar.Header>
70+
<Separator class="mb-3" />
71+
<Sidebar.GroupContent>
72+
<Sidebar.Menu>
73+
{#each navigations as item (item.name)}
74+
<Sidebar.MenuItem>
75+
{@const isActive = page.url.pathname.startsWith(item.url)}
76+
<Sidebar.MenuButton {isActive} class="relative">
77+
{#snippet child({ props })}
78+
{@const className = props.class as string}
79+
<a
80+
href={item.url}
81+
onclick={handleNavigationMobile}
82+
{...props}
83+
class={cn("relative z-10", className)}
84+
>
85+
<item.icon />
86+
<span>{item.name}</span>
87+
</a>
88+
{#if isActive}
89+
<!-- svelte-ignore element_invalid_self_closing_tag -->
90+
<div
91+
class={cn(
92+
"absolute inset-0 rounded-md bg-sidebar-accent",
93+
isActive &&
94+
"overflow-hidden before:absolute before:left-0 before:h-full before:w-0.5 before:bg-foreground"
95+
)}
96+
in:send={{ key: "active-sidebar-tab" }}
97+
out:receive={{ key: "active-sidebar-tab" }}
98+
/>
99+
{/if}
100+
{/snippet}
101+
</Sidebar.MenuButton>
102+
</Sidebar.MenuItem>
103+
{/each}
104+
<Sidebar.MenuItem>
105+
<Sidebar.MenuButton>
66106
{#snippet child({ props })}
67107
{@const className = props.class as string}
68108
<a
69-
href={item.url}
70-
onclick={handleNavigationMobile}
109+
href="https://mail.gnuweeb.org/roundcube/"
71110
{...props}
72-
class={cn("relative z-10", className)}
111+
class={cn("group/roundcube", className)}
112+
target="_blank"
73113
>
74-
<item.icon />
75-
<span>{item.name}</span>
76-
</a>
77-
{#if isActive}
78-
<!-- svelte-ignore element_invalid_self_closing_tag -->
79-
<div
80-
class={cn(
81-
"absolute inset-0 rounded-md bg-sidebar-accent",
82-
isActive &&
83-
"overflow-hidden before:absolute before:left-0 before:h-full before:w-0.5 before:bg-foreground"
84-
)}
85-
in:send={{ key: "active-sidebar-tab" }}
86-
out:receive={{ key: "active-sidebar-tab" }}
114+
<IconRoundcube />
115+
<span>Roundcube</span>
116+
<SquareArrowOutUpRight
117+
class="!size-3 transition-transform group-hover/roundcube:!scale-125"
87118
/>
88-
{/if}
119+
</a>
89120
{/snippet}
90121
</Sidebar.MenuButton>
91122
</Sidebar.MenuItem>
92-
{/each}
93-
<Sidebar.MenuItem>
94-
<Sidebar.MenuButton>
123+
</Sidebar.Menu>
124+
</Sidebar.GroupContent>
125+
</Sidebar.Group>
126+
</Sidebar.Content>
127+
<Sidebar.Footer>
128+
<Sidebar.Menu>
129+
<Sidebar.MenuItem>
130+
<DropdownMenu.Root>
131+
<DropdownMenu.Trigger>
95132
{#snippet child({ props })}
96-
{@const className = props.class as string}
97-
<a
98-
href="https://mail.gnuweeb.org/roundcube/"
133+
<Sidebar.MenuButton
134+
size="lg"
135+
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
99136
{...props}
100-
class={cn("group/roundcube", className)}
101-
target="_blank"
102137
>
103-
<IconRoundcube />
104-
<span>Roundcube</span>
105-
<SquareArrowOutUpRight
106-
class="!size-3 transition-transform group-hover/roundcube:!scale-125"
107-
/>
108-
</a>
138+
<Avatar.Root class="h-8 w-8 rounded-lg">
139+
<Avatar.Image src={auth.user?.photo} alt="{auth.user?.username}@gnuweeb.org" />
140+
<Avatar.Fallback class="rounded-lg text-xs">
141+
{#if !Boolean(auth.user?.photo)}
142+
{getShortName()}
143+
{:else}
144+
<Loading class="size-3 text-black" />
145+
{/if}
146+
</Avatar.Fallback>
147+
</Avatar.Root>
148+
<div class="grid flex-1 text-left text-sm leading-tight">
149+
<span class="truncate font-semibold">{auth.user?.full_name}</span>
150+
<span class="truncate text-xs">{auth.user?.username}@gnuweeb.org</span>
151+
</div>
152+
<ChevronsUpDown class="ml-auto size-4" />
153+
</Sidebar.MenuButton>
109154
{/snippet}
110-
</Sidebar.MenuButton>
111-
</Sidebar.MenuItem>
112-
</Sidebar.Menu>
113-
</Sidebar.GroupContent>
114-
</Sidebar.Group>
115-
</Sidebar.Content>
116-
<Sidebar.Footer>
117-
<Sidebar.Menu>
118-
<Button
119-
variant="destructive"
120-
onclick={handleLogout}
121-
class="flex w-full items-center justify-start"
122-
>
123-
<LogOut />
124-
<span>Logout</span>
125-
</Button>
126-
</Sidebar.Menu>
127-
</Sidebar.Footer>
128-
</Sidebar.Root>
155+
</DropdownMenu.Trigger>
156+
<DropdownMenu.Content
157+
class="w-[var(--bits-dropdown-menu-anchor-width)] min-w-56 rounded-lg"
158+
side={sidebar.isMobile ? "bottom" : "right"}
159+
align="end"
160+
sideOffset={4}
161+
>
162+
<DropdownMenu.Label class="p-0 font-normal">
163+
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
164+
<Avatar.Root class="h-8 w-8 rounded-lg">
165+
<Avatar.Image src={auth.user?.photo} alt="{auth.user?.username}@gnuweeb.org" />
166+
<Avatar.Fallback class="rounded-lg text-xs">
167+
{#if !Boolean(auth.user?.photo)}
168+
{getShortName()}
169+
{:else}
170+
<Loading class="size-3 text-black" />
171+
{/if}
172+
</Avatar.Fallback>
173+
</Avatar.Root>
174+
<div class="grid flex-1 text-left text-sm leading-tight">
175+
<span class="truncate font-semibold">{auth.user?.full_name}</span>
176+
<span class="truncate text-xs">{auth.user?.username}@gnuweeb.org</span>
177+
</div>
178+
</div>
179+
</DropdownMenu.Label>
180+
<DropdownMenu.Separator />
181+
<DropdownMenu.Item
182+
onclick={() => (showModalConfirmation = true)}
183+
class="text-destructive hover:!text-destructive"
184+
>
185+
<LogOut />
186+
Log out
187+
</DropdownMenu.Item>
188+
</DropdownMenu.Content>
189+
</DropdownMenu.Root>
190+
</Sidebar.MenuItem>
191+
</Sidebar.Menu>
192+
</Sidebar.Footer>
193+
</Sidebar.Root>
194+
<Dialog.Content class="sm:max-w-[425px]">
195+
<Dialog.Header>
196+
<Dialog.Title>Logout Confirmation</Dialog.Title>
197+
<Dialog.Description>Confirm logout from your profile.</Dialog.Description>
198+
</Dialog.Header>
199+
<p class="text-sm font-medium">
200+
You are about to logout from your profile, click logout button below to proceed.
201+
</p>
202+
<Dialog.Footer>
203+
<Button type="submit" variant="destructive" onclick={handleLogout}>Logout</Button>
204+
</Dialog.Footer>
205+
</Dialog.Content>
206+
</Dialog.Root>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<script lang="ts">
2+
import { DropdownMenu as DropdownMenuPrimitive, type WithoutChildrenOrChild } from "bits-ui";
3+
import Check from "lucide-svelte/icons/check";
4+
import Minus from "lucide-svelte/icons/minus";
5+
import { cn } from "$utils";
6+
import type { Snippet } from "svelte";
7+
8+
let {
9+
ref = $bindable(null),
10+
class: className,
11+
children: childrenProp,
12+
checked = $bindable(false),
13+
indeterminate = $bindable(false),
14+
...restProps
15+
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
16+
children?: Snippet;
17+
} = $props();
18+
</script>
19+
20+
<DropdownMenuPrimitive.CheckboxItem
21+
bind:ref
22+
bind:checked
23+
bind:indeterminate
24+
class={cn(
25+
"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",
26+
className
27+
)}
28+
{...restProps}
29+
>
30+
{#snippet children({ checked, indeterminate })}
31+
<span class="absolute left-2 flex size-3.5 items-center justify-center">
32+
{#if indeterminate}
33+
<Minus class="size-4" />
34+
{:else}
35+
<Check class={cn("size-4", !checked && "text-transparent")} />
36+
{/if}
37+
</span>
38+
{@render childrenProp?.()}
39+
{/snippet}
40+
</DropdownMenuPrimitive.CheckboxItem>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<script lang="ts">
2+
import { cn } from "$utils";
3+
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
4+
5+
let {
6+
ref = $bindable(null),
7+
class: className,
8+
sideOffset = 4,
9+
portalProps,
10+
...restProps
11+
}: DropdownMenuPrimitive.ContentProps & {
12+
portalProps?: DropdownMenuPrimitive.PortalProps;
13+
} = $props();
14+
</script>
15+
16+
<DropdownMenuPrimitive.Portal {...portalProps}>
17+
<DropdownMenuPrimitive.Content
18+
bind:ref
19+
{sideOffset}
20+
class={cn(
21+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
22+
"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",
23+
className
24+
)}
25+
{...restProps}
26+
/>
27+
</DropdownMenuPrimitive.Portal>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<script lang="ts">
2+
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
3+
import { cn } from "$utils";
4+
5+
let {
6+
ref = $bindable(null),
7+
class: className,
8+
inset,
9+
...restProps
10+
}: DropdownMenuPrimitive.GroupHeadingProps & {
11+
inset?: boolean;
12+
} = $props();
13+
</script>
14+
15+
<DropdownMenuPrimitive.GroupHeading
16+
bind:ref
17+
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
18+
{...restProps}
19+
/>

0 commit comments

Comments
 (0)