Skip to content

Commit

Permalink
Add notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
arjunkomath committed Jan 15, 2025
1 parent 7ee63af commit bd591b4
Showing 12 changed files with 1,339 additions and 21 deletions.
16 changes: 15 additions & 1 deletion app/(dashboard)/[tenant]/settings/actions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use server";

import { logtoConfig } from "@/app/logto";
import { user } from "@/drizzle/schema";
import { notification, user } from "@/drizzle/schema";
import { updateUser } from "@/lib/ops/auth";
import { database } from "@/lib/utils/useDatabase";
import { getOwner } from "@/lib/utils/useOwner";
@@ -43,6 +43,20 @@ export async function updateUserData(payload: FormData) {
return { success: true };
}

export async function getUserNotifications() {
const { userId } = await getOwner();

const db = await database();
const notifications = await db.query.notification.findMany({
where: eq(notification.userId, userId),
with: {
user: true,
},
});

return notifications;
}

export async function logout() {
await signOut(logtoConfig);
}
5 changes: 4 additions & 1 deletion components/console/navbar.tsx
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ import Image from "next/image";
import Link from "next/link";
import logo from "../../public/images/logo.png";
import { OrgSwitcher, ProjectSwitcher, UserButton } from "../core/auth";
import { Notifications } from "../ui/popover-with-notificaition";
import NavBarLinks from "./navbar-links";

export default function NavBar({
@@ -52,7 +53,9 @@ export default function NavBar({
<ProjectSwitcher projects={projects} />
</div>

<div className="ml-2 flex justify-center">
<div className="ml-2 flex justify-center space-x-4">
<Notifications />

<UserButton orgSlug={activeOrgSlug} />
</div>
</div>
6 changes: 1 addition & 5 deletions components/core/auth.tsx
Original file line number Diff line number Diff line change
@@ -137,11 +137,7 @@ export const UserButton = ({ orgSlug }: { orgSlug: string }) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="icon"
className="overflow-hidden rounded-full"
>
<Button variant="outline" size="icon" className="overflow-hidden">
<User className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
122 changes: 122 additions & 0 deletions components/ui/popover-with-notificaition.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"use client";

import { getUserNotifications } from "@/app/(dashboard)/[tenant]/settings/actions";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { NotificationWithUser } from "@/drizzle/types";
import { Bell } from "lucide-react";
import { useCallback, useState } from "react";

function Dot({ className }: { className?: string }) {
return (
<svg
width="6"
height="6"
fill="currentColor"
viewBox="0 0 6 6"
xmlns="http://www.w3.org/2000/svg"
className={className}
aria-hidden="true"
>
<circle cx="3" cy="3" r="3" />
</svg>
);
}

function Notifications() {
const [notifications, setNotifications] = useState<NotificationWithUser[]>([]);
const unreadCount = notifications.filter((n) => !n.read).length;

const handleMarkAllAsRead = () => {
setNotifications(
notifications.map((notification) => ({
...notification,
unread: false,
})),
);
};

const handleNotificationClick = (id: number) => {
setNotifications(
notifications.map((notification) =>
notification.id === id ? { ...notification, unread: false } : notification,
),
);
};

const fetchNotifications = useCallback(async () => {
getUserNotifications().then(setNotifications);
}, []);

return (
<Popover onOpenChange={fetchNotifications}>
<PopoverTrigger asChild>
<Button size="icon" variant="outline" className="relative" aria-label="Open notifications">
<Bell size={16} strokeWidth={2} aria-hidden="true" />
{unreadCount > 0 && (
<Badge
className="absolute -top-2 left-full -translate-x-1/2 px-1 rounded-full h-5 min-w-[20px] flex items-center justify-center"
>
{unreadCount > 99 ? "99+" : unreadCount}
</Badge>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 p-1">
<div className="flex items-baseline justify-between gap-4 px-3 py-2">
<div className="text-sm font-semibold">Notifications</div>
{unreadCount > 0 && (
<button className="text-xs font-medium hover:underline" onClick={handleMarkAllAsRead}>
Mark all as read
</button>
)}
</div>
<div
role="separator"
aria-orientation="horizontal"
className="-mx-1 my-1 h-px bg-border"
></div>
{notifications.map((notification) => (
<div
key={notification.id}
className="rounded-md px-3 py-2 text-sm transition-colors hover:bg-accent"
>
<div className="relative flex items-start gap-3 pe-3">
{/* <img
className="size-9 rounded-md"
src={notification.image}
width={32}
height={32}
alt={notification.user}
/> */}
<div className="flex-1 space-y-1">
<button
className="text-left text-foreground/80 after:absolute after:inset-0"
onClick={() => handleNotificationClick(notification.id)}
>
<span className="font-medium text-foreground hover:underline">
{notification.user.firstName}
</span>{" "}
{notification.target}{" "}
<span className="font-medium text-foreground hover:underline">
{notification.target}
</span>
.
</button>
<div className="text-xs text-muted-foreground">{notification.createdAt.toLocaleDateString()}</div>
</div>
{!notification.read ? (
<div className="absolute end-0 self-center">
<Dot />
</div>
) : null}
</div>
</div>
))}
</PopoverContent>
</Popover>
);
}

export { Notifications }
7 changes: 7 additions & 0 deletions drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from "drizzle-kit";

export default defineConfig({
dialect: "sqlite",
schema: "./drizzle/schema.ts",
out: "./drizzle",
});
10 changes: 10 additions & 0 deletions drizzle/0008_optimal_obadiah_stane.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CREATE TABLE `Notification` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`type` text,
`message` text NOT NULL,
`target` text NOT NULL,
`read` integer DEFAULT false NOT NULL,
`createdAt` integer NOT NULL,
`userId` text NOT NULL,
FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON UPDATE cascade ON DELETE cascade
);
Loading

0 comments on commit bd591b4

Please sign in to comment.