Compare commits
14 Commits
feat/-adds
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bbf7f9c1c | ||
|
|
2584539fb0 | ||
|
|
101c235855 | ||
|
|
b89b2b1ba5 | ||
|
|
c11a3f9a7d | ||
|
|
763b887d65 | ||
|
|
db8cee836a | ||
|
|
46bbd8106b | ||
|
|
aaa62ae3f5 | ||
|
|
9ae190754a | ||
|
|
9faa512c0b | ||
|
|
7955465be4 | ||
| 3ac50d4132 | |||
|
|
1206e51f6d |
2076
backend/package-lock.json
generated
Normal file
2076
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Career" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"post" TEXT NOT NULL,
|
||||||
|
"designation" TEXT,
|
||||||
|
"qualification" TEXT,
|
||||||
|
"experienceNeed" TEXT,
|
||||||
|
"email" TEXT,
|
||||||
|
"number" TEXT,
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'new',
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Career_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Candidate" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"fullName" TEXT NOT NULL,
|
||||||
|
"mobile" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"subject" TEXT NOT NULL,
|
||||||
|
"coverLetter" TEXT NOT NULL,
|
||||||
|
"careerId" INTEGER NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Candidate_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Candidate" ADD CONSTRAINT "Candidate_careerId_fkey" FOREIGN KEY ("careerId") REFERENCES "Career"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Appointment" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"mobileNumber" TEXT NOT NULL,
|
||||||
|
"email" TEXT,
|
||||||
|
"message" TEXT,
|
||||||
|
"date" TIMESTAMP(3) NOT NULL,
|
||||||
|
"doctorId" INTEGER NOT NULL,
|
||||||
|
"departmentId" INTEGER NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Appointment_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Appointment" ADD CONSTRAINT "Appointment_doctorId_fkey" FOREIGN KEY ("doctorId") REFERENCES "Doctor"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Appointment" ADD CONSTRAINT "Appointment_departmentId_fkey" FOREIGN KEY ("departmentId") REFERENCES "Department"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "Appointment" DROP CONSTRAINT "Appointment_departmentId_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "Appointment" DROP CONSTRAINT "Appointment_doctorId_fkey";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Appointment" ALTER COLUMN "doctorId" SET DATA TYPE TEXT,
|
||||||
|
ALTER COLUMN "departmentId" SET DATA TYPE TEXT;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Appointment" ADD CONSTRAINT "Appointment_doctorId_fkey" FOREIGN KEY ("doctorId") REFERENCES "Doctor"("doctorId") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Appointment" ADD CONSTRAINT "Appointment_departmentId_fkey" FOREIGN KEY ("departmentId") REFERENCES "Department"("departmentId") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Inquiry" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"fullName" TEXT NOT NULL,
|
||||||
|
"number" TEXT NOT NULL,
|
||||||
|
"emailId" TEXT,
|
||||||
|
"subject" TEXT,
|
||||||
|
"message" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Inquiry_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "AcademicsResearch" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"fullName" TEXT NOT NULL,
|
||||||
|
"number" TEXT NOT NULL,
|
||||||
|
"emailId" TEXT,
|
||||||
|
"subject" TEXT,
|
||||||
|
"courseName" TEXT,
|
||||||
|
"message" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "AcademicsResearch_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
}
|
}
|
||||||
@@ -9,92 +8,169 @@ datasource db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
username String @unique
|
username String @unique
|
||||||
password String
|
password String
|
||||||
role String? @default("admin")
|
role String? @default("admin")
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
model Doctor {
|
model Doctor {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
doctorId String @unique
|
doctorId String @unique
|
||||||
name String
|
name String
|
||||||
designation String?
|
designation String?
|
||||||
workingStatus String?
|
workingStatus String?
|
||||||
qualification String?
|
qualification String?
|
||||||
|
|
||||||
departments DoctorDepartment[]
|
departments DoctorDepartment[]
|
||||||
|
appointments Appointment[]
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
model Department {
|
model Department {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
departmentId String @unique
|
departmentId String @unique
|
||||||
name String
|
name String
|
||||||
|
|
||||||
para1 String?
|
para1 String?
|
||||||
para2 String?
|
para2 String?
|
||||||
para3 String?
|
para3 String?
|
||||||
facilities String?
|
facilities String?
|
||||||
services String?
|
services String?
|
||||||
|
|
||||||
doctors DoctorDepartment[]
|
doctors DoctorDepartment[]
|
||||||
|
appointments Appointment[]
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
model DoctorDepartment {
|
model DoctorDepartment {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
|
|
||||||
doctorId Int
|
doctorId Int
|
||||||
departmentId Int
|
departmentId Int
|
||||||
|
|
||||||
doctor Doctor @relation(fields: [doctorId], references: [id])
|
doctor Doctor @relation(fields: [doctorId], references: [id])
|
||||||
department Department @relation(fields: [departmentId], references: [id])
|
department Department @relation(fields: [departmentId], references: [id])
|
||||||
|
|
||||||
timing DoctorTiming?
|
timing DoctorTiming?
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
@@unique([doctorId, departmentId])
|
@@unique([doctorId, departmentId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model DoctorTiming {
|
model DoctorTiming {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
|
|
||||||
doctorDepartmentId Int @unique
|
doctorDepartmentId Int @unique
|
||||||
doctorDepartment DoctorDepartment @relation(fields: [doctorDepartmentId], references: [id])
|
doctorDepartment DoctorDepartment @relation(fields: [doctorDepartmentId], references: [id])
|
||||||
|
|
||||||
monday String?
|
monday String?
|
||||||
tuesday String?
|
tuesday String?
|
||||||
wednesday String?
|
wednesday String?
|
||||||
thursday String?
|
thursday String?
|
||||||
friday String?
|
friday String?
|
||||||
saturday String?
|
saturday String?
|
||||||
sunday String?
|
sunday String?
|
||||||
additional String?
|
additional String?
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
model Blog {
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
title String
|
|
||||||
writer String?
|
|
||||||
image String?
|
|
||||||
content Json
|
|
||||||
isActive Boolean @default(true)
|
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model Blog {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
title String
|
||||||
|
writer String?
|
||||||
|
image String?
|
||||||
|
content Json
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model Career {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
post String
|
||||||
|
designation String?
|
||||||
|
qualification String?
|
||||||
|
experienceNeed String?
|
||||||
|
email String?
|
||||||
|
number String?
|
||||||
|
status String @default("new")
|
||||||
|
|
||||||
|
candidates Candidate[]
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model Candidate {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
fullName String
|
||||||
|
mobile String
|
||||||
|
email String
|
||||||
|
subject String
|
||||||
|
coverLetter String
|
||||||
|
careerId Int
|
||||||
|
|
||||||
|
career Career @relation(fields: [careerId], references: [id])
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model Appointment {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
|
||||||
|
name String
|
||||||
|
mobileNumber String
|
||||||
|
email String?
|
||||||
|
message String?
|
||||||
|
date DateTime
|
||||||
|
|
||||||
|
doctorId String
|
||||||
|
departmentId String
|
||||||
|
|
||||||
|
doctor Doctor @relation(fields: [doctorId], references: [doctorId])
|
||||||
|
department Department @relation(fields: [departmentId], references: [departmentId])
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model Inquiry {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
|
||||||
|
fullName String
|
||||||
|
number String
|
||||||
|
emailId String?
|
||||||
|
subject String?
|
||||||
|
message String?
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model AcademicsResearch {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
|
||||||
|
fullName String
|
||||||
|
number String
|
||||||
|
emailId String?
|
||||||
|
subject String?
|
||||||
|
courseName String?
|
||||||
|
message String?
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,12 @@ import departmentRoutes from "./routes/department.routes.js";
|
|||||||
import authRoutes from "./routes/auth.routes.js";
|
import authRoutes from "./routes/auth.routes.js";
|
||||||
import blogRoutes from "./routes/blog.routes.js";
|
import blogRoutes from "./routes/blog.routes.js";
|
||||||
import uploadRoutes from "./routes/upload.routes.js";
|
import uploadRoutes from "./routes/upload.routes.js";
|
||||||
|
import doctorRoutes from "./routes/doctor.routes.js";
|
||||||
|
import careerRoutes from "./routes/career.routes.js";
|
||||||
|
import candidateRoutes from "./routes/candidate.routes.js";
|
||||||
|
import appointmentRoutes from "./routes/appointment.routes.js";
|
||||||
|
import inquiryRoutes from "./routes/inquiry.routes.js";
|
||||||
|
import academicsResearchRoutes from "./routes/academicsResearch.routes.js";
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
@@ -35,6 +41,12 @@ app.use("/api/auth", authRoutes);
|
|||||||
app.use("/api/blogs", blogRoutes);
|
app.use("/api/blogs", blogRoutes);
|
||||||
app.use("/uploads", express.static("uploads"));
|
app.use("/uploads", express.static("uploads"));
|
||||||
app.use("/api/upload", uploadRoutes);
|
app.use("/api/upload", uploadRoutes);
|
||||||
|
app.use("/api/doctors", doctorRoutes);
|
||||||
|
app.use("/api/careers", careerRoutes);
|
||||||
|
app.use("/api/candidates", candidateRoutes);
|
||||||
|
app.use("/api/appointments", appointmentRoutes);
|
||||||
|
app.use("/api/inquiry", inquiryRoutes);
|
||||||
|
app.use("/api/academics", academicsResearchRoutes);
|
||||||
|
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
|
|||||||
117
backend/src/controllers/academicsResearch.controller.js
Normal file
117
backend/src/controllers/academicsResearch.controller.js
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import prisma from "../prisma/client.js";
|
||||||
|
|
||||||
|
// CREATE ACADEMICS & RESEARCH
|
||||||
|
|
||||||
|
export const createAcademicsResearch = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {fullName, number, emailId, subject, courseName, message} = req.body;
|
||||||
|
|
||||||
|
if (!fullName || !number) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: "Full name and number are required",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await prisma.academicsResearch.create({
|
||||||
|
data: {
|
||||||
|
fullName,
|
||||||
|
number,
|
||||||
|
emailId,
|
||||||
|
subject,
|
||||||
|
courseName,
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
status: 200,
|
||||||
|
data,
|
||||||
|
message: "Academics & Research added successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to add Academics & Research inquiry",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// GET ALL
|
||||||
|
|
||||||
|
export const getAcademicsResearch = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const data = await prisma.academicsResearch.findMany({
|
||||||
|
orderBy: {
|
||||||
|
createdAt: "desc",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to fetch records",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// GET SINGLE
|
||||||
|
|
||||||
|
export const getSingleAcademicsResearch = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {id} = req.params;
|
||||||
|
|
||||||
|
const data = await prisma.academicsResearch.findUnique({
|
||||||
|
where: {
|
||||||
|
id: Number(id),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: "Record not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to fetch record",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// DELETE
|
||||||
|
|
||||||
|
export const deleteAcademicsResearch = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {id} = req.params;
|
||||||
|
|
||||||
|
await prisma.academicsResearch.delete({
|
||||||
|
where: {
|
||||||
|
id: Number(id),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: "Record deleted successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to delete record",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
224
backend/src/controllers/appointment.controller.js
Normal file
224
backend/src/controllers/appointment.controller.js
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import prisma from "../prisma/client.js";
|
||||||
|
//CREATE APPOINTMENT
|
||||||
|
|
||||||
|
export const createAppointment = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {name, mobileNumber, email, message, date, doctorId, departmentId} =
|
||||||
|
req.body;
|
||||||
|
|
||||||
|
if (!name || !mobileNumber || !doctorId || !departmentId || !date) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: "Required fields missing",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const appointment = await prisma.appointment.create({
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
mobileNumber,
|
||||||
|
email,
|
||||||
|
message,
|
||||||
|
date: new Date(date),
|
||||||
|
doctorId,
|
||||||
|
departmentId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
doctor: true,
|
||||||
|
department: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
message: "Appointment booked successfully",
|
||||||
|
data: appointment,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to create appointment",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// GET ALL APPOINTMENTS
|
||||||
|
|
||||||
|
export const getAppointments = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const appointments = await prisma.appointment.findMany({
|
||||||
|
include: {
|
||||||
|
doctor: true,
|
||||||
|
department: true,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: "desc",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: appointments,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to fetch appointments",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// GET SINGLE APPOINTMENT
|
||||||
|
|
||||||
|
export const getAppointment = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {id} = req.params;
|
||||||
|
|
||||||
|
const appointment = await prisma.appointment.findUnique({
|
||||||
|
where: {
|
||||||
|
id: Number(id),
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
doctor: true,
|
||||||
|
department: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!appointment) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: "Appointment not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: appointment,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to fetch appointment",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// GET APPOINTMENTS BY DOCTOR
|
||||||
|
|
||||||
|
export const getAppointmentsByDoctor = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {doctorId} = req.params;
|
||||||
|
|
||||||
|
const appointments = await prisma.appointment.findMany({
|
||||||
|
where: {
|
||||||
|
doctorId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
doctor: true,
|
||||||
|
department: true,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
date: "asc",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: appointments,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to fetch doctor appointments",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// GET APPOINTMENTS BY DEPARTMENT
|
||||||
|
|
||||||
|
export const getAppointmentsByDepartment = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {departmentId} = req.params;
|
||||||
|
|
||||||
|
const appointments = await prisma.appointment.findMany({
|
||||||
|
where: {
|
||||||
|
departmentId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
doctor: true,
|
||||||
|
department: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: appointments,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to fetch department appointments",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// UPDATE APPOINTMENT
|
||||||
|
|
||||||
|
export const updateAppointment = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {id} = req.params;
|
||||||
|
|
||||||
|
const appointment = await prisma.appointment.update({
|
||||||
|
where: {
|
||||||
|
id: Number(id),
|
||||||
|
},
|
||||||
|
data: req.body,
|
||||||
|
include: {
|
||||||
|
doctor: true,
|
||||||
|
department: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: "Appointment updated successfully",
|
||||||
|
data: appointment,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to update appointment",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//DELETE APPOINTMENT
|
||||||
|
|
||||||
|
export const deleteAppointment = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {id} = req.params;
|
||||||
|
|
||||||
|
await prisma.appointment.delete({
|
||||||
|
where: {
|
||||||
|
id: Number(id),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: "Appointment deleted successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to delete appointment",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
183
backend/src/controllers/candidate.controller.js
Normal file
183
backend/src/controllers/candidate.controller.js
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import prisma from "../prisma/client.js";
|
||||||
|
|
||||||
|
// CREATE CANDIDATE
|
||||||
|
|
||||||
|
export const createCandidate = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {fullName, mobile, email, subject, coverLetter, careerId} = req.body;
|
||||||
|
|
||||||
|
if (!fullName || !mobile || !email || !careerId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: "Required fields missing",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidate = await prisma.candidate.create({
|
||||||
|
data: {
|
||||||
|
fullName,
|
||||||
|
mobile,
|
||||||
|
email,
|
||||||
|
subject,
|
||||||
|
coverLetter,
|
||||||
|
careerId: Number(careerId),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
message: "Application submitted successfully",
|
||||||
|
data: candidate,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to create candidate",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// GET ALL CANDIDATES
|
||||||
|
|
||||||
|
export const getCandidates = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const candidates = await prisma.candidate.findMany({
|
||||||
|
include: {
|
||||||
|
career: true,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: "desc",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: candidates,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to fetch candidates",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// GET SINGLE CANDIDATE
|
||||||
|
|
||||||
|
export const getCandidate = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {id} = req.params;
|
||||||
|
|
||||||
|
const candidate = await prisma.candidate.findUnique({
|
||||||
|
where: {
|
||||||
|
id: Number(id),
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
career: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!candidate) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: "Candidate not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: candidate,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to fetch candidate",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// GET CANDIDATES BY CAREER
|
||||||
|
|
||||||
|
export const getCandidatesByCareer = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {careerId} = req.params;
|
||||||
|
|
||||||
|
const candidates = await prisma.candidate.findMany({
|
||||||
|
where: {
|
||||||
|
careerId: Number(careerId),
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
career: true,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: "desc",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: candidates,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to fetch candidates",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// UPDATE CANDIDATE
|
||||||
|
|
||||||
|
export const updateCandidate = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {id} = req.params;
|
||||||
|
|
||||||
|
const candidate = await prisma.candidate.update({
|
||||||
|
where: {
|
||||||
|
id: Number(id),
|
||||||
|
},
|
||||||
|
data: req.body,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: "Candidate updated successfully",
|
||||||
|
data: candidate,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to update candidate",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// DELETE CANDIDATE
|
||||||
|
|
||||||
|
export const deleteCandidate = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {id} = req.params;
|
||||||
|
|
||||||
|
await prisma.candidate.delete({
|
||||||
|
where: {
|
||||||
|
id: Number(id),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: "Candidate deleted successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to delete candidate",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
128
backend/src/controllers/career.controller.js
Normal file
128
backend/src/controllers/career.controller.js
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import prisma from "../prisma/client.js";
|
||||||
|
|
||||||
|
// GET ALL CAREERS
|
||||||
|
|
||||||
|
export const getAllCareers = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const careers = await prisma.career.findMany({
|
||||||
|
orderBy: {createdAt: "desc"},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = careers.map((c) => ({
|
||||||
|
id: c.id,
|
||||||
|
post: c.post,
|
||||||
|
designation: c.designation,
|
||||||
|
qualification: c.qualification,
|
||||||
|
experienceNeed: c.experienceNeed,
|
||||||
|
email: c.email,
|
||||||
|
number: c.number,
|
||||||
|
status: c.status,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: response,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to fetch careers",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// CREATE CAREER
|
||||||
|
|
||||||
|
export const createCareer = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
post,
|
||||||
|
designation,
|
||||||
|
qualification,
|
||||||
|
experienceNeed,
|
||||||
|
email,
|
||||||
|
number,
|
||||||
|
status,
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
if (!post || !designation) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: "Post and designation are required",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const career = await prisma.career.create({
|
||||||
|
data: {
|
||||||
|
post,
|
||||||
|
designation,
|
||||||
|
qualification,
|
||||||
|
experienceNeed,
|
||||||
|
email,
|
||||||
|
number,
|
||||||
|
status,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
message: "Career created successfully",
|
||||||
|
data: career,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to create career",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// UPDATE CAREER (PATCH)
|
||||||
|
|
||||||
|
export const updateCareer = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {id} = req.params;
|
||||||
|
|
||||||
|
const career = await prisma.career.update({
|
||||||
|
where: {id: Number(id)},
|
||||||
|
data: req.body,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: "Career updated successfully",
|
||||||
|
data: career,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to update career",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// DELETE CAREER
|
||||||
|
|
||||||
|
export const deleteCareer = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {id} = req.params;
|
||||||
|
|
||||||
|
await prisma.career.delete({
|
||||||
|
where: {id: Number(id)},
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: "Career deleted successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to delete career",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -8,7 +8,7 @@ export const getAllDepartments = async (req, res) => {
|
|||||||
|
|
||||||
const response = departments.map((dep) => ({
|
const response = departments.map((dep) => ({
|
||||||
departmentId: dep.departmentId,
|
departmentId: dep.departmentId,
|
||||||
Department: dep.name,
|
name: dep.name,
|
||||||
para1: dep.para1 ?? "",
|
para1: dep.para1 ?? "",
|
||||||
para2: dep.para2 ?? "",
|
para2: dep.para2 ?? "",
|
||||||
para3: dep.para3 ?? "",
|
para3: dep.para3 ?? "",
|
||||||
@@ -64,3 +64,56 @@ export async function createDepartment(req, res) {
|
|||||||
res.status(500).json({error: "Failed to create department"});
|
res.status(500).json({error: "Failed to create department"});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const updateDepartment = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {departmentId} = req.params;
|
||||||
|
|
||||||
|
const {name, para1, para2, para3, facilities, services} = req.body;
|
||||||
|
|
||||||
|
const department = await prisma.department.update({
|
||||||
|
where: {departmentId},
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
para1,
|
||||||
|
para2,
|
||||||
|
para3,
|
||||||
|
facilities,
|
||||||
|
services,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: "Department updated successfully",
|
||||||
|
data: department,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to update department",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteDepartment = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {departmentId} = req.params;
|
||||||
|
|
||||||
|
await prisma.department.delete({
|
||||||
|
where: {departmentId},
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: "Department deleted successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to delete department",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
416
backend/src/controllers/doctor.controller.js
Normal file
416
backend/src/controllers/doctor.controller.js
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
import prisma from "../prisma/client.js";
|
||||||
|
|
||||||
|
// get doctors
|
||||||
|
|
||||||
|
export const getAllDoctors = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const doctors = await prisma.doctor.findMany({
|
||||||
|
include: {
|
||||||
|
departments: {
|
||||||
|
include: {
|
||||||
|
department: true,
|
||||||
|
timing: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: {name: "asc"},
|
||||||
|
});
|
||||||
|
|
||||||
|
const formatted = doctors.map((doc, index) => {
|
||||||
|
return {
|
||||||
|
SL_NO: String(index + 1),
|
||||||
|
doctorId: doc.doctorId,
|
||||||
|
name: doc.name,
|
||||||
|
designation: doc.designation,
|
||||||
|
workingStatus: doc.workingStatus,
|
||||||
|
qualification: doc.qualification,
|
||||||
|
|
||||||
|
departments: doc.departments.map((d) => {
|
||||||
|
const t = d.timing || {};
|
||||||
|
|
||||||
|
const timingArray = [
|
||||||
|
t.monday && `Monday ${t.monday}`,
|
||||||
|
t.tuesday && `Tuesday ${t.tuesday}`,
|
||||||
|
t.wednesday && `Wednesday ${t.wednesday}`,
|
||||||
|
t.thursday && `Thursday ${t.thursday}`,
|
||||||
|
t.friday && `Friday ${t.friday}`,
|
||||||
|
t.saturday && `Saturday ${t.saturday}`,
|
||||||
|
t.sunday && `Sunday ${t.sunday}`,
|
||||||
|
t.additional && t.additional,
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
return {
|
||||||
|
departmentId: d.department.departmentId,
|
||||||
|
departmentName: d.department.name,
|
||||||
|
|
||||||
|
timing: timingArray.join(" & "),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: formatted,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to fetch doctors",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// get doctor by id
|
||||||
|
|
||||||
|
export const getDoctorByDoctorId = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {doctorId} = req.params;
|
||||||
|
|
||||||
|
const doctor = await prisma.doctor.findUnique({
|
||||||
|
where: {doctorId},
|
||||||
|
include: {
|
||||||
|
departments: {
|
||||||
|
include: {
|
||||||
|
department: true,
|
||||||
|
timing: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!doctor) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: "Doctor not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
doctorId: doctor.doctorId,
|
||||||
|
name: doctor.name,
|
||||||
|
designation: doctor.designation,
|
||||||
|
workingStatus: doctor.workingStatus,
|
||||||
|
qualification: doctor.qualification,
|
||||||
|
departments: doctor.departments.map((d) => ({
|
||||||
|
departmentId: d.department.departmentId,
|
||||||
|
departmentName: d.department.name,
|
||||||
|
timing: d.timing || {},
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: response,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to fetch doctor",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// add doctors
|
||||||
|
export const createDoctor = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
doctorId,
|
||||||
|
name,
|
||||||
|
designation,
|
||||||
|
workingStatus,
|
||||||
|
qualification,
|
||||||
|
departments,
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const doctor = await prisma.doctor.create({
|
||||||
|
data: {
|
||||||
|
doctorId,
|
||||||
|
name,
|
||||||
|
designation,
|
||||||
|
workingStatus,
|
||||||
|
qualification,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const dep of departments) {
|
||||||
|
const department = await prisma.department.findUnique({
|
||||||
|
where: {departmentId: dep.departmentId},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!department) continue;
|
||||||
|
|
||||||
|
const doctorDepartment = await prisma.doctorDepartment.create({
|
||||||
|
data: {
|
||||||
|
doctorId: doctor.id,
|
||||||
|
departmentId: department.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (dep.timing) {
|
||||||
|
await prisma.doctorTiming.create({
|
||||||
|
data: {
|
||||||
|
doctorDepartmentId: doctorDepartment.id,
|
||||||
|
...dep.timing,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
message: "Doctor created successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to create doctor",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//update doctors
|
||||||
|
export const updateDoctor = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {doctorId} = req.params;
|
||||||
|
const {name, designation, workingStatus, qualification, departments} =
|
||||||
|
req.body;
|
||||||
|
|
||||||
|
const doctor = await prisma.doctor.findUnique({
|
||||||
|
where: {doctorId},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!doctor) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: "Doctor not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.doctor.update({
|
||||||
|
where: {id: doctor.id},
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
designation,
|
||||||
|
workingStatus,
|
||||||
|
qualification,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const oldRelations = await prisma.doctorDepartment.findMany({
|
||||||
|
where: {doctorId: doctor.id},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const rel of oldRelations) {
|
||||||
|
await prisma.doctorTiming.deleteMany({
|
||||||
|
where: {doctorDepartmentId: rel.id},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.doctorDepartment.deleteMany({
|
||||||
|
where: {doctorId: doctor.id},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const dep of departments) {
|
||||||
|
const department = await prisma.department.findUnique({
|
||||||
|
where: {departmentId: dep.departmentId},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!department) continue;
|
||||||
|
|
||||||
|
const doctorDepartment = await prisma.doctorDepartment.create({
|
||||||
|
data: {
|
||||||
|
doctorId: doctor.id,
|
||||||
|
departmentId: department.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (dep.timing) {
|
||||||
|
await prisma.doctorTiming.create({
|
||||||
|
data: {
|
||||||
|
doctorDepartmentId: doctorDepartment.id,
|
||||||
|
...dep.timing,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: "Doctor updated successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to update doctor",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
//delete doctor
|
||||||
|
|
||||||
|
export const deleteDoctor = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {doctorId} = req.params;
|
||||||
|
|
||||||
|
if (!doctorId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: "Doctor ID is required",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const doctor = await prisma.doctor.findUnique({
|
||||||
|
where: {doctorId},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!doctor) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: `Doctor with ID ${doctorId} not found`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const doctorDepartments = await prisma.doctorDepartment.findMany({
|
||||||
|
where: {doctorId: doctor.id},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const dd of doctorDepartments) {
|
||||||
|
await prisma.doctorTiming.deleteMany({
|
||||||
|
where: {doctorDepartmentId: dd.id},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.doctorDepartment.deleteMany({
|
||||||
|
where: {doctorId: doctor.id},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.doctor.delete({
|
||||||
|
where: {id: doctor.id},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: `Doctor ${doctorId} deleted successfully`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to delete doctor",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// get doctor timings
|
||||||
|
|
||||||
|
export const getDoctorTimings = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const doctors = await prisma.doctor.findMany({
|
||||||
|
include: {
|
||||||
|
departments: {
|
||||||
|
include: {
|
||||||
|
timing: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = doctors.map((doc) => {
|
||||||
|
let timing = {};
|
||||||
|
|
||||||
|
if (doc.departments.length > 0) {
|
||||||
|
timing = doc.departments[0].timing ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
Doctor_ID: doc.doctorId,
|
||||||
|
Doctor: doc.name,
|
||||||
|
Monday: timing?.monday ?? "",
|
||||||
|
Tuesday: timing?.tuesday ?? "",
|
||||||
|
Wednesday: timing?.wednesday ?? "",
|
||||||
|
Thursday: timing?.thursday ?? "",
|
||||||
|
Friday: timing?.friday ?? "",
|
||||||
|
Saturday: timing?.saturday ?? "",
|
||||||
|
Sunday: timing?.sunday ?? "",
|
||||||
|
Additional: timing?.additional ?? "",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: result,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to fetch doctor timings",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// get doctor timings by id
|
||||||
|
|
||||||
|
export const getDoctorTimingById = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {doctorId} = req.params;
|
||||||
|
|
||||||
|
const doctor = await prisma.doctor.findUnique({
|
||||||
|
where: {doctorId},
|
||||||
|
include: {
|
||||||
|
departments: {
|
||||||
|
include: {
|
||||||
|
department: true,
|
||||||
|
timing: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!doctor) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: "Doctor not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
doctorId: doctor.doctorId,
|
||||||
|
doctorName: doctor.name,
|
||||||
|
|
||||||
|
departments: doctor.departments.map((d) => {
|
||||||
|
const t = d.timing || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
departmentId: d.department.departmentId,
|
||||||
|
departmentName: d.department.name,
|
||||||
|
|
||||||
|
timing: {
|
||||||
|
monday: t.monday || "",
|
||||||
|
tuesday: t.tuesday || "",
|
||||||
|
wednesday: t.wednesday || "",
|
||||||
|
thursday: t.thursday || "",
|
||||||
|
friday: t.friday || "",
|
||||||
|
saturday: t.saturday || "",
|
||||||
|
sunday: t.sunday || "",
|
||||||
|
additional: t.additional || "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: result,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to fetch doctor timing",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
108
backend/src/controllers/inquiry.controller.js
Normal file
108
backend/src/controllers/inquiry.controller.js
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import prisma from "../prisma/client.js";
|
||||||
|
|
||||||
|
/* CREATE INQUIRY */
|
||||||
|
export const createInquiry = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {fullName, number, emailId, subject, message} = req.body;
|
||||||
|
|
||||||
|
if (!fullName || !number) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: "Full name and number are required",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const inquiry = await prisma.inquiry.create({
|
||||||
|
data: {
|
||||||
|
fullName,
|
||||||
|
number,
|
||||||
|
emailId,
|
||||||
|
subject,
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
status: 200,
|
||||||
|
data: inquiry,
|
||||||
|
message: "Inquiry added successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to add inquiry",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* GET ALL */
|
||||||
|
export const getInquiries = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const inquiries = await prisma.inquiry.findMany({
|
||||||
|
orderBy: {
|
||||||
|
createdAt: "desc",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: inquiries,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to fetch inquiries",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* GET SINGLE */
|
||||||
|
export const getInquiry = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {id} = req.params;
|
||||||
|
|
||||||
|
const inquiry = await prisma.inquiry.findUnique({
|
||||||
|
where: {id: Number(id)},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!inquiry) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: "Inquiry not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: inquiry,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to fetch inquiry",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* DELETE */
|
||||||
|
export const deleteInquiry = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {id} = req.params;
|
||||||
|
|
||||||
|
await prisma.inquiry.delete({
|
||||||
|
where: {id: Number(id)},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: "Inquiry deleted successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to delete inquiry",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
18
backend/src/routes/academicsResearch.routes.js
Normal file
18
backend/src/routes/academicsResearch.routes.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import express from "express";
|
||||||
|
import {
|
||||||
|
createAcademicsResearch,
|
||||||
|
getAcademicsResearch,
|
||||||
|
getSingleAcademicsResearch,
|
||||||
|
deleteAcademicsResearch,
|
||||||
|
} from "../controllers/academicsResearch.controller.js";
|
||||||
|
|
||||||
|
import jwtAuthMiddleware from "../middleware/auth.js";
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post("/", createAcademicsResearch);
|
||||||
|
router.get("/getAll", getAcademicsResearch);
|
||||||
|
router.get("/:id", getSingleAcademicsResearch);
|
||||||
|
router.delete("/:id", jwtAuthMiddleware, deleteAcademicsResearch);
|
||||||
|
|
||||||
|
export default router;
|
||||||
23
backend/src/routes/appointment.routes.js
Normal file
23
backend/src/routes/appointment.routes.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import express from "express";
|
||||||
|
import {
|
||||||
|
createAppointment,
|
||||||
|
getAppointments,
|
||||||
|
getAppointment,
|
||||||
|
updateAppointment,
|
||||||
|
deleteAppointment,
|
||||||
|
} from "../controllers/appointment.controller.js";
|
||||||
|
|
||||||
|
import jwtAuthMiddleware from "../middleware/auth.js";
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
/* PUBLIC */
|
||||||
|
|
||||||
|
router.get("/getall", getAppointments);
|
||||||
|
router.post("/", createAppointment);
|
||||||
|
|
||||||
|
router.get("/:id", getAppointment);
|
||||||
|
router.patch("/:id", updateAppointment);
|
||||||
|
router.delete("/:id", jwtAuthMiddleware, deleteAppointment);
|
||||||
|
|
||||||
|
export default router;
|
||||||
25
backend/src/routes/candidate.routes.js
Normal file
25
backend/src/routes/candidate.routes.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import express from "express";
|
||||||
|
import {
|
||||||
|
createCandidate,
|
||||||
|
getCandidates,
|
||||||
|
getCandidate,
|
||||||
|
getCandidatesByCareer,
|
||||||
|
updateCandidate,
|
||||||
|
deleteCandidate,
|
||||||
|
} from "../controllers/candidate.controller.js";
|
||||||
|
|
||||||
|
import jwtAuthMiddleware from "../middleware/auth.js";
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
/* PUBLIC */
|
||||||
|
|
||||||
|
router.get("/getAll", getCandidates);
|
||||||
|
router.get("/:id", getCandidate);
|
||||||
|
router.get("/career/:careerId", getCandidatesByCareer);
|
||||||
|
|
||||||
|
router.post("/", createCandidate);
|
||||||
|
router.patch("/:id", updateCandidate);
|
||||||
|
router.delete("/:id", jwtAuthMiddleware, deleteCandidate);
|
||||||
|
|
||||||
|
export default router;
|
||||||
17
backend/src/routes/career.routes.js
Normal file
17
backend/src/routes/career.routes.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import express from "express";
|
||||||
|
import {
|
||||||
|
getAllCareers,
|
||||||
|
createCareer,
|
||||||
|
updateCareer,
|
||||||
|
deleteCareer,
|
||||||
|
} from "../controllers/career.controller.js";
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get("/getAll", getAllCareers);
|
||||||
|
|
||||||
|
router.post("/", createCareer);
|
||||||
|
router.patch("/:id", updateCareer);
|
||||||
|
router.delete("/:id", deleteCareer);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -2,6 +2,8 @@ import express from "express";
|
|||||||
import {
|
import {
|
||||||
getAllDepartments,
|
getAllDepartments,
|
||||||
createDepartment,
|
createDepartment,
|
||||||
|
updateDepartment,
|
||||||
|
deleteDepartment,
|
||||||
} from "../controllers/department.controller.js";
|
} from "../controllers/department.controller.js";
|
||||||
import jwtAuthMiddleware from "../middleware/auth.js";
|
import jwtAuthMiddleware from "../middleware/auth.js";
|
||||||
|
|
||||||
@@ -12,5 +14,7 @@ router.get("/getAll", getAllDepartments);
|
|||||||
|
|
||||||
// Protected
|
// Protected
|
||||||
router.post("/", jwtAuthMiddleware, createDepartment);
|
router.post("/", jwtAuthMiddleware, createDepartment);
|
||||||
|
router.put("/:departmentId", jwtAuthMiddleware, updateDepartment);
|
||||||
|
router.delete("/:departmentId", jwtAuthMiddleware, deleteDepartment);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
25
backend/src/routes/doctor.routes.js
Normal file
25
backend/src/routes/doctor.routes.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import express from "express";
|
||||||
|
import {
|
||||||
|
getAllDoctors,
|
||||||
|
createDoctor,
|
||||||
|
updateDoctor,
|
||||||
|
deleteDoctor,
|
||||||
|
getDoctorTimings,
|
||||||
|
getDoctorTimingById,
|
||||||
|
getDoctorByDoctorId,
|
||||||
|
} from "../controllers/doctor.controller.js";
|
||||||
|
|
||||||
|
import jwtAuthMiddleware from "../middleware/auth.js";
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get("/getAll", getAllDoctors);
|
||||||
|
router.get("/:doctorId", getDoctorByDoctorId);
|
||||||
|
router.get("/getTimings", getDoctorTimings);
|
||||||
|
router.get("/getTimings/:doctorId", getDoctorTimingById);
|
||||||
|
|
||||||
|
router.post("/", jwtAuthMiddleware, createDoctor);
|
||||||
|
router.patch("/:doctorId", jwtAuthMiddleware, updateDoctor);
|
||||||
|
router.delete("/:doctorId", jwtAuthMiddleware, deleteDoctor);
|
||||||
|
|
||||||
|
export default router;
|
||||||
19
backend/src/routes/inquiry.routes.js
Normal file
19
backend/src/routes/inquiry.routes.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import express from "express";
|
||||||
|
import {
|
||||||
|
createInquiry,
|
||||||
|
getInquiries,
|
||||||
|
getInquiry,
|
||||||
|
deleteInquiry,
|
||||||
|
} from "../controllers/inquiry.controller.js";
|
||||||
|
|
||||||
|
import jwtAuthMiddleware from "../middleware/auth.js";
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post("/", createInquiry);
|
||||||
|
|
||||||
|
router.get("/getAll", getInquiries);
|
||||||
|
router.get("/:id", getInquiry);
|
||||||
|
router.delete("/:id", jwtAuthMiddleware, deleteInquiry);
|
||||||
|
|
||||||
|
export default router;
|
||||||
BIN
backend/uploads/blog/1773814232254.png
Normal file
BIN
backend/uploads/blog/1773814232254.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
BIN
backend/uploads/blog/1773814239753.png
Normal file
BIN
backend/uploads/blog/1773814239753.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
BIN
backend/uploads/blog/1773814266558.png
Normal file
BIN
backend/uploads/blog/1773814266558.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
BIN
backend/uploads/blog/1773814356620.png
Normal file
BIN
backend/uploads/blog/1773814356620.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
BIN
backend/uploads/blog/1773814805822.png
Normal file
BIN
backend/uploads/blog/1773814805822.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
18980
frontend/package-lock.json
generated
18980
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,10 +10,21 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@editorjs/code": "^2.9.4",
|
||||||
|
"@editorjs/delimiter": "^1.4.2",
|
||||||
|
"@editorjs/editorjs": "^2.31.5",
|
||||||
|
"@editorjs/embed": "^2.8.0",
|
||||||
|
"@editorjs/header": "^2.8.8",
|
||||||
|
"@editorjs/image": "^2.10.3",
|
||||||
|
"@editorjs/list": "^2.0.9",
|
||||||
|
"@editorjs/quote": "^2.7.6",
|
||||||
|
"@editorjs/table": "^2.4.5",
|
||||||
"@fontsource-variable/geist": "^5.2.8",
|
"@fontsource-variable/geist": "^5.2.8",
|
||||||
|
"@tailwindcss/postcss": "^4.2.1",
|
||||||
"axios": "^1.13.6",
|
"axios": "^1.13.6",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
@@ -21,21 +32,23 @@
|
|||||||
"react-router-dom": "^7.13.1",
|
"react-router-dom": "^7.13.1",
|
||||||
"shadcn": "^4.0.5",
|
"shadcn": "^4.0.5",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
|
"tailwindcss": "^4.2.1",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tw-animate-css": "^1.4.0"
|
"tw-animate-css": "^1.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
|
"@types/estree": "^1.0.8",
|
||||||
|
"@types/json-schema": "^7.0.15",
|
||||||
"@types/node": "^24.12.0",
|
"@types/node": "^24.12.0",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.1.4",
|
"@vitejs/plugin-react": "^5.1.4",
|
||||||
"autoprefixer": "^10.4.27",
|
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.24",
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
"globals": "^16.5.0",
|
"globals": "^16.5.0",
|
||||||
"postcss": "^8.5.8",
|
"postcss": "^8.5.8",
|
||||||
"tailwindcss": "^3.4.19",
|
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.48.0",
|
"typescript-eslint": "^8.48.0",
|
||||||
"vite": "^7.3.1"
|
"vite": "^7.3.1"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export default {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
"@tailwindcss/postcss": {},
|
||||||
autoprefixer: {},
|
},
|
||||||
},
|
};
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,42 +1,42 @@
|
|||||||
#root {
|
#root {
|
||||||
max-width: 1280px;
|
max-width: 1280px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
height: 6em;
|
height: 6em;
|
||||||
padding: 1.5em;
|
padding: 1.5em;
|
||||||
will-change: filter;
|
will-change: filter;
|
||||||
transition: filter 300ms;
|
transition: filter 300ms;
|
||||||
}
|
}
|
||||||
.logo:hover {
|
.logo:hover {
|
||||||
filter: drop-shadow(0 0 2em #646cffaa);
|
filter: drop-shadow(0 0 2em #646cffaa);
|
||||||
}
|
}
|
||||||
.logo.react:hover {
|
.logo.react:hover {
|
||||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes logo-spin {
|
@keyframes logo-spin {
|
||||||
from {
|
from {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
a:nth-of-type(2) .logo {
|
a:nth-of-type(2) .logo {
|
||||||
animation: logo-spin infinite 20s linear;
|
animation: logo-spin infinite 20s linear;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
padding: 2em;
|
padding: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.read-the-docs {
|
.read-the-docs {
|
||||||
color: #888;
|
color: #888;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,41 @@
|
|||||||
import {BrowserRouter, Routes, Route} from "react-router-dom";
|
import {BrowserRouter, Routes, Route, Navigate} from "react-router-dom";
|
||||||
|
|
||||||
import Login from "@/pages/Login";
|
import Login from "@/pages/Login";
|
||||||
import Dashboard from "@/pages/Dashboard";
|
|
||||||
import Blog from "@/pages/Blog";
|
|
||||||
import Department from "@/pages/Department";
|
|
||||||
|
|
||||||
import ProtectedRoute from "./components/ProtectedRoutes/ProtectedRoutes";
|
import DashboardLayout from "./layouts/DashboardLayout";
|
||||||
|
|
||||||
|
// import ProtectedRoute from "./components/ProtectedRoutes/ProtectedRoutes";
|
||||||
|
|
||||||
|
import ProtectedRoute from "./auth/ProtectedRoute";
|
||||||
|
import PublicRoute from "./auth/PublicRoute";
|
||||||
|
import {AuthProvider} from "./context/AuthContext";
|
||||||
|
import Department from "./pages/Department";
|
||||||
|
import Doctor from "./pages/Doctor";
|
||||||
|
import Blog from "./pages/Blog";
|
||||||
|
import BlogEditorPage from "./pages/BlogEditor";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<AuthProvider>
|
||||||
<Route path="/" element={<Login />} />
|
<Routes>
|
||||||
|
<Route element={<PublicRoute />}>
|
||||||
|
<Route path="/" element={<Login />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
<Route
|
<Route element={<ProtectedRoute />}>
|
||||||
path="/dashboard"
|
<Route element={<DashboardLayout />}>
|
||||||
element={
|
<Route path="/department" element={<Department />} />
|
||||||
<ProtectedRoute>
|
<Route path="/doctor" element={<Doctor />} />
|
||||||
<Dashboard />
|
<Route path="/blog" element={<Blog />} />
|
||||||
</ProtectedRoute>
|
<Route path="/blog/create" element={<BlogEditorPage />} />
|
||||||
}
|
<Route path="/blog/edit/:id" element={<BlogEditorPage />} />
|
||||||
/>
|
</Route>
|
||||||
|
</Route>
|
||||||
|
|
||||||
<Route
|
<Route path="*" element={<Navigate to="/department" replace />} />
|
||||||
path="/blog"
|
</Routes>
|
||||||
element={
|
</AuthProvider>
|
||||||
<ProtectedRoute>
|
|
||||||
<Blog />
|
|
||||||
</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/department"
|
|
||||||
element={
|
|
||||||
<ProtectedRoute>
|
|
||||||
<Department />
|
|
||||||
</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Routes>
|
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
12
frontend/src/api/auth.ts
Normal file
12
frontend/src/api/auth.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import apiClient from "./client";
|
||||||
|
|
||||||
|
export const loginApi = async (
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
): Promise<any> => {
|
||||||
|
const response = await apiClient.post("/auth/login/", {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
48
frontend/src/api/blog.ts
Normal file
48
frontend/src/api/blog.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import apiClient from "@/api/client";
|
||||||
|
|
||||||
|
export interface Blog {
|
||||||
|
id?: number;
|
||||||
|
title: string;
|
||||||
|
writer: string;
|
||||||
|
image?: string;
|
||||||
|
content: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAllBlogsApi = async () => {
|
||||||
|
const res = await apiClient.get("/blogs");
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBlogByIdApi = async (id: number) => {
|
||||||
|
const res = await apiClient.get(`/blogs/${id}`);
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createBlogApi = async (data: Blog) => {
|
||||||
|
const res = await apiClient.post("/blogs", data);
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateBlogApi = async (id: number, data: Blog) => {
|
||||||
|
const res = await apiClient.put(`/blogs/${id}`, data);
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteBlogApi = async (id: number) => {
|
||||||
|
const res = await apiClient.delete(`/blogs/${id}`);
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* IMAGE UPLOAD */
|
||||||
|
export const uploadImageApi = async (file: File) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("image", file);
|
||||||
|
|
||||||
|
const res = await apiClient.post("/upload/image", formData, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
48
frontend/src/api/client.ts
Normal file
48
frontend/src/api/client.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import type {InternalAxiosRequestConfig} from "axios";
|
||||||
|
|
||||||
|
const BASE_URL: string = "http://localhost:3000/api";
|
||||||
|
|
||||||
|
const apiClient = axios.create({
|
||||||
|
baseURL: BASE_URL,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setAxiosAuthToken = (token: string | null): void => {
|
||||||
|
if (token) {
|
||||||
|
apiClient.defaults.headers.common["Authorization"] = `Bearer ${token}`;
|
||||||
|
} else {
|
||||||
|
delete apiClient.defaults.headers.common["Authorization"];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
apiClient.interceptors.request.use(
|
||||||
|
(config: InternalAxiosRequestConfig) => {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
|
if (token && config.headers) {
|
||||||
|
config.headers["Authorization"] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error: any) => Promise.reject(error),
|
||||||
|
);
|
||||||
|
|
||||||
|
apiClient.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
async (error) => {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
console.error("Unauthorized - token missing or invalid");
|
||||||
|
|
||||||
|
localStorage.removeItem("token");
|
||||||
|
window.location.href = "/login";
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default apiClient;
|
||||||
49
frontend/src/api/department.ts
Normal file
49
frontend/src/api/department.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import apiClient from "@/api/client";
|
||||||
|
|
||||||
|
export interface Department {
|
||||||
|
departmentId: string;
|
||||||
|
name: string;
|
||||||
|
para1: string;
|
||||||
|
para2: string;
|
||||||
|
para3: string;
|
||||||
|
facilities: string;
|
||||||
|
services: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDepartmentsApi = async () => {
|
||||||
|
const res = await apiClient.get("/departments/getAll");
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createDepartmentApi = async (data: {
|
||||||
|
departmentId: string;
|
||||||
|
name: string;
|
||||||
|
para1?: string;
|
||||||
|
para2?: string;
|
||||||
|
para3?: string;
|
||||||
|
facilities?: string;
|
||||||
|
services?: string;
|
||||||
|
}) => {
|
||||||
|
const res = await apiClient.post("/departments", data);
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateDepartmentApi = async (
|
||||||
|
departmentId: string,
|
||||||
|
data: {
|
||||||
|
name?: string;
|
||||||
|
para1?: string;
|
||||||
|
para2?: string;
|
||||||
|
para3?: string;
|
||||||
|
facilities?: string;
|
||||||
|
services?: string;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const res = await apiClient.put(`/departments/${departmentId}`, data);
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteDepartmentApi = async (departmentId: string) => {
|
||||||
|
const res = await apiClient.delete(`/departments/${departmentId}`);
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
56
frontend/src/api/doctor.ts
Normal file
56
frontend/src/api/doctor.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import apiClient from "@/api/client";
|
||||||
|
|
||||||
|
export interface Doctor {
|
||||||
|
doctorId: string;
|
||||||
|
name: string;
|
||||||
|
designation?: string;
|
||||||
|
workingStatus?: string;
|
||||||
|
qualification?: string;
|
||||||
|
|
||||||
|
departments: {
|
||||||
|
departmentId: string;
|
||||||
|
timing?: {
|
||||||
|
monday?: string;
|
||||||
|
tuesday?: string;
|
||||||
|
wednesday?: string;
|
||||||
|
thursday?: string;
|
||||||
|
friday?: string;
|
||||||
|
saturday?: string;
|
||||||
|
sunday?: string;
|
||||||
|
additional?: string;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDoctorsApi = async () => {
|
||||||
|
const res = await apiClient.get("/doctors/getAll");
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDoctorByIdApi = async (doctorId: string) => {
|
||||||
|
const res = await apiClient.get(`/doctors/${doctorId}`);
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createDoctorApi = async (data: Doctor) => {
|
||||||
|
const res = await apiClient.post("/doctors", data);
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateDoctorApi = async (
|
||||||
|
doctorId: string,
|
||||||
|
data: Partial<Doctor>,
|
||||||
|
) => {
|
||||||
|
const res = await apiClient.patch(`/doctors/${doctorId}`, data);
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteDoctorApi = async (doctorId: string) => {
|
||||||
|
const res = await apiClient.delete(`/doctors/${doctorId}`);
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDoctorTimingApi = async (doctorId: string) => {
|
||||||
|
const res = await apiClient.get(`/doctors/getTimings/${doctorId}`);
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
7
frontend/src/auth/ProtectedRoute.tsx
Normal file
7
frontend/src/auth/ProtectedRoute.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import {Navigate, Outlet} from "react-router-dom";
|
||||||
|
import {useAuth} from "@/context/AuthContext";
|
||||||
|
|
||||||
|
export default function ProtectedRoute() {
|
||||||
|
const {token} = useAuth();
|
||||||
|
return token ? <Outlet /> : <Navigate to="/" replace />;
|
||||||
|
}
|
||||||
7
frontend/src/auth/PublicRoute.tsx
Normal file
7
frontend/src/auth/PublicRoute.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import {Navigate, Outlet} from "react-router-dom";
|
||||||
|
import {useAuth} from "@/context/AuthContext";
|
||||||
|
|
||||||
|
export default function PublicRoute() {
|
||||||
|
const {token} = useAuth();
|
||||||
|
return token ? <Navigate to="/dashboard" replace /> : <Outlet />;
|
||||||
|
}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import Sidebar from "./Sidebar"
|
|
||||||
|
|
||||||
export default function DashboardLayout({children}:{children:React.ReactNode}){
|
|
||||||
|
|
||||||
return(
|
|
||||||
|
|
||||||
<div className="flex">
|
|
||||||
|
|
||||||
<Sidebar/>
|
|
||||||
|
|
||||||
<div className="flex-1 p-6 bg-slate-50 min-h-screen">
|
|
||||||
|
|
||||||
{children}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
35
frontend/src/components/layout/Header.tsx
Normal file
35
frontend/src/components/layout/Header.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import {useState, useEffect} from "react";
|
||||||
|
import {useAuth} from "@/context/AuthContext";
|
||||||
|
import {Button} from "@/components/ui/button";
|
||||||
|
import {Switch} from "@/components/ui/switch";
|
||||||
|
import {log} from "console";
|
||||||
|
|
||||||
|
export default function Header() {
|
||||||
|
const {user, logout} = useAuth();
|
||||||
|
const [darkMode, setDarkMode] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (darkMode) document.documentElement.classList.add("dark");
|
||||||
|
else document.documentElement.classList.remove("dark");
|
||||||
|
}, [darkMode]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="border-b bg-card">
|
||||||
|
<div className="flex items-center justify-between px-6 h-16">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">Welcome back</p>
|
||||||
|
<p className="font-semibold">{user?.username}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm">Dark</span>
|
||||||
|
<Switch checked={darkMode} onCheckedChange={setDarkMode} />
|
||||||
|
</div>
|
||||||
|
<Button variant="destructive" onClick={logout}>
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,17 +1,50 @@
|
|||||||
import {Link} from "react-router-dom";
|
import {Link, useLocation} from "react-router-dom";
|
||||||
|
|
||||||
|
import {Button} from "@/components/ui/button";
|
||||||
|
import {Separator} from "@/components/ui/separator";
|
||||||
|
|
||||||
export default function Sidebar() {
|
export default function Sidebar() {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{
|
||||||
|
name: "Department",
|
||||||
|
path: "/department",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Doctor",
|
||||||
|
path: "/doctor",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Blog",
|
||||||
|
path: "/blog",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-[220px] h-screen border-r bg-white p-4">
|
<div className="w-64 border-r bg-card">
|
||||||
<h2 className="text-lg font-semibold mb-6">Admin</h2>
|
<div className="p-6">
|
||||||
|
<h2 className="text-xl font-bold">GG Dashboard</h2>
|
||||||
<div className="space-y-3">
|
|
||||||
<Link to="/dashboard">Dashboard</Link>
|
|
||||||
|
|
||||||
<Link to="/blog">Blog</Link>
|
|
||||||
|
|
||||||
<Link to="/department">Department</Link>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<nav className="p-4 space-y-2">
|
||||||
|
{navItems.map((item) => {
|
||||||
|
const active = location.pathname === item.path;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link key={item.path} to={item.path}>
|
||||||
|
<Button
|
||||||
|
variant={active ? "secondary" : "ghost"}
|
||||||
|
className="w-full justify-start"
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
193
frontend/src/components/ui/command.tsx
Normal file
193
frontend/src/components/ui/command.tsx
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Command as CommandPrimitive } from "cmdk"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
import {
|
||||||
|
InputGroup,
|
||||||
|
InputGroupAddon,
|
||||||
|
} from "@/components/ui/input-group"
|
||||||
|
import { SearchIcon, CheckIcon } from "lucide-react"
|
||||||
|
|
||||||
|
function Command({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive
|
||||||
|
data-slot="command"
|
||||||
|
className={cn(
|
||||||
|
"flex size-full flex-col overflow-hidden rounded-xl! bg-popover p-1 text-popover-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandDialog({
|
||||||
|
title = "Command Palette",
|
||||||
|
description = "Search for a command to run...",
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
showCloseButton = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Dialog> & {
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
className?: string
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Dialog {...props}>
|
||||||
|
<DialogHeader className="sr-only">
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
<DialogDescription>{description}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogContent
|
||||||
|
className={cn(
|
||||||
|
"top-1/3 translate-y-0 overflow-hidden rounded-xl! p-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
showCloseButton={showCloseButton}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandInput({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||||
|
return (
|
||||||
|
<div data-slot="command-input-wrapper" className="p-1 pb-0">
|
||||||
|
<InputGroup className="h-8! rounded-lg! border-input/30 bg-input/30 shadow-none! *:data-[slot=input-group-addon]:pl-2!">
|
||||||
|
<CommandPrimitive.Input
|
||||||
|
data-slot="command-input"
|
||||||
|
className={cn(
|
||||||
|
"w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
<InputGroupAddon>
|
||||||
|
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||||
|
</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandList({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.List
|
||||||
|
data-slot="command-list"
|
||||||
|
className={cn(
|
||||||
|
"no-scrollbar max-h-72 scroll-py-1 overflow-x-hidden overflow-y-auto outline-none",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandEmpty({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Empty
|
||||||
|
data-slot="command-empty"
|
||||||
|
className={cn("py-6 text-center text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandGroup({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Group
|
||||||
|
data-slot="command-group"
|
||||||
|
className={cn(
|
||||||
|
"overflow-hidden p-1 text-foreground **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Separator
|
||||||
|
data-slot="command-separator"
|
||||||
|
className={cn("-mx-1 h-px bg-border", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Item
|
||||||
|
data-slot="command-item"
|
||||||
|
className={cn(
|
||||||
|
"group/command-item relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none in-data-[slot=dialog-content]:rounded-lg! data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-selected:bg-muted data-selected:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-selected:*:[svg]:text-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<CheckIcon className="ml-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100" />
|
||||||
|
</CommandPrimitive.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="command-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"ml-auto text-xs tracking-widest text-muted-foreground group-data-selected/command-item:text-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Command,
|
||||||
|
CommandDialog,
|
||||||
|
CommandInput,
|
||||||
|
CommandList,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandItem,
|
||||||
|
CommandShortcut,
|
||||||
|
CommandSeparator,
|
||||||
|
}
|
||||||
163
frontend/src/components/ui/dialog.tsx
Normal file
163
frontend/src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Dialog as DialogPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { XIcon } from "lucide-react"
|
||||||
|
|
||||||
|
function Dialog({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||||
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||||
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||||
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
data-slot="dialog-overlay"
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DialogPortal>
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
data-slot="dialog-content"
|
||||||
|
className={cn(
|
||||||
|
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-background p-4 text-sm ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{showCloseButton && (
|
||||||
|
<DialogPrimitive.Close data-slot="dialog-close" asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="absolute top-2 right-2"
|
||||||
|
size="icon-sm"
|
||||||
|
>
|
||||||
|
<XIcon
|
||||||
|
/>
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</Button>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
)}
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-header"
|
||||||
|
className={cn("flex flex-col gap-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogFooter({
|
||||||
|
className,
|
||||||
|
showCloseButton = false,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-footer"
|
||||||
|
className={cn(
|
||||||
|
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{showCloseButton && (
|
||||||
|
<DialogPrimitive.Close asChild>
|
||||||
|
<Button variant="outline">Close</Button>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
data-slot="dialog-title"
|
||||||
|
className={cn("text-base leading-none font-medium", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
data-slot="dialog-description"
|
||||||
|
className={cn(
|
||||||
|
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogPortal,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
}
|
||||||
156
frontend/src/components/ui/input-group.tsx
Normal file
156
frontend/src/components/ui/input-group.tsx
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
|
|
||||||
|
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="input-group"
|
||||||
|
role="group"
|
||||||
|
className={cn(
|
||||||
|
"group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputGroupAddonVariants = cva(
|
||||||
|
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
align: {
|
||||||
|
"inline-start":
|
||||||
|
"order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]",
|
||||||
|
"inline-end":
|
||||||
|
"order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]",
|
||||||
|
"block-start":
|
||||||
|
"order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2",
|
||||||
|
"block-end":
|
||||||
|
"order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
align: "inline-start",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function InputGroupAddon({
|
||||||
|
className,
|
||||||
|
align = "inline-start",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="group"
|
||||||
|
data-slot="input-group-addon"
|
||||||
|
data-align={align}
|
||||||
|
className={cn(inputGroupAddonVariants({ align }), className)}
|
||||||
|
onClick={(e) => {
|
||||||
|
if ((e.target as HTMLElement).closest("button")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.currentTarget.parentElement?.querySelector("input")?.focus()
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputGroupButtonVariants = cva(
|
||||||
|
"flex items-center gap-2 text-sm shadow-none",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
|
||||||
|
sm: "",
|
||||||
|
"icon-xs":
|
||||||
|
"size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0",
|
||||||
|
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
size: "xs",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function InputGroupButton({
|
||||||
|
className,
|
||||||
|
type = "button",
|
||||||
|
variant = "ghost",
|
||||||
|
size = "xs",
|
||||||
|
...props
|
||||||
|
}: Omit<React.ComponentProps<typeof Button>, "size"> &
|
||||||
|
VariantProps<typeof inputGroupButtonVariants>) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type={type}
|
||||||
|
data-size={size}
|
||||||
|
variant={variant}
|
||||||
|
className={cn(inputGroupButtonVariants({ size }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputGroupInput({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"input">) {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
data-slot="input-group-control"
|
||||||
|
className={cn(
|
||||||
|
"flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputGroupTextarea({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"textarea">) {
|
||||||
|
return (
|
||||||
|
<Textarea
|
||||||
|
data-slot="input-group-control"
|
||||||
|
className={cn(
|
||||||
|
"flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
InputGroup,
|
||||||
|
InputGroupAddon,
|
||||||
|
InputGroupButton,
|
||||||
|
InputGroupText,
|
||||||
|
InputGroupInput,
|
||||||
|
InputGroupTextarea,
|
||||||
|
}
|
||||||
87
frontend/src/components/ui/popover.tsx
Normal file
87
frontend/src/components/ui/popover.tsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Popover as PopoverPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Popover({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||||
|
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||||
|
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverContent({
|
||||||
|
className,
|
||||||
|
align = "center",
|
||||||
|
sideOffset = 4,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<PopoverPrimitive.Portal>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
data-slot="popover-content"
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 flex w-72 origin-(--radix-popover-content-transform-origin) flex-col gap-2.5 rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverAnchor({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||||
|
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="popover-header"
|
||||||
|
className={cn("flex flex-col gap-0.5 text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="popover-title"
|
||||||
|
className={cn("font-medium", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"p">) {
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
data-slot="popover-description"
|
||||||
|
className={cn("text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Popover,
|
||||||
|
PopoverAnchor,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverDescription,
|
||||||
|
PopoverHeader,
|
||||||
|
PopoverTitle,
|
||||||
|
PopoverTrigger,
|
||||||
|
}
|
||||||
53
frontend/src/components/ui/scroll-area.tsx
Normal file
53
frontend/src/components/ui/scroll-area.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function ScrollArea({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<ScrollAreaPrimitive.Root
|
||||||
|
data-slot="scroll-area"
|
||||||
|
className={cn("relative", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.Viewport
|
||||||
|
data-slot="scroll-area-viewport"
|
||||||
|
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
<ScrollBar />
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ScrollBar({
|
||||||
|
className,
|
||||||
|
orientation = "vertical",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||||
|
return (
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||||
|
data-slot="scroll-area-scrollbar"
|
||||||
|
data-orientation={orientation}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"flex touch-none p-px transition-colors select-none data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||||
|
data-slot="scroll-area-thumb"
|
||||||
|
className="relative flex-1 rounded-full bg-border"
|
||||||
|
/>
|
||||||
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ScrollArea, ScrollBar }
|
||||||
31
frontend/src/components/ui/switch.tsx
Normal file
31
frontend/src/components/ui/switch.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import {Switch as SwitchPrimitive} from "radix-ui";
|
||||||
|
|
||||||
|
import {cn} from "@/lib/utils";
|
||||||
|
|
||||||
|
function Switch({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SwitchPrimitive.Root> & {
|
||||||
|
size?: "sm" | "default";
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SwitchPrimitive.Root
|
||||||
|
data-slot="switch"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SwitchPrimitive.Thumb
|
||||||
|
data-slot="switch-thumb"
|
||||||
|
className="pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
|
||||||
|
/>
|
||||||
|
</SwitchPrimitive.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {Switch};
|
||||||
114
frontend/src/components/ui/table.tsx
Normal file
114
frontend/src/components/ui/table.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="table-container"
|
||||||
|
className="relative w-full overflow-x-auto"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
data-slot="table"
|
||||||
|
className={cn("w-full caption-bottom text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||||
|
return (
|
||||||
|
<thead
|
||||||
|
data-slot="table-header"
|
||||||
|
className={cn("[&_tr]:border-b", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||||
|
return (
|
||||||
|
<tbody
|
||||||
|
data-slot="table-body"
|
||||||
|
className={cn("[&_tr:last-child]:border-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||||
|
return (
|
||||||
|
<tfoot
|
||||||
|
data-slot="table-footer"
|
||||||
|
className={cn(
|
||||||
|
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
data-slot="table-row"
|
||||||
|
className={cn(
|
||||||
|
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
data-slot="table-head"
|
||||||
|
className={cn(
|
||||||
|
"h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
data-slot="table-cell"
|
||||||
|
className={cn(
|
||||||
|
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCaption({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"caption">) {
|
||||||
|
return (
|
||||||
|
<caption
|
||||||
|
data-slot="table-caption"
|
||||||
|
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableCaption,
|
||||||
|
}
|
||||||
18
frontend/src/components/ui/textarea.tsx
Normal file
18
frontend/src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
data-slot="textarea"
|
||||||
|
className={cn(
|
||||||
|
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Textarea }
|
||||||
@@ -1,3 +1,124 @@
|
|||||||
@tailwind base;
|
@import "tailwindcss";
|
||||||
@tailwind components;
|
@import "tw-animate-css";
|
||||||
@tailwind utilities;
|
@import "shadcn/tailwind.css";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--radius-2xl: calc(var(--radius) + 8px);
|
||||||
|
--radius-3xl: calc(var(--radius) + 12px);
|
||||||
|
--radius-4xl: calc(var(--radius) + 16px);
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--primary: oklch(0.21 0.006 285.885);
|
||||||
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
--secondary: oklch(0.967 0.001 286.375);
|
||||||
|
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--muted: oklch(0.967 0.001 286.375);
|
||||||
|
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||||
|
--accent: oklch(0.967 0.001 286.375);
|
||||||
|
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.92 0.004 286.32);
|
||||||
|
--input: oklch(0.92 0.004 286.32);
|
||||||
|
--ring: oklch(0.705 0.015 286.067);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--sidebar-primary: oklch(0.21 0.006 285.885);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||||
|
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||||
|
--sidebar-ring: oklch(0.705 0.015 286.067);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.141 0.005 285.823);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.21 0.006 285.885);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.21 0.006 285.885);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.92 0.004 286.32);
|
||||||
|
--primary-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--secondary: oklch(0.274 0.006 286.033);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.274 0.006 286.033);
|
||||||
|
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||||
|
--accent: oklch(0.274 0.006 286.033);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.552 0.016 285.938);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.21 0.006 285.885);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.552 0.016 285.938);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
18
frontend/src/layouts/DashboardLayout.tsx
Normal file
18
frontend/src/layouts/DashboardLayout.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import {Outlet} from "react-router-dom";
|
||||||
|
|
||||||
|
import Sidebar from "@/components/layout/Sidebar";
|
||||||
|
import Header from "@/components/layout/Header";
|
||||||
|
|
||||||
|
export default function DashboardLayout() {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen bg-background">
|
||||||
|
<Sidebar />
|
||||||
|
<div className="flex flex-col flex-1">
|
||||||
|
<Header />
|
||||||
|
<main className="flex-1 p-6">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,13 +1,182 @@
|
|||||||
import DashboardLayout from "@/components/layout/DashboardLayout";
|
import {useState, useEffect, useCallback} from "react";
|
||||||
|
import {AxiosError} from "axios";
|
||||||
|
import {useNavigate} from "react-router-dom";
|
||||||
|
|
||||||
|
import {getAllBlogsApi, deleteBlogApi} from "@/api/blog";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
|
||||||
|
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
||||||
|
import {Button} from "@/components/ui/button";
|
||||||
|
import {Input} from "@/components/ui/input";
|
||||||
|
|
||||||
|
import {Loader2, RefreshCw, Plus, Pencil, Trash} from "lucide-react";
|
||||||
|
|
||||||
|
interface Blog {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
writer: string;
|
||||||
|
image: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BlogPage() {
|
||||||
|
const [blogs, setBlogs] = useState<Blog[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [searchText, setSearchText] = useState("");
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const fetchBlogs = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await getAllBlogsApi();
|
||||||
|
|
||||||
|
if (Array.isArray(res)) {
|
||||||
|
setBlogs(res);
|
||||||
|
} else {
|
||||||
|
setBlogs([]);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof AxiosError) {
|
||||||
|
setError(err.response?.data?.message || "Failed to load blogs");
|
||||||
|
} else {
|
||||||
|
setError("Something went wrong");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchBlogs();
|
||||||
|
}, [fetchBlogs]);
|
||||||
|
|
||||||
|
const filteredBlogs = blogs.filter((b) => {
|
||||||
|
const text = searchText.toLowerCase();
|
||||||
|
|
||||||
|
return (
|
||||||
|
b.title?.toLowerCase().includes(text) ||
|
||||||
|
b.writer?.toLowerCase().includes(text)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleDelete(id: number) {
|
||||||
|
const confirmDelete = confirm("Delete this blog?");
|
||||||
|
if (!confirmDelete) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteBlogApi(id);
|
||||||
|
fetchBlogs();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function Blog() {
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<div className="p-6 space-y-6">
|
||||||
<h1 className="text-xl font-semibold mb-4">Blog Management</h1>
|
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-3">
|
||||||
|
<h1 className="text-2xl font-bold">Blogs</h1>
|
||||||
|
|
||||||
<button className="px-4 py-2 bg-black text-white rounded">
|
<div className="flex flex-wrap gap-3">
|
||||||
Create Blog
|
<Input
|
||||||
</button>
|
placeholder="Search blog..."
|
||||||
</DashboardLayout>
|
value={searchText}
|
||||||
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
|
className="w-[220px]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button variant="outline" onClick={fetchBlogs} disabled={loading}>
|
||||||
|
<RefreshCw className="mr-2 h-4 w-4" />
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button onClick={() => navigate("/blog/create")}>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Add Blog
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="p-4 text-red-600 bg-red-50 border rounded-md">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Blog List</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
<div className="border rounded-md overflow-x-auto max-w-full">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>ID</TableHead>
|
||||||
|
<TableHead>Title</TableHead>
|
||||||
|
<TableHead>Writer</TableHead>
|
||||||
|
<TableHead>Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
|
||||||
|
<TableBody>
|
||||||
|
{loading ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={4} className="text-center">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin mx-auto" />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : filteredBlogs.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={4} className="text-center">
|
||||||
|
No blogs found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
filteredBlogs.map((blog) => (
|
||||||
|
<TableRow key={blog.id}>
|
||||||
|
<TableCell>{blog.id}</TableCell>
|
||||||
|
|
||||||
|
<TableCell>{blog.title}</TableCell>
|
||||||
|
|
||||||
|
<TableCell>{blog.writer}</TableCell>
|
||||||
|
|
||||||
|
<TableCell className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => navigate(`/blog/edit/${blog.id}`)}
|
||||||
|
>
|
||||||
|
<Pencil className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => handleDelete(blog.id)}
|
||||||
|
>
|
||||||
|
<Trash className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
208
frontend/src/pages/BlogEditor.tsx
Normal file
208
frontend/src/pages/BlogEditor.tsx
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
import {useEffect, useRef, useState} from "react";
|
||||||
|
import {useNavigate, useParams} from "react-router-dom";
|
||||||
|
|
||||||
|
import EditorJS, {OutputData} from "@editorjs/editorjs";
|
||||||
|
import Header from "@editorjs/header";
|
||||||
|
import List from "@editorjs/list";
|
||||||
|
import ImageTool from "@editorjs/image";
|
||||||
|
import Quote from "@editorjs/quote";
|
||||||
|
import Table from "@editorjs/table";
|
||||||
|
import CodeTool from "@editorjs/code";
|
||||||
|
import Embed from "@editorjs/embed";
|
||||||
|
import Delimiter from "@editorjs/delimiter";
|
||||||
|
|
||||||
|
import {
|
||||||
|
createBlogApi,
|
||||||
|
updateBlogApi,
|
||||||
|
getBlogByIdApi,
|
||||||
|
uploadImageApi,
|
||||||
|
} from "@/api/blog";
|
||||||
|
|
||||||
|
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
||||||
|
import {Input} from "@/components/ui/input";
|
||||||
|
import {Button} from "@/components/ui/button";
|
||||||
|
|
||||||
|
export default function BlogEditorPage() {
|
||||||
|
const {id} = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const editorRef = useRef<EditorJS | null>(null);
|
||||||
|
const hasInitialized = useRef(false);
|
||||||
|
const hasRenderedContent = useRef(false);
|
||||||
|
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
const [writer, setWriter] = useState("");
|
||||||
|
const [coverImage, setCoverImage] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const isEdit = Boolean(id);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasInitialized.current) return;
|
||||||
|
hasInitialized.current = true;
|
||||||
|
|
||||||
|
let editor: EditorJS;
|
||||||
|
|
||||||
|
const initEditor = async () => {
|
||||||
|
editor = new EditorJS({
|
||||||
|
holder: "editorjs",
|
||||||
|
|
||||||
|
placeholder: "Write blog content...",
|
||||||
|
|
||||||
|
tools: {
|
||||||
|
header: {
|
||||||
|
class: Header,
|
||||||
|
inlineToolbar: true,
|
||||||
|
config: {
|
||||||
|
placeholder: "Enter heading",
|
||||||
|
levels: [1, 2, 3, 4],
|
||||||
|
defaultLevel: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
list: {
|
||||||
|
class: List,
|
||||||
|
inlineToolbar: true,
|
||||||
|
config: {
|
||||||
|
defaultStyle: "unordered",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
quote: Quote,
|
||||||
|
table: Table,
|
||||||
|
code: CodeTool,
|
||||||
|
embed: Embed,
|
||||||
|
delimiter: Delimiter,
|
||||||
|
|
||||||
|
image: {
|
||||||
|
class: ImageTool,
|
||||||
|
config: {
|
||||||
|
uploader: {
|
||||||
|
uploadByFile: async (file: File) => {
|
||||||
|
const res = await uploadImageApi(file);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: 1,
|
||||||
|
file: {url: res.file.url},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await editor.isReady;
|
||||||
|
editorRef.current = editor;
|
||||||
|
|
||||||
|
if (isEdit && id && !hasRenderedContent.current) {
|
||||||
|
try {
|
||||||
|
const res = await getBlogByIdApi(Number(id));
|
||||||
|
|
||||||
|
setTitle(res.title);
|
||||||
|
setWriter(res.writer);
|
||||||
|
setCoverImage(res.image || "");
|
||||||
|
|
||||||
|
if (res.content) {
|
||||||
|
await editor.blocks.clear();
|
||||||
|
await editor.render(res.content);
|
||||||
|
hasRenderedContent.current = true;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initEditor();
|
||||||
|
}, [id, isEdit]);
|
||||||
|
|
||||||
|
const handleCoverUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await uploadImageApi(file);
|
||||||
|
setCoverImage(res.file.url);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!editorRef.current) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content: OutputData = await editorRef.current.save();
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
title,
|
||||||
|
writer,
|
||||||
|
image: coverImage,
|
||||||
|
content,
|
||||||
|
isActive: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isEdit) {
|
||||||
|
await updateBlogApi(Number(id), payload);
|
||||||
|
} else {
|
||||||
|
await createBlogApi(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate("/blog");
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 max-w-5xl mx-auto space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{isEdit ? "Edit Blog" : "Create Blog"}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<Input
|
||||||
|
placeholder="Blog Title"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
placeholder="Writer Name"
|
||||||
|
value={writer}
|
||||||
|
onChange={(e) => setWriter(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Cover Image</label>
|
||||||
|
|
||||||
|
<Input type="file" onChange={handleCoverUpload} />
|
||||||
|
|
||||||
|
{coverImage && (
|
||||||
|
<img
|
||||||
|
src={coverImage}
|
||||||
|
alt="cover"
|
||||||
|
className="w-full max-h-[250px] object-cover rounded-md"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="editorjs"
|
||||||
|
className="border rounded-md p-4 bg-white min-h-[300px]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button onClick={handleSave} disabled={loading}>
|
||||||
|
{loading ? "Saving..." : "Save Blog"}
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import DashboardLayout from "@/components/layout/DashboardLayout";
|
|
||||||
|
|
||||||
export default function Dashboard() {
|
|
||||||
return (
|
|
||||||
<DashboardLayout>
|
|
||||||
<h1 className="text-2xl font-bold mb-6">Dashboard</h1>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-6">
|
|
||||||
<div className="p-6 bg-white shadow rounded">Blogs</div>
|
|
||||||
|
|
||||||
<div className="p-6 bg-white shadow rounded">Departments</div>
|
|
||||||
|
|
||||||
<div className="p-6 bg-white shadow rounded">Users</div>
|
|
||||||
</div>
|
|
||||||
</DashboardLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import DashboardLayout from "@/components/layout/DashboardLayout";
|
|
||||||
|
|
||||||
export default function Department() {
|
|
||||||
return (
|
|
||||||
<DashboardLayout>
|
|
||||||
<h1 className="text-xl font-semibold mb-4">Departments</h1>
|
|
||||||
</DashboardLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
358
frontend/src/pages/Department.tsx
Normal file
358
frontend/src/pages/Department.tsx
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
import {useState, useEffect, useCallback} from "react";
|
||||||
|
import {AxiosError} from "axios";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getDepartmentsApi,
|
||||||
|
createDepartmentApi,
|
||||||
|
updateDepartmentApi,
|
||||||
|
deleteDepartmentApi,
|
||||||
|
} from "@/api/department";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
|
||||||
|
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
||||||
|
import {Button} from "@/components/ui/button";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
import {Input} from "@/components/ui/input";
|
||||||
|
import {Textarea} from "@/components/ui/textarea";
|
||||||
|
|
||||||
|
import {Loader2, RefreshCw, Plus, Pencil, Trash} from "lucide-react";
|
||||||
|
|
||||||
|
interface Department {
|
||||||
|
departmentId: string;
|
||||||
|
name: string;
|
||||||
|
para1: string;
|
||||||
|
para2: string;
|
||||||
|
para3: string;
|
||||||
|
facilities: string;
|
||||||
|
services: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DepartmentPage() {
|
||||||
|
const [departments, setDepartments] = useState<Department[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
const [openModal, setOpenModal] = useState(false);
|
||||||
|
const [editing, setEditing] = useState<Department | null>(null);
|
||||||
|
|
||||||
|
const [searchText, setSearchText] = useState("");
|
||||||
|
|
||||||
|
const [form, setForm] = useState<Department>({
|
||||||
|
departmentId: "",
|
||||||
|
name: "",
|
||||||
|
para1: "",
|
||||||
|
para2: "",
|
||||||
|
para3: "",
|
||||||
|
facilities: "",
|
||||||
|
services: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchDepartments = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await getDepartmentsApi();
|
||||||
|
setDepartments(res?.data || []);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof AxiosError) {
|
||||||
|
setError(err.response?.data?.message || "Failed to load departments");
|
||||||
|
} else {
|
||||||
|
setError("Something went wrong");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDepartments();
|
||||||
|
}, [fetchDepartments]);
|
||||||
|
|
||||||
|
const filteredDepartments = departments.filter((dep) => {
|
||||||
|
const text = searchText.toLowerCase();
|
||||||
|
|
||||||
|
return (
|
||||||
|
dep.name.toLowerCase().includes(text) ||
|
||||||
|
dep.departmentId.toLowerCase().includes(text) ||
|
||||||
|
dep.para1.toLowerCase().includes(text) ||
|
||||||
|
dep.para2.toLowerCase().includes(text) ||
|
||||||
|
dep.para3.toLowerCase().includes(text) ||
|
||||||
|
dep.facilities.toLowerCase().includes(text) ||
|
||||||
|
dep.services.toLowerCase().includes(text)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleChange(
|
||||||
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||||
|
) {
|
||||||
|
setForm({
|
||||||
|
...form,
|
||||||
|
[e.target.name]: e.target.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAdd() {
|
||||||
|
setEditing(null);
|
||||||
|
|
||||||
|
setForm({
|
||||||
|
departmentId: "",
|
||||||
|
name: "",
|
||||||
|
para1: "",
|
||||||
|
para2: "",
|
||||||
|
para3: "",
|
||||||
|
facilities: "",
|
||||||
|
services: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
setOpenModal(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(dep: Department) {
|
||||||
|
setEditing(dep);
|
||||||
|
setForm(dep);
|
||||||
|
setOpenModal(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
try {
|
||||||
|
if (editing) {
|
||||||
|
const {departmentId, ...updateData} = form;
|
||||||
|
await updateDepartmentApi(editing.departmentId, updateData);
|
||||||
|
} else {
|
||||||
|
await createDepartmentApi(form);
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpenModal(false);
|
||||||
|
fetchDepartments();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(departmentId: string) {
|
||||||
|
const confirmDelete = confirm("Delete this department?");
|
||||||
|
if (!confirmDelete) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteDepartmentApi(departmentId);
|
||||||
|
fetchDepartments();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{/* HEADER */}
|
||||||
|
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-3">
|
||||||
|
<h1 className="text-2xl font-bold">Departments</h1>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
<Input
|
||||||
|
placeholder="Search department..."
|
||||||
|
value={searchText}
|
||||||
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
|
className="w-[220px]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={fetchDepartments}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<RefreshCw className="mr-2 h-4 w-4" />
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button onClick={openAdd}>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Add Department
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="p-4 text-red-600 bg-red-50 border rounded-md">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Department List</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
<div className="border rounded-md overflow-x-auto max-w-full">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>ID</TableHead>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Para1</TableHead>
|
||||||
|
<TableHead>Para2</TableHead>
|
||||||
|
<TableHead>Para3</TableHead>
|
||||||
|
<TableHead>Facilities</TableHead>
|
||||||
|
<TableHead>Services</TableHead>
|
||||||
|
<TableHead>Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
|
||||||
|
<TableBody>
|
||||||
|
{loading ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={8} className="text-center">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin mx-auto" />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : filteredDepartments.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={8} className="text-center">
|
||||||
|
No departments found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
filteredDepartments.map((dep) => (
|
||||||
|
<TableRow key={dep.departmentId}>
|
||||||
|
<TableCell>{dep.departmentId}</TableCell>
|
||||||
|
|
||||||
|
<TableCell>{dep.name}</TableCell>
|
||||||
|
|
||||||
|
<TableCell className="max-w-[300px] whitespace-normal break-words">
|
||||||
|
{dep.para1}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell className="max-w-[300px] whitespace-normal break-words">
|
||||||
|
{dep.para2}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell className="max-w-[300px] whitespace-normal break-words">
|
||||||
|
{dep.para3}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell className="max-w-[300px] whitespace-normal break-words">
|
||||||
|
{dep.facilities}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell className="max-w-[300px] whitespace-normal break-words">
|
||||||
|
{dep.services}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => openEdit(dep)}
|
||||||
|
>
|
||||||
|
<Pencil className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => handleDelete(dep.departmentId)}
|
||||||
|
>
|
||||||
|
<Trash className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* MODAL */}
|
||||||
|
<Dialog open={openModal} onOpenChange={setOpenModal}>
|
||||||
|
<DialogContent className="max-w-4xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{editing ? "Edit Department" : "Add Department"}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 max-h-[70vh] overflow-y-auto pr-2">
|
||||||
|
<Input
|
||||||
|
name="departmentId"
|
||||||
|
placeholder="Department ID"
|
||||||
|
value={form.departmentId}
|
||||||
|
onChange={handleChange}
|
||||||
|
disabled={!!editing}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
name="name"
|
||||||
|
placeholder="Department Name"
|
||||||
|
value={form.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
name="para1"
|
||||||
|
placeholder="Paragraph 1"
|
||||||
|
value={form.para1}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
name="para2"
|
||||||
|
placeholder="Paragraph 2"
|
||||||
|
value={form.para2}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
name="para3"
|
||||||
|
placeholder="Paragraph 3"
|
||||||
|
value={form.para3}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
name="facilities"
|
||||||
|
placeholder="Facilities"
|
||||||
|
value={form.facilities}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
name="services"
|
||||||
|
placeholder="Services"
|
||||||
|
value={form.services}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setOpenModal(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button onClick={handleSubmit}>
|
||||||
|
{editing ? "Update" : "Create"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
471
frontend/src/pages/Doctor.tsx
Normal file
471
frontend/src/pages/Doctor.tsx
Normal file
@@ -0,0 +1,471 @@
|
|||||||
|
import {useState, useEffect, useCallback} from "react";
|
||||||
|
import {
|
||||||
|
getDoctorsApi,
|
||||||
|
createDoctorApi,
|
||||||
|
updateDoctorApi,
|
||||||
|
deleteDoctorApi,
|
||||||
|
getDoctorTimingApi,
|
||||||
|
} from "@/api/doctor";
|
||||||
|
|
||||||
|
import {getDepartmentsApi} from "@/api/department";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
|
||||||
|
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
||||||
|
import {Button} from "@/components/ui/button";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandGroup,
|
||||||
|
CommandItem,
|
||||||
|
CommandInput,
|
||||||
|
} from "@/components/ui/command";
|
||||||
|
|
||||||
|
import {Input} from "@/components/ui/input";
|
||||||
|
import {Loader2, Plus, Pencil, Trash, RefreshCw} from "lucide-react";
|
||||||
|
|
||||||
|
interface Department {
|
||||||
|
departmentId: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DoctorPage() {
|
||||||
|
const [doctors, setDoctors] = useState<any[]>([]);
|
||||||
|
const [departments, setDepartments] = useState<Department[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [openModal, setOpenModal] = useState(false);
|
||||||
|
const [editing, setEditing] = useState<any>(null);
|
||||||
|
|
||||||
|
const [searchText, setSearchText] = useState("");
|
||||||
|
const [filterDepartment, setFilterDepartment] = useState("");
|
||||||
|
|
||||||
|
const [form, setForm] = useState<any>({
|
||||||
|
doctorId: "",
|
||||||
|
name: "",
|
||||||
|
designation: "",
|
||||||
|
workingStatus: "",
|
||||||
|
qualification: "",
|
||||||
|
departments: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchAll = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const [docRes, depRes] = await Promise.all([
|
||||||
|
getDoctorsApi(),
|
||||||
|
getDepartmentsApi(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
setDoctors(docRes?.data || []);
|
||||||
|
setDepartments(depRes?.data || []);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAll();
|
||||||
|
}, [fetchAll]);
|
||||||
|
|
||||||
|
const filteredDoctors = doctors.filter((doc) => {
|
||||||
|
const matchesSearch =
|
||||||
|
doc.name.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||||
|
doc.doctorId.toLowerCase().includes(searchText.toLowerCase());
|
||||||
|
|
||||||
|
const matchesDepartment = filterDepartment
|
||||||
|
? doc.departments?.some((d: any) => d.departmentId === filterDepartment)
|
||||||
|
: true;
|
||||||
|
|
||||||
|
return matchesSearch && matchesDepartment;
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleChange(e: any) {
|
||||||
|
setForm({...form, [e.target.name]: e.target.value});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDepartmentChange(depId: string) {
|
||||||
|
const exists = form.departments.find((d: any) => d.departmentId === depId);
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
setForm({
|
||||||
|
...form,
|
||||||
|
departments: form.departments.filter(
|
||||||
|
(d: any) => d.departmentId !== depId,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setForm({
|
||||||
|
...form,
|
||||||
|
departments: [...form.departments, {departmentId: depId, timing: {}}],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTimingChange(depId: string, day: string, value: string) {
|
||||||
|
setForm({
|
||||||
|
...form,
|
||||||
|
departments: form.departments.map((d: any) =>
|
||||||
|
d.departmentId === depId
|
||||||
|
? {
|
||||||
|
...d,
|
||||||
|
timing: {...d.timing, [day]: value},
|
||||||
|
}
|
||||||
|
: d,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAdd() {
|
||||||
|
setEditing(null);
|
||||||
|
setForm({
|
||||||
|
doctorId: "",
|
||||||
|
name: "",
|
||||||
|
designation: "",
|
||||||
|
workingStatus: "",
|
||||||
|
qualification: "",
|
||||||
|
departments: [],
|
||||||
|
});
|
||||||
|
setOpenModal(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openEdit(doc: any) {
|
||||||
|
setEditing(doc);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const timingRes = await getDoctorTimingApi(doc.doctorId);
|
||||||
|
const timingData = timingRes?.data?.departments || [];
|
||||||
|
|
||||||
|
const mappedDepartments = timingData.map((d: any) => ({
|
||||||
|
departmentId: d.departmentId,
|
||||||
|
timing: d.timing || {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
setForm({
|
||||||
|
doctorId: doc.doctorId,
|
||||||
|
name: doc.name,
|
||||||
|
designation: doc.designation,
|
||||||
|
workingStatus: doc.workingStatus,
|
||||||
|
qualification: doc.qualification,
|
||||||
|
departments: mappedDepartments,
|
||||||
|
});
|
||||||
|
|
||||||
|
setOpenModal(true);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
try {
|
||||||
|
if (editing) {
|
||||||
|
await updateDoctorApi(editing.doctorId, form);
|
||||||
|
} else {
|
||||||
|
await createDoctorApi(form);
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpenModal(false);
|
||||||
|
fetchAll();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(id: string) {
|
||||||
|
if (!confirm("Delete doctor?")) return;
|
||||||
|
await deleteDoctorApi(id);
|
||||||
|
fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{/* HEADER */}
|
||||||
|
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-3">
|
||||||
|
<h1 className="text-2xl font-bold">Doctors</h1>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="Search doctor..."
|
||||||
|
value={searchText}
|
||||||
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
|
className="w-[200px]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<select
|
||||||
|
value={filterDepartment}
|
||||||
|
onChange={(e) => setFilterDepartment(e.target.value)}
|
||||||
|
className="border rounded px-2 py-1"
|
||||||
|
>
|
||||||
|
<option value="">All Departments</option>
|
||||||
|
{departments.map((dep) => (
|
||||||
|
<option key={dep.departmentId} value={dep.departmentId}>
|
||||||
|
{dep.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<Button variant="outline" onClick={fetchAll} disabled={loading}>
|
||||||
|
<RefreshCw className="mr-2 h-4 w-4" />
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button onClick={openAdd}>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Add Doctor
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* TABLE */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Doctor List</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table className="min-w-[1000px]">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>ID</TableHead>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Designation</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead>Qualification</TableHead>
|
||||||
|
<TableHead>Departments</TableHead>
|
||||||
|
<TableHead>Timing</TableHead>
|
||||||
|
<TableHead>Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
|
||||||
|
<TableBody>
|
||||||
|
{loading ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={8} className="text-center">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin mx-auto" />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : filteredDoctors.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={8} className="text-center">
|
||||||
|
No doctors found
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
filteredDoctors.map((doc) => (
|
||||||
|
<TableRow key={doc.doctorId}>
|
||||||
|
<TableCell>{doc.doctorId}</TableCell>
|
||||||
|
<TableCell>{doc.name}</TableCell>
|
||||||
|
<TableCell>{doc.designation}</TableCell>
|
||||||
|
<TableCell>{doc.workingStatus}</TableCell>
|
||||||
|
<TableCell>{doc.qualification}</TableCell>
|
||||||
|
|
||||||
|
<TableCell>
|
||||||
|
{doc.departments
|
||||||
|
?.map((d: any) => d.departmentName)
|
||||||
|
.join(", ")}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell className="max-w-[250px] whitespace-normal">
|
||||||
|
{doc.departments?.map((d: any) => (
|
||||||
|
<div key={d.departmentId}>
|
||||||
|
<b>{d.departmentName}:</b>{" "}
|
||||||
|
{JSON.stringify(d.timing)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => openEdit(doc)}
|
||||||
|
>
|
||||||
|
<Pencil className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => handleDelete(doc.doctorId)}
|
||||||
|
>
|
||||||
|
<Trash className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* MODAL */}
|
||||||
|
{/* MODAL */}
|
||||||
|
<Dialog open={openModal} onOpenChange={setOpenModal}>
|
||||||
|
<DialogContent className="overflow-y-auto max-h-[80vh] ">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{editing ? "Edit Doctor" : "Add Doctor"}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Input
|
||||||
|
name="doctorId"
|
||||||
|
placeholder="Doctor ID"
|
||||||
|
value={form.doctorId}
|
||||||
|
onChange={handleChange}
|
||||||
|
disabled={!!editing}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
name="name"
|
||||||
|
placeholder="Name"
|
||||||
|
value={form.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
name="designation"
|
||||||
|
placeholder="Designation"
|
||||||
|
value={form.designation}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
name="workingStatus"
|
||||||
|
placeholder="Working Status"
|
||||||
|
value={form.workingStatus}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
name="qualification"
|
||||||
|
placeholder="Qualification"
|
||||||
|
value={form.qualification}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Departments */}
|
||||||
|
<div>
|
||||||
|
<p className="font-medium mb-2">Departments</p>
|
||||||
|
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button className="w-full justify-between h-auto min-h-[40px]">
|
||||||
|
{form.departments.length > 0 ? (
|
||||||
|
<div className="flex flex-col items-start gap-1 text-left">
|
||||||
|
{form.departments.map((d: any) => {
|
||||||
|
const name = departments.find(
|
||||||
|
(dep) => dep.departmentId === d.departmentId,
|
||||||
|
)?.name;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span key={d.departmentId} className="text-sm">
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span>Select Departments</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
|
||||||
|
<PopoverContent className="w-[300px] p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="Search department..." />
|
||||||
|
|
||||||
|
<CommandGroup className="max-h-[250px] overflow-y-auto">
|
||||||
|
{departments.map((dep) => {
|
||||||
|
const selected = form.departments.some(
|
||||||
|
(d: any) => d.departmentId === dep.departmentId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandItem
|
||||||
|
key={dep.departmentId}
|
||||||
|
className="flex justify-between"
|
||||||
|
onSelect={() =>
|
||||||
|
handleDepartmentChange(dep.departmentId)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>{dep.name}</span>
|
||||||
|
{selected && <span>✔</span>}
|
||||||
|
</CommandItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CommandGroup>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{form.departments.map((dep: any) => {
|
||||||
|
const depName = departments.find(
|
||||||
|
(d) => d.departmentId === dep.departmentId,
|
||||||
|
)?.name;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={dep.departmentId}
|
||||||
|
className="tw-border tw-p-3 tw-rounded"
|
||||||
|
>
|
||||||
|
<p className="tw-font-semibold">{depName}</p>
|
||||||
|
|
||||||
|
{[
|
||||||
|
"monday",
|
||||||
|
"tuesday",
|
||||||
|
"wednesday",
|
||||||
|
"thursday",
|
||||||
|
"friday",
|
||||||
|
"saturday",
|
||||||
|
"sunday",
|
||||||
|
"additional",
|
||||||
|
].map((day) => (
|
||||||
|
<Input
|
||||||
|
key={day}
|
||||||
|
placeholder={day}
|
||||||
|
value={dep.timing?.[day] || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleTimingChange(
|
||||||
|
dep.departmentId,
|
||||||
|
day,
|
||||||
|
e.target.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setOpenModal(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSubmit}>
|
||||||
|
{editing ? "Update" : "Create"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,4 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
export default {
|
export default {
|
||||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||||
theme: {
|
|
||||||
extend: {},
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user