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

PWA and notification- Feat/web app #544

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
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
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,12 @@ LIGHTCAST_CLIENT_SECRET=

# To run the application in production environment / check the envs
# SKIP_ENV_CHECK=true npm run [replace with your script name]


# To test notification using web-push
# run 'node ./generate-vapidKey.js' in terminal and get Public/Private Key

NEXT_PUBLIC_VAPID_PUBLIC_KEY=
VAPID_PRIVATE_KEY=

ENABLE_SW=true #true if want to test locally
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,9 @@ bun.lockb
package-lock.json
yarn.lock

**/public/sw.js
**/public/workbox-*.js
**/public/worker-*.js
**/public/sw.js.map
**/public/workbox-*.js.map
**/public/worker-*.js.map
5 changes: 5 additions & 0 deletions generate-vapidKey.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import webpush from 'web-push';
const vapidKeys = webpush.generateVAPIDKeys();

console.log('NEXT_PUBLIC_VAPID_PUBLIC_KEY=', vapidKeys.publicKey);
console.log('VAPID_PRIVATE_KEY=', vapidKeys.privateKey);
14 changes: 12 additions & 2 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { fileURLToPath } from 'node:url';
import createJiti from 'jiti';
import withPWA from 'next-pwa';

if (process.env.SKIP_ENV_CHECK !== 'true') {
const jiti = createJiti(fileURLToPath(import.meta.url));
Expand Down Expand Up @@ -34,9 +35,18 @@ const nextConfig = {
{
protocol: 'https',
hostname: 'www.example.com',
}
},
],
},
};

export default nextConfig; // ES module export
const pwaConfig = withPWA({
dest: 'public',
register: true,
importScripts: ['/worker.js'],
// disable: process.env.NODE_ENV === 'development',
publicExcludes: ['!noprecache/**/*'],
buildExcludes: [/middleware-manifest.json$/],
});

export default pwaConfig(nextConfig); // ES module export
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"lucide-react": "^0.426.0",
"next": "^14.2.12",
"next-auth": "^4.24.7",
"next-pwa": "^5.6.0",
"next-themes": "^0.3.0",
"nextjs-toploader": "^3.7.15",
"node-cron": "^3.0.3",
Expand All @@ -87,17 +88,20 @@
"tailwindcss-animate": "^1.0.7",
"uuid": "^10.0.0",
"vaul": "^0.9.1",
"web-push": "^3.6.7",
"zod": "^3.23.8",
"zod-error": "^1.5.0"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6",
"@types/next-pwa": "^5.6.9",
"@types/node": "^20.16.10",
"@types/node-cron": "^3.0.11",
"@types/nodemailer": "^6.4.16",
"@types/randomstring": "^1.3.0",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/web-push": "^3.6.4",
"@typescript-eslint/eslint-plugin": "^8.1.0",
"@typescript-eslint/parser": "^8.1.0",
"100xdevs-job-board": "file:",
Expand Down
18 changes: 18 additions & 0 deletions prisma/migrations/20241022165852_merged/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
-- CreateEnum
CREATE TYPE "ProjectStack" AS ENUM ('GO', 'PYTHON', 'MERN', 'NEXTJS', 'AI_GPT_APIS', 'SPRINGBOOT', 'OTHERS');

-- DropForeignKey
ALTER TABLE "Experience" DROP CONSTRAINT "Experience_userId_fkey";

-- DropForeignKey
ALTER TABLE "Project" DROP CONSTRAINT "Project_userId_fkey";

-- AlterTable
ALTER TABLE "Project" ADD COLUMN "projectThumbnail" TEXT,
ADD COLUMN "stack" "ProjectStack" NOT NULL DEFAULT 'OTHERS';

-- AddForeignKey
ALTER TABLE "Experience" ADD CONSTRAINT "Experience_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Project" ADD CONSTRAINT "Project_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
11 changes: 11 additions & 0 deletions prisma/migrations/20241023151855_added_notifications/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- CreateTable
CREATE TABLE "Notifications" (
"id" TEXT NOT NULL,
"subscription" TEXT NOT NULL,
"userId" TEXT NOT NULL,

CONSTRAINT "Notifications_pkey" PRIMARY KEY ("id")
);

-- AddForeignKey
ALTER TABLE "Notifications" ADD CONSTRAINT "Notifications_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
2 changes: 2 additions & 0 deletions prisma/migrations/20241023170501_deleted_at/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Job" ADD COLUMN "deletedAt" TIMESTAMP(3);
12 changes: 10 additions & 2 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ model User {
oauthId String?

blockedByAdmin DateTime?
onBoard Boolean @default(false)
bookmark Bookmark[]
onBoard Boolean @default(false)
bookmark Bookmark[]
notifications Notifications[]
}

