Skip to content

Commit b9c388d

Browse files
committed
feat: notification integration
1 parent f7ec00e commit b9c388d

File tree

12 files changed

+755
-90
lines changed

12 files changed

+755
-90
lines changed

app/(protected)/layout.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
1-
export default function ProtectedLayout({
1+
import { NavUser } from '@/components/nav-user'
2+
import { IsLoggedIn } from '@/lib/firebase/firebase'
3+
4+
export default async function ProtectedLayout({
25
children,
36
}: Readonly<{ children: React.ReactNode }>) {
4-
return <div className="container mx-auto px-4 my-4">{children}</div>
7+
const claim = await IsLoggedIn()
8+
return (
9+
<div className="container mx-auto px-4 my-4">
10+
<nav className="flex items-center justify-between">
11+
<div>LibrarEase</div>
12+
{claim && (
13+
<NavUser
14+
user={{
15+
id: claim.librarease.id,
16+
avatar: 'https://github.com/agmmtoo.png',
17+
email: claim.email ?? '',
18+
name: claim.librarease.role,
19+
}}
20+
/>
21+
)}
22+
</nav>
23+
24+
{children}
25+
</div>
26+
)
527
}

app/page.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
import { Button } from '@/components/ui/button'
1313
import { IsLoggedIn } from '@/lib/firebase/firebase'
1414
import Landing from '@/components/landing'
15-
import { cookies } from 'next/headers'
15+
import { logoutAction } from '@/lib/actions/logout'
1616

1717
const menuItems = [
1818
{ title: 'Dashboard', icon: ChartSpline, href: '/dashboard', level: 3 },
@@ -30,13 +30,6 @@ const menuItems = [
3030
{ title: 'Borrows', icon: BookCopy, href: '/borrows', level: 2 },
3131
]
3232

33-
async function logoutAction() {
34-
'use server'
35-
const cookieStore = await cookies()
36-
const sessionName = process.env.SESSION_COOKIE_NAME as string
37-
cookieStore.delete(sessionName)
38-
}
39-
4033
export default async function LibraryDashboard() {
4134
const claim = await IsLoggedIn()
4235

components/hooks/use-toast.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as React from 'react'
55

66
import type { ToastActionElement, ToastProps } from '@/components/ui/toast'
77

8-
const TOAST_LIMIT = 1
8+
const TOAST_LIMIT = 2
99
const TOAST_REMOVE_DELAY = 1000000
1010

1111
type ToasterToast = ToastProps & {

components/nav-user.tsx

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
'use client'
2+
3+
import {
4+
BellIcon,
5+
CreditCardIcon,
6+
LogOutIcon,
7+
UserCircleIcon,
8+
} from 'lucide-react'
9+
10+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
11+
import {
12+
DropdownMenu,
13+
DropdownMenuContent,
14+
DropdownMenuGroup,
15+
DropdownMenuItem,
16+
DropdownMenuLabel,
17+
DropdownMenuSeparator,
18+
DropdownMenuTrigger,
19+
} from '@/components/ui/dropdown-menu'
20+
import { Button } from './ui/button'
21+
import { useEffect } from 'react'
22+
import { useToast } from './hooks/use-toast'
23+
import { Notification } from '@/lib/types/notification'
24+
import { streamNoti } from '@/lib/api/noti'
25+
import { logoutAction } from '@/lib/actions/logout'
26+
27+
export function NavUser({
28+
user,
29+
}: {
30+
user: {
31+
id: string
32+
name: string
33+
email: string
34+
avatar: string
35+
}
36+
}) {
37+
const { toast } = useToast()
38+
39+
useEffect(() => {
40+
function onMessage(data: Notification) {
41+
toast({
42+
title: data.title,
43+
description: data.message,
44+
variant: 'default',
45+
})
46+
}
47+
function onError(event: Event) {
48+
toast({
49+
title: 'Error',
50+
description: JSON.stringify(event),
51+
variant: 'destructive',
52+
})
53+
}
54+
55+
const cleanup = streamNoti(user.id, {
56+
onMessage: onMessage,
57+
onError: onError,
58+
onConnect: console.log,
59+
})
60+
return cleanup
61+
}, [user.id, toast])
62+
63+
return (
64+
<DropdownMenu>
65+
<DropdownMenuTrigger asChild>
66+
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
67+
<Avatar className="w-8 h-8">
68+
<AvatarImage src={user.avatar} alt={user.name} />
69+
<AvatarFallback className="rounded-lg">AO</AvatarFallback>
70+
</Avatar>
71+
</Button>
72+
</DropdownMenuTrigger>
73+
<DropdownMenuContent
74+
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
75+
// side={isMobile ? "bottom" : "right"}
76+
side="bottom"
77+
align="end"
78+
sideOffset={4}
79+
>
80+
<DropdownMenuLabel className="p-0 font-normal">
81+
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
82+
<Avatar className="h-8 w-8 rounded-lg">
83+
<AvatarImage src={user.avatar} alt={user.name} />
84+
<AvatarFallback className="rounded-lg">AO</AvatarFallback>
85+
</Avatar>
86+
<div className="grid flex-1 text-left text-sm leading-tight">
87+
<span className="truncate font-medium">{user.name}</span>
88+
<span className="truncate text-xs text-muted-foreground">
89+
{user.email}
90+
</span>
91+
</div>
92+
</div>
93+
</DropdownMenuLabel>
94+
<DropdownMenuSeparator />
95+
<DropdownMenuGroup>
96+
<DropdownMenuItem>
97+
<UserCircleIcon />
98+
Account
99+
</DropdownMenuItem>
100+
<DropdownMenuItem>
101+
<CreditCardIcon />
102+
Billing
103+
</DropdownMenuItem>
104+
<DropdownMenuItem>
105+
<BellIcon />
106+
Notifications
107+
</DropdownMenuItem>
108+
</DropdownMenuGroup>
109+
<DropdownMenuSeparator />
110+
<DropdownMenuItem onClick={logoutAction}>
111+
<LogOutIcon />
112+
Log out
113+
</DropdownMenuItem>
114+
</DropdownMenuContent>
115+
</DropdownMenu>
116+
)
117+
}

components/ui/avatar.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"use client"
2+
3+
import * as React from "react"
4+
import * as AvatarPrimitive from "@radix-ui/react-avatar"
5+
6+
import { cn } from "@/lib/utils"
7+
8+
function Avatar({
9+
className,
10+
...props
11+
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
12+
return (
13+
<AvatarPrimitive.Root
14+
data-slot="avatar"
15+
className={cn(
16+
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
17+
className
18+
)}
19+
{...props}
20+
/>
21+
)
22+
}
23+
24+
function AvatarImage({
25+
className,
26+
...props
27+
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
28+
return (
29+
<AvatarPrimitive.Image
30+
data-slot="avatar-image"
31+
className={cn("aspect-square size-full", className)}
32+
{...props}
33+
/>
34+
)
35+
}
36+
37+
function AvatarFallback({
38+
className,
39+
...props
40+
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
41+
return (
42+
<AvatarPrimitive.Fallback
43+
data-slot="avatar-fallback"
44+
className={cn(
45+
"bg-muted flex size-full items-center justify-center rounded-full",
46+
className
47+
)}
48+
{...props}
49+
/>
50+
)
51+
}
52+
53+
export { Avatar, AvatarImage, AvatarFallback }

0 commit comments

Comments
 (0)