Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"postman.settings.dotenv-detection-notification-visibility": false
}
3 changes: 2 additions & 1 deletion client/src/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ export function cn(...inputs) {
}

export function getApiBaseUrl() {
return import.meta.env.VITE_API_URL;
// return import.meta.env.VITE_API_URL;
return "http://localhost:3000";
}

export function formatMoney(amountInMinorUnit, currency) {
Expand Down
99 changes: 94 additions & 5 deletions client/src/pages/dashboard/Notification.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,99 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";

import { Button } from "@/components/ui/button";

import { Bell, Trash2 } from "lucide-react";

import { fetchWithAuth } from "@/lib/utils";

export default function NotificationPage() {
const queryClient = useQueryClient();

const { data, isLoading, isError } = useQuery({
queryKey: ["notifications"],
queryFn: () =>
fetchWithAuth("/api/notification", {
credentials: "include",
}).then((res) => res.json()),
});

const deleteMutation = useMutation({
mutationFn: (id) =>
fetchWithAuth(`/api/notification/delete/${id}`, {
method: "DELETE",
credentials: "include",
}).then((res) => res.json()),

onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["notifications"],
});
},
});

if (isLoading) {
return <div className="p-6">Loading notifications...</div>;
}

if (isError) {
return (
<div className="p-6 text-red-500">Failed to load notifications.</div>
);
}

const notifications = data?.data ?? [];

const Notification = () => {
return (
<div>Notification</div>
)
}
<section className="space-y-6">
<header>
<h1 className="text-page-title font-bold">Notifications</h1>

<p className="text-subly-text-secondary">
Stay updated on subscription reminders and activity.
</p>
</header>

{!notifications.length ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12 gap-4">
<Bell className="size-10" />

export default Notification
<p className="text-subly-text-secondary">
No notifications available.
</p>
</CardContent>
</Card>
) : (
<div className="space-y-4">
{notifications.map((notification) => (
<Card key={notification.id}>
<CardHeader className="flex flex-row items-start justify-between">
<div>
<CardTitle>{notification.title}</CardTitle>

<p className="text-sm text-subly-text-secondary mt-1">
{new Date(notification.createdAt).toLocaleDateString()}
</p>
</div>

<Button
variant="ghost"
size="icon"
onClick={() => deleteMutation.mutate(notification.id)}
>
<Trash2 className="size-4" />
</Button>
</CardHeader>

<CardContent>
<p className="text-sm">{notification.message}</p>
</CardContent>
</Card>
))}
</div>
)}
</section>
);
}
50 changes: 50 additions & 0 deletions server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"express-rate-limit": "^8.5.2",
"jsonwebtoken": "^9.0.3",
"pg": "^8.21.0",
"resend": "^6.14.0",
"zod": "^4.4.3"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
Warnings:

- You are about to drop the column `reminderDaysBefore` on the `Subscription` table. All the data in the column will be lost.
- You are about to drop the column `reminderEnabled` on the `Subscription` table. All the data in the column will be lost.
- You are about to drop the column `reminderSentAt` on the `Subscription` table. All the data in the column will be lost.

*/
-- AlterTable
ALTER TABLE "Subscription" DROP COLUMN "reminderDaysBefore",
DROP COLUMN "reminderEnabled",
DROP COLUMN "reminderSentAt";

-- AlterTable
ALTER TABLE "User" ADD COLUMN "emailNofiticationEnabled" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "reminderDaysBefore" INTEGER NOT NULL DEFAULT 3;

-- CreateTable
CREATE TABLE "Notification" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"title" TEXT NOT NULL,
"message" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

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

-- AddForeignKey
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
24 changes: 20 additions & 4 deletions server/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ model User {
role Role @default(USER)

subscriptions Subscription[]
notifications Notification[]

reminderDaysBefore Int @default(3)
emailNofiticationEnabled Boolean @default(true)

createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
Expand All @@ -49,10 +53,9 @@ model Subscription {
status Status @default(ACTIVE)
nextBillingDate DateTime

reminderEnabled Boolean @default(true)
reminderDaysBefore Int @default(3)

reminderSent Boolean @default(false)
reminderSentAt DateTime?


expiredEmailSent Boolean @default(false)
expiredEmailSentAt DateTime?
Expand Down Expand Up @@ -99,6 +102,18 @@ model History {

}

model Notification{
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)

title String
message String


createdAt DateTime @default(now())
}


model Rates{
id String @id @default(cuid())
Expand Down Expand Up @@ -139,4 +154,5 @@ enum Type {
EDITED
RENEWED

}
}

2 changes: 2 additions & 0 deletions server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { refreshRouter } from "./src/routes/refresh.route.js";
import { currencyRouters } from "./src/routes/rate.route.js";
import { historyRouter } from "./src/routes/history.route.js";
import cors from "cors";
import { notificationRouter } from "./src/routes/notification.route.js";



Expand Down Expand Up @@ -71,6 +72,7 @@ app.use("/api/refresh", apiLimiter, refreshRouter);
app.use("/api/subscription", requireAuth, subscriptionRouter);
app.use("/api/history", requireAuth, historyRouter);
app.use("/api/admin", apiLimiter, requireAuth, requireRole, adminRouter);
app.use("/api/notification", requireAuth, notificationRouter);

app.use((err, req, res, next) => {
if (err) {
Expand Down
17 changes: 9 additions & 8 deletions server/src/config/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import dotenv from 'dotenv'
dotenv.config()

export const env = {
PORT: process.env.PORT || 3001,
ACCESS_TOKEN_SECRET: process.env.ACCESS_TOKEN_SECRET,
REFRESH_TOKEN_SECRET: process.env.REFRESH_TOKEN_SECRET,
NODE_ENV: process.env.NODE_ENV,
DATABASE_URL: process.env.DATABASE_URL,
CLIENT_URL: process.env.CLIENT_URL,
CORS_ORIGINS: process.env.CORS_ORIGINS,
}
PORT: process.env.PORT || 3001,
ACCESS_TOKEN_SECRET: process.env.ACCESS_TOKEN_SECRET,
REFRESH_TOKEN_SECRET: process.env.REFRESH_TOKEN_SECRET,
NODE_ENV: process.env.NODE_ENV,
DATABASE_URL: process.env.DATABASE_URL,
CLIENT_URL: process.env.CLIENT_URL,
CORS_ORIGINS: process.env.CORS_ORIGINS,
RESEND_API_KEY: process.env.RESEND_API_KEY,
};
49 changes: 49 additions & 0 deletions server/src/controllers/notification.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { prisma } from "../libs/prisma.js"
export async function getNotification(req, res, next) {
try {
const userId = req.user.id

const notifications = await prisma.notification.findMany({
where: {
userId
}
})

return res.json({data: notifications, count:notifications.length , message: 'notification fetched successfully '})

}
catch (err) {
next(err)
}
}

export async function deleteNotification(req, res, next) {
try {
const userId = req.user.id

const id = req.params.id

const existingNotification = await prisma.notification.findFirst({
where: {
userId,
id
}
})

if (!existingNotification) {
return res.status(404).json({message: "notification not found"})
}

await prisma.notification.delete({
where: {
id: existingNotification.id
}
})

return res.status(200).json({
message: "Notification deleted successfully",
});
} catch (err) {
next(err)
}
}
Loading