enum OauthProvider {
Expand Down Expand Up @@ -122,6 +123,13 @@ model Project {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model Notifications {
id String @id @default(uuid())
subscription String
userId String
user User @relation(fields: [userId],references: [id],onDelete: Cascade)
}

enum ProjectStack {
GO
PYTHON
Expand Down
58 changes: 58 additions & 0 deletions public/worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//Installing the sw.js

const installEvent = () => {
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('v1').then((cache) => {
return cache.addAll(['/', '/offline', '/manifest.json']);
})
);
});
};
installEvent();

//Activating the sw.js

const activateEvent = () => {
self.addEventListener('activate', () => {
return true;
});
};
activateEvent();

//handling the push manager

self.addEventListener('push', async (e) => {
const { message, body, icon, route } = JSON.parse(e.data.text());

e.waitUntil(
self.registration.showNotification(message, {
body,
icon: icon || '/main.png',
badge: '/main.png',
vibrate: [100, 50, 100],
data: { route },
})
);
});

self.addEventListener('notificationclick', (event) => {
event.notification.close();

const route = event.notification.data.route;

event.waitUntil(
clients
.matchAll({
type: 'window',
})
.then((clientList) => {
for (const client of clientList) {
if (client.url === '/' && 'focus' in client)
return client.focu(route ? route : '/jobs');
}
if (clients.openWindow)
return clients.openWindow(route ? route : '/jobs');
})
);
});
16 changes: 15 additions & 1 deletion src/actions/job.action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
} from '@/types/jobs.types';
import { withAdminServerAction } from '@/lib/admin';
import { revalidatePath } from 'next/cache';
import { sendNotificationAction } from '@/actions/notification';

type additional = {
isVerifiedJob: boolean;
Expand Down Expand Up @@ -467,15 +468,28 @@ export const toggleApproveJob = withAdminServerAction<
throw new Error('Job not found');
}

await prisma.job.update({
const updatedJob = await prisma.job.update({
where: {
id: id,
},
data: {
isVerifiedJob: !job.isVerifiedJob,
},
select: {
id: true,
isVerifiedJob: true,
},
});

if (updatedJob.isVerifiedJob) {
await sendNotificationAction(
'New Posting Alert',
'New Job is recently posted by 100xdevs , come fast and apply before other applys.',
`/jobs/${updatedJob.id}`,
'/main.png'
);
}

revalidatePath('/manage');
const message = job.isVerifiedJob ? 'Job Unapproved' : 'Job Approved';
return new SuccessResponse(message, 200, { jobId: id }).serialize();
Expand Down
142 changes: 142 additions & 0 deletions src/actions/notification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
'use server';

import prisma from '@/config/prisma.config';
import { authOptions } from '@/lib/authOptions';
import { getServerSession } from 'next-auth';
import webpush from 'web-push';

const vapidKeys = {
publicKey: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY,
privateKey: process.env.VAPID_PRIVATE_KEY,
};

//Handling the db add logic for subscription

export async function AddUserSubscription(subscription: string) {
try {
const user = await getServerSession(authOptions);
const userId = user?.user.id;

if (!userId) throw new Error('User not loggedIn');

const isUser = await prisma.user.findFirst({
where: {
id: userId,
},
select: {
id: true,
},
});

if (!isUser) throw new Error('No user found');

await prisma.notifications.create({
data: {
subscription: subscription,
userId: isUser.id,
},
});

return {
status: 200,
message: 'Subscription Added',
};
} catch (error) {
return {
status: 400,
message: (error as Error).message,
};
}
}

//Handling the db remove logic for subscription

export async function DeleteUserSubscription() {
try {
const user = await getServerSession(authOptions);
const userId = user?.user.id;

if (!userId) throw new Error('User not loggedIn');

const isUser = await prisma.notifications.findFirst({
where: {
userId: userId,
},
select: {
id: true,
},
});

if (!isUser) throw new Error('No user found');

await prisma.notifications.deleteMany({
where: {
id: isUser.id,
},
});

return {
status: 200,
message: 'Subscription Delete',
};
} catch (error) {
return {
status: 400,
message: (error as Error).message,
};
}
}

//Handling the push notification

export async function sendNotificationAction(
message: string,
body: string,
route: string,
icon: string
) {
try {
const user = await getServerSession(authOptions);
const userId = user?.user.id;

if (!userId) throw new Error('User not loggedIn');

const allUser = await prisma.notifications.findMany({
where: {
userId: {
not: userId,
},
},
});

if (!allUser || allUser.length <= 0) throw new Error('No other user found');

webpush.setVapidDetails(
'mailto:[email protected]',
vapidKeys.publicKey as string,
vapidKeys.privateKey as string
);

for (let data of allUser) {
await webpush.sendNotification(
JSON.parse(data.subscription),
JSON.stringify({
message,
icon,
body,
route,
})
);
}

return {
status: 200,
message: 'Notification sent successfully',
};
} catch (error) {
return {
status: 400,
message: (error as Error).message,
};
}
}
Loading
Loading