Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add novu notification bell #3

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ AUTH_TRUST_HOST=""
MAILCHIMP_API_KEY=""
MAILCHIMP_LIST_ID=""
REDIS_URL=""
NOVU_SECRET_KEY=""
NEXT_PUBLIC_NOVU_APP_ID=""
15 changes: 15 additions & 0 deletions .github/workflows/novu.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# .github/workflows/novu.yml
name: Novu Sync

on:
workflow_dispatch:

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Sync State to Novu
uses: novuhq/actions-novu-sync@v2
with:
secret-key: ${{ secrets.NOVU_SECRET_KEY }}
bridge-url: <YOUR_DEPLOYED_BRIDGE_URL>
50 changes: 42 additions & 8 deletions apps/frontend/src/app/api/dashboard/messages/route.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { auth } from "@frontend/auth";
import { prisma } from "@db/prisma";
import { auth } from '@frontend/auth';
import { prisma } from '@db/prisma';
import { Novu } from '@novu/node';

const novu = new Novu(process.env['NOVU_SECRET_KEY'] as string);

export const GET = auth(async (req, res) => {
const findSquad = await prisma.user.findFirst({
Expand All @@ -16,7 +19,7 @@ export const GET = auth(async (req, res) => {
squadId: findSquad?.squadId!,
},
orderBy: {
createdAt: "asc",
createdAt: 'asc',
},
select: {
id: true,
Expand All @@ -27,8 +30,8 @@ export const GET = auth(async (req, res) => {
name: true,
profilePicture: true,
handle: true,
}
}
},
},
},
});

Expand All @@ -38,7 +41,7 @@ export const GET = auth(async (req, res) => {
export const POST = auth(async (req, res) => {
const body = await req?.json();
if (!body.message || body.message.length < 3) {
return Response.json({ message: "Message is required" });
return Response.json({ message: 'Message is required' });
}

const findSquad = await prisma.user.findFirst({
Expand All @@ -50,13 +53,44 @@ export const POST = auth(async (req, res) => {
},
});

await prisma.squadMessages.create({
const squadeUsersAndSquadName = await prisma.squad.findFirst({
where: {
id: findSquad?.squadId!,
},
select: {
members: true,
name: true,
},
});

const novuSubscribers = squadeUsersAndSquadName?.members.map((member) => {
const firstName = member?.name?.split(' ')[0];
const lastName = member?.name?.split(' ')[1] ?? null;
return {
subscriberId: member.id,
email: member.email,
firstName: firstName,
lastName: lastName,
};
}) as [];

console.log(novuSubscribers);
if (novuSubscribers.length) {
await novu.trigger('devfest-chat-message', {
to: novuSubscribers,
payload: {
message: `Your **${squadeUsersAndSquadName?.name}** squad has a new message - **${body.message}**`,
},
});
}

await await prisma.squadMessages.create({
data: {
squadId: findSquad?.squadId!,
message: body.message!,
userId: req?.auth?.user?.id!,
},
});

return Response.json({ message: "Added" });
return Response.json({ message: 'Added' });
});
62 changes: 31 additions & 31 deletions apps/frontend/src/auth.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import { PrismaAdapter } from "@auth/prisma-adapter";
import NextAuth from 'next-auth';
import GitHub from 'next-auth/providers/github';
import { PrismaAdapter } from '@auth/prisma-adapter';
import { prisma } from '@db/prisma';

export const { handlers, signIn, signOut, auth } = NextAuth({
adapter: PrismaAdapter(prisma),
providers: [GitHub],
session: { strategy: "jwt" },
session: { strategy: 'jwt' },
cookies: {
pkceCodeVerifier: {
name: 'next-auth.pkce.code_verifier',
Expand All @@ -20,7 +20,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
},
callbacks: {
async jwt({ token, user, trigger, session, account }) {
if (trigger === "update" && session?.color) {
if (trigger === 'update' && session?.color) {
token.color = session.color;
}
if (user) {
Expand All @@ -38,31 +38,31 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
token.isMod = user.isMod;
}

if (trigger === "signUp") {
const [key, country] = process.env.MAILCHIMP_API_KEY!.split("-");
const [name, lastName] = user?.name?.split(" ")!;
if (trigger === 'signUp') {
// const [key, country] = process.env.MAILCHIMP_API_KEY!.split("-");
// const [name, lastName] = user?.name?.split(" ")!;

await fetch(
`https://${country}.api.mailchimp.com/3.0/lists/${process.env.MAILCHIMP_LIST_ID}/members`,
{
method: "POST",
headers: {
Authorization: `Basic ${Buffer.from("nevo:" + key).toString("base64")}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
email_address: user.email,
status: "subscribed",
merge_fields: {
FNAME: name || "",
LNAME: lastName || "",
},
}),
},
);
// await fetch(
// `https://${country}.api.mailchimp.com/3.0/lists/${process.env.MAILCHIMP_LIST_ID}/members`,
// {
// method: "POST",
// headers: {
// Authorization: `Basic ${Buffer.from("nevo:" + key).toString("base64")}`,
// "Content-Type": "application/json",
// },
// body: JSON.stringify({
// email_address: user.email,
// status: "subscribed",
// merge_fields: {
// FNAME: name || "",
// LNAME: lastName || "",
// },
// }),
// },
// );

const { login, avatar_url } = await (
await fetch("https://api.github.com/user", {
await fetch('https://api.github.com/user', {
headers: {
Authorization: `Bearer ${account?.access_token}`,
},
Expand Down Expand Up @@ -104,16 +104,16 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
},
authorized: async ({ auth, request }) => {
// Logged in users are authenticated, otherwise redirect to login page
if (!auth && request.url.indexOf("/api") > -1) {
if (!auth && request.url.indexOf('/api') > -1) {
return Response.json(
{ message: "You have to login" },
{ message: 'You have to login' },
{
status: 401,
},
}
);
}
if (!auth) {
return Response.redirect(new URL("/", request.url));
return Response.redirect(new URL('/', request.url));
}

return true;
Expand Down
13 changes: 9 additions & 4 deletions apps/frontend/src/components/dashboard/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ const SendMessage: FC<{ update: () => void }> = (props) => {
const submit = useCallback(
async (e: any) => {
e.preventDefault();
if (value.length < 3) {
return alert('You have to type at least 3 characters');
}

await fetch('/api/dashboard/messages', {
method: 'POST',
body: JSON.stringify({ message: value }),
});
if (value.length < 3) {
return alert('You have to type at least 3 characters');
}

setValue('');
props.update();
},
Expand Down Expand Up @@ -128,7 +130,10 @@ export function Chat() {
<br />
<br />
Make sure you create your{' '}
<a href="/dashboard/ticket" className="underline hover:font-bold">
<a
href="/dashboard/ticket"
className="underline hover:font-bold"
>
personal ticket
</a>{' '}
and share it.
Expand Down
10 changes: 10 additions & 0 deletions apps/frontend/src/components/layout/wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Footer } from '@frontend/sections';
import { useSession, signOut } from 'next-auth/react';
import { Toaster } from 'react-hot-toast';
import dynamic from 'next/dynamic';
import { NotificationBell } from '../notification/novu';
import clsx from 'clsx';

const ShowEvent = dynamic(() => import('@frontend/utils/show.event'), {
Expand Down Expand Up @@ -123,6 +124,15 @@ export const Wrapper = ({ children }: { children: ReactNode }) => {
</li>
</Link>
)}
{session.status == 'authenticated' && (
<li
className={`cursor-pointer transition duration-300 ease-in active:text-[#FBFF14] hover:text-[#A489FF] hidden md:block md:border-none md:py-[30px]`}
>
<NotificationBell
userId={session?.data?.user?.id as string}
/>
</li>
)}
{session.status == 'authenticated' && (
<li
onClick={() => signOut()}
Expand Down
22 changes: 22 additions & 0 deletions apps/frontend/src/components/notification/novu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use client';
import { Inbox } from '@novu/react';
import { dark } from '@novu/react/themes';

export const NotificationBell = ({ userId }: { userId: string }) => {
console.log('userId', userId);
return (
<Inbox
applicationIdentifier={process.env.NEXT_PUBLIC_NOVU_APP_ID as string}
subscriberId={userId as string}
appearance={{
elements: {
popoverTrigger: 'novu-popover-trigger',
bellIcon: 'novu-bell-icon',
bellContainer: 'novu-bell-container',
bellDot: 'novu-bell-dot',
},
baseTheme: dark,
}}
/>
);
};
6 changes: 6 additions & 0 deletions apps/frontend/src/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,9 @@ h6 {
.little-black {
background: rgba(0, 0, 0, 0.9);
}
.novu-popover-trigger { background-color: white;}
.novu-popover-trigger:hover { background-color: #A489FF;}

.novu-bell-container > svg > path {
fill: black;
}
Loading