Compare commits

..

44 Commits

Author SHA1 Message Date
Kailasdevdas 78e2618a29 chore: file formatting 2026-05-26 15:48:01 +05:30
Kailasdevdas 8a21e0bf38 chore: setup prettier 2026-05-26 15:23:53 +05:30
kailasdevdas 6c5e529017 Merge pull request 'feat: seo preview' (#44) from feat/healthcheck-seo into dev
Reviewed-on: #44
2026-05-26 07:33:40 +00:00
Kailasdevdas 8b563e45a2 feat: health check preview url 2026-05-26 12:58:13 +05:30
Kailasdevdas e6d77b72b4 feat: health check seo preview 2026-05-26 12:41:43 +05:30
Kailasdevdas 3d7b8eef6c Merge branch 'feat/seo-preview' into feat/healthcheck-seo 2026-05-26 12:39:17 +05:30
rishalkv 5aae2824ef fix: edge case creation of og 2026-05-26 12:36:11 +05:30
Kailasdevdas f3cb4aee91 Merge branch 'feat/seo-preview' into feat/healthcheck-seo 2026-05-26 12:34:21 +05:30
rishalkv 3af6401429 fix: og title description 2026-05-26 12:33:51 +05:30
Kailasdevdas 2e63106439 Merge branch 'feat/seo-preview' into feat/healthcheck-seo 2026-05-26 11:59:23 +05:30
rishalkv c2b54725fe fix: og image update 2026-05-26 11:57:10 +05:30
Kailasdevdas 4d73da5ddd feat: health check seo 2026-05-26 11:56:22 +05:30
rishalkv fa06126219 chore: add seo reusable component 2026-05-26 11:38:34 +05:30
rishalkv fc491f4050 feat: seo preview 2026-05-25 16:20:47 +05:30
kailasdevdas 31c0e50177 Merge pull request 'fix: handle empty package pricing fields correctly' (#42) from fix/optional-pricing into dev
Reviewed-on: #42
2026-05-25 07:37:12 +00:00
Kailasdevdas 8f813ed7c4 fix: handle empty package pricing fields correctly 2026-05-25 12:59:44 +05:30
kailasdevdas 9a14965a54 Merge pull request 'fix: remove duplicate toasts' (#41) from fix/optional-pricing into dev
Reviewed-on: #41
2026-05-25 07:00:27 +00:00
kailasdevdas 2fc57a1ae9 Merge pull request 'feat: add dynamic slug' (#40) from feat/dynamic-slug into dev
Reviewed-on: #40
2026-05-25 06:57:42 +00:00
Kailasdevdas d76011d301 fix: remove duplicate toasts 2026-05-25 12:19:09 +05:30
rishalkv 6d5e243e06 feat: add dynamic slug 2026-05-25 12:04:14 +05:30
kailasdevdas 9210621d67 Merge pull request 'fix: optional price fields' (#39) from fix/optional-pricing into dev
Reviewed-on: #39
2026-05-25 06:11:31 +00:00
Kailasdevdas cefaf3a850 fix: optional price fields 2026-05-25 11:37:51 +05:30
kailasdevdas 120ff12fef Merge pull request 'fix: add toggle action update controller' (#38) from fix/doc-update-controller into dev
Reviewed-on: #38
2026-05-22 11:40:52 +00:00
rishalkv 12d9f2a4cb fix: add toggle action update controller 2026-05-22 16:34:37 +05:30
kailasdevdas 5eecc5092d Merge pull request 'fix: use migrate deploy' (#37) from fix/prisma-migrate into dev
Reviewed-on: #37
2026-05-22 09:20:03 +00:00
Kailasdevdas f11c8ae8dc fix: use migrate deploy 2026-05-22 14:21:25 +05:30
kailasdevdas 558ab12e1f Merge pull request 'fix: bytescale type' (#36) from fix/bytescale-type into dev
Reviewed-on: #36
2026-05-21 09:12:43 +00:00
rishalkv 0f839c7f84 fix: bytescale type 2026-05-21 14:36:17 +05:30
kailasdevdas 9271ea9b38 Merge pull request 'feat:add seo and more about doctors' (#34) from feat/doc-seo-content-enhacement into dev
Reviewed-on: #34
2026-05-21 08:47:32 +00:00
rishalkv eb68d0acc4 Merge pull request 'fix:added validations for api' (#35) from fix/doc-validations into feat/doc-seo-content-enhacement
Reviewed-on: #35
2026-05-21 06:06:30 +00:00
rishalkv 667e15513c fix:added validations for api 2026-05-21 11:20:09 +05:30
rishalkv 2a786ef118 fix:unwanted query exec on update 2026-05-20 10:43:05 +05:30
rishalkv da6587c83d fix:editing doctor dept 2026-05-20 10:28:46 +05:30
rishalkv 5fea2a306d feat:add seo and more about doctores 2026-05-20 10:15:53 +05:30
kailasdevdas 08b9c2647e Merge pull request 'chore: add toast show validation errors' (#32) from fix/health-checkup-validation into dev
Reviewed-on: #32
2026-05-18 11:21:57 +00:00
Kailasdevdas 98194283df chore: add toast show validation errors 2026-05-18 16:41:38 +05:30
kailasdevdas 1320ce6fe6 Merge pull request 'chore: show required image dimension' (#31) from feat/healthcheckup-crud into dev
Reviewed-on: #31
2026-05-18 07:47:06 +00:00
Kailasdevdas 3dbbb2e77e chore: show required image dimension 2026-05-18 13:15:00 +05:30
kailasdevdas 5b1d626661 Merge pull request 'feat: health checkup CRUD apis' (#30) from feat/healthcheckup-crud into dev
Reviewed-on: #30
2026-05-18 06:26:27 +00:00
Kailasdevdas 098fe12fd7 feat: add image upload for health package 2026-05-18 11:55:55 +05:30
Kailasdevdas 852a25269a feat: add toast 2026-05-18 10:58:24 +05:30
Kailasdevdas d92e0538bd fix: make category slug optional 2026-05-18 10:58:14 +05:30
Kailasdevdas 8d60afdc49 feat: health checkup page 2026-05-15 17:58:25 +05:30
Kailasdevdas 9bc0bf406a feat: health checkup CRUD apis 2026-05-15 17:46:52 +05:30
129 changed files with 15448 additions and 13300 deletions
+1
View File
@@ -1 +1,2 @@
.env
node_modules
+14
View File
@@ -0,0 +1,14 @@
node_modules
dist
build
coverage
.next
out
*.log
backend/node_modules
backend/dist
frontend/node_modules
frontend/build
frontend/dist
+13
View File
@@ -0,0 +1,13 @@
{
"printWidth": 120,
"useTabs": true,
"tabWidth": 2,
"trailingComma": "es5",
"semi": true,
"singleQuote": true,
"bracketSpacing": true,
"arrowParens": "always",
"jsxSingleQuote": false,
"bracketSameLine": false,
"endOfLine": "lf"
}
+4
View File
@@ -0,0 +1,4 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
}
+3
View File
@@ -35,6 +35,7 @@ npm run create-user <username> <password> [role] # role defaults to "admin"
```
In Docker, create an admin via:
```bash
docker exec -it gg-backend-api-backend-1 node src/utils/createUser.js <name> <password> <role>
```
@@ -86,6 +87,7 @@ Note the natural-key relations: `Appointment.doctorId` references `Doctor.doctor
## Environment variables
`backend/.env`:
```
DATABASE_URL=postgresql://user:password@db:5432/mydb
PORT=5008
@@ -97,6 +99,7 @@ EMAIL_FROM=admin@example.com
```
`frontend/.env`:
```
VITE_API_URL=http://localhost:5008/api
```
+5 -5
View File
@@ -1,14 +1,14 @@
// This file was generated by Prisma, and assumes you have installed the following:
// npm install --save-dev prisma dotenv
import "dotenv/config";
import {defineConfig} from "prisma/config";
import 'dotenv/config';
import { defineConfig } from 'prisma/config';
export default defineConfig({
schema: "prisma/schema.prisma",
schema: 'prisma/schema.prisma',
migrations: {
path: "prisma/migrations",
path: 'prisma/migrations',
},
datasource: {
url: process.env["DATABASE_URL"],
url: process.env['DATABASE_URL'],
},
});
@@ -0,0 +1,63 @@
-- CreateTable
CREATE TABLE "HealthCheckCategory" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"description" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"sortOrder" INTEGER NOT NULL DEFAULT 1000,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "HealthCheckCategory_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "HealthPackage" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"description" TEXT,
"price" DECIMAL(10,2),
"discountedPrice" DECIMAL(10,2),
"inclusions" JSONB NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"isFeatured" BOOLEAN NOT NULL DEFAULT false,
"sortOrder" INTEGER NOT NULL DEFAULT 1000,
"categoryId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "HealthPackage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "HealthPackageInquiry" (
"id" SERIAL NOT NULL,
"fullName" TEXT NOT NULL,
"mobileNumber" TEXT NOT NULL,
"email" TEXT,
"preferredDate" TIMESTAMP(3),
"message" TEXT,
"packageId" INTEGER NOT NULL,
"status" TEXT NOT NULL DEFAULT 'pending',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "HealthPackageInquiry_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "HealthCheckCategory_name_key" ON "HealthCheckCategory"("name");
-- CreateIndex
CREATE UNIQUE INDEX "HealthCheckCategory_slug_key" ON "HealthCheckCategory"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "HealthPackage_slug_key" ON "HealthPackage"("slug");
-- AddForeignKey
ALTER TABLE "HealthPackage" ADD CONSTRAINT "HealthPackage_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "HealthCheckCategory"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "HealthPackageInquiry" ADD CONSTRAINT "HealthPackageInquiry_packageId_fkey" FOREIGN KEY ("packageId") REFERENCES "HealthPackage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "HealthPackageInquiry" ADD COLUMN "age" INTEGER,
ADD COLUMN "gender" TEXT;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "HealthPackage" ALTER COLUMN "inclusions" SET DEFAULT '{}';
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "HealthCheckCategory" ALTER COLUMN "slug" DROP NOT NULL;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "HealthPackage" ADD COLUMN "image" TEXT;
@@ -0,0 +1,50 @@
/*
Warnings:
- A unique constraint covering the columns `[seoId]` on the table `Doctor` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "Doctor" ADD COLUMN "professionalSummary" TEXT,
ADD COLUMN "seoId" INTEGER;
-- CreateTable
CREATE TABLE "DoctorSpecialization" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"doctorId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "DoctorSpecialization_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Seo" (
"id" SERIAL NOT NULL,
"seoTitle" TEXT,
"metaDescription" TEXT,
"focusKeyphrase" TEXT,
"slug" TEXT,
"tags" TEXT[],
"ogTitle" TEXT,
"ogDescription" TEXT,
"ogImage" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Seo_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Seo_slug_key" ON "Seo"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "Doctor_seoId_key" ON "Doctor"("seoId");
-- AddForeignKey
ALTER TABLE "Doctor" ADD CONSTRAINT "Doctor_seoId_fkey" FOREIGN KEY ("seoId") REFERENCES "Seo"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DoctorSpecialization" ADD CONSTRAINT "DoctorSpecialization_doctorId_fkey" FOREIGN KEY ("doctorId") REFERENCES "Doctor"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Doctor" ADD COLUMN "experience" INTEGER;
@@ -0,0 +1,14 @@
/*
Warnings:
- A unique constraint covering the columns `[seoId]` on the table `HealthPackage` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "HealthPackage" ADD COLUMN "seoId" INTEGER;
-- CreateIndex
CREATE UNIQUE INDEX "HealthPackage_seoId_key" ON "HealthPackage"("seoId");
-- AddForeignKey
ALTER TABLE "HealthPackage" ADD CONSTRAINT "HealthPackage_seoId_fkey" FOREIGN KEY ("seoId") REFERENCES "Seo"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+95 -1
View File
@@ -23,11 +23,15 @@ model Doctor {
name String
image String?
designation String?
experience Int?
workingStatus String?
qualification String?
isActive Boolean @default(true)
globalSortOrder Int @default(1000)
specializations DoctorSpecialization[]
professionalSummary String? @db.Text
seoId Int? @unique
seo Seo? @relation(fields: [seoId], references: [id])
departments DoctorDepartment[]
appointments Appointment[]
@@ -220,3 +224,93 @@ model NewsImage {
createdAt DateTime @default(now())
}
model HealthCheckCategory {
id Int @id @default(autoincrement())
name String @unique
slug String? @unique
description String?
isActive Boolean @default(true)
sortOrder Int @default(1000)
packages HealthPackage[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model HealthPackage {
id Int @id @default(autoincrement())
name String
slug String @unique
description String?
price Decimal? @db.Decimal(10, 2)
image String?
discountedPrice Decimal? @db.Decimal(10, 2)
inclusions Json @default("{}")
isActive Boolean @default(true)
isFeatured Boolean @default(false)
sortOrder Int @default(1000)
categoryId Int
category HealthCheckCategory @relation(fields: [categoryId], references: [id])
inquiries HealthPackageInquiry[]
seoId Int? @unique
seo Seo? @relation(fields: [seoId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model HealthPackageInquiry {
id Int @id @default(autoincrement())
fullName String
mobileNumber String
email String?
age Int?
gender String?
preferredDate DateTime?
message String?
packageId Int
healthPackage HealthPackage @relation(fields: [packageId], references: [id])
status String @default("pending")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model DoctorSpecialization {
id Int @id @default(autoincrement())
name String
description String? @db.Text
doctorId Int
doctor Doctor @relation(fields: [doctorId],references: [id],onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Seo {
id Int @id @default(autoincrement())
doctor Doctor?
healthPackage HealthPackage?
seoTitle String?
metaDescription String? @db.Text
focusKeyphrase String?
slug String? @unique
tags String[]
ogTitle String?
ogDescription String? @db.Text
ogImage String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
+39 -37
View File
@@ -1,60 +1,62 @@
import express from "express";
import dotenv from "dotenv";
import cors from "cors";
import express from 'express';
import dotenv from 'dotenv';
import cors from 'cors';
import departmentRoutes from "./routes/department.routes.js";
import authRoutes from "./routes/auth.routes.js";
import blogRoutes from "./routes/blog.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";
import emailConfigRoutes from "./routes/emailConfig.routes.js";
import newsMediaRoutes from "./routes/newsMedia.routes.js";
import importRoutes from "./routes/importRoutes.js";
import departmentRoutes from './routes/department.routes.js';
import authRoutes from './routes/auth.routes.js';
import blogRoutes from './routes/blog.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';
import emailConfigRoutes from './routes/emailConfig.routes.js';
import newsMediaRoutes from './routes/newsMedia.routes.js';
import importRoutes from './routes/importRoutes.js';
import healthCheckRoutes from './routes/healthCheck.route.js';
dotenv.config();
const app = express();
app.use(express.json({ limit: "50mb" }));
app.use(express.urlencoded({ limit: "50mb", extended: true }));
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ limit: '50mb', extended: true }));
const allowedOrigins = process.env.CORS_ALLOWED_ORIGINS
? process.env.CORS_ALLOWED_ORIGINS.split(" ")
: ["http://localhost:3001"];
? process.env.CORS_ALLOWED_ORIGINS.split(' ')
: ['http://localhost:3001'];
const corsOptions = {
origin: function (origin, callback) {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error("Not allowed by CORS"));
callback(new Error('Not allowed by CORS'));
}
},
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"],
allowedHeaders: "*",
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: '*',
};
app.use(cors(corsOptions));
app.use("/api/departments", departmentRoutes);
app.use("/api/auth", authRoutes);
app.use("/api/blogs", blogRoutes);
app.use("/uploads", express.static("uploads"));
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);
app.use("/api/email", emailConfigRoutes);
app.use("/api/newsMedia", newsMediaRoutes);
app.use("/api/import", importRoutes);
app.use('/api/departments', departmentRoutes);
app.use('/api/auth', authRoutes);
app.use('/api/blogs', blogRoutes);
app.use('/uploads', express.static('uploads'));
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);
app.use('/api/email', emailConfigRoutes);
app.use('/api/newsMedia', newsMediaRoutes);
app.use('/api/import', importRoutes);
app.use('/api/health-check', healthCheckRoutes);
const PORT = process.env.PORT || 5008;
app.listen(PORT, () => {
@@ -1,18 +1,17 @@
import prisma from "../prisma/client.js";
import { sendEmail } from "../utils/sendEmail.js";
import { getEmailsByType } from "../utils/getEmailByTypes.js";
import prisma from '../prisma/client.js';
import { sendEmail } from '../utils/sendEmail.js';
import { getEmailsByType } from '../utils/getEmailByTypes.js';
// CREATE ACADEMICS & RESEARCH
export const createAcademicsResearch = async (req, res) => {
try {
const { fullName, number, emailId, subject, courseName, message } =
req.body;
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",
message: 'Full name and number are required',
});
}
@@ -28,12 +27,12 @@ export const createAcademicsResearch = async (req, res) => {
});
try {
const emailList = await getEmailsByType("ACADEMICS");
const emailList = await getEmailsByType('ACADEMICS');
if (emailList && emailList.length > 0) {
await sendEmail({
to: emailList,
subject: "New Academics & Research Inquiry",
subject: 'New Academics & Research Inquiry',
html: `
<div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;">
@@ -63,15 +62,15 @@ export const createAcademicsResearch = async (req, res) => {
</tr>
<tr>
<td style="padding: 8px 0;"><b>Email:</b></td>
<td style="padding: 8px 0;">${emailId || "-"}</td>
<td style="padding: 8px 0;">${emailId || '-'}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Course:</b></td>
<td style="padding: 8px 0;">${courseName || "-"}</td>
<td style="padding: 8px 0;">${courseName || '-'}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Subject:</b></td>
<td style="padding: 8px 0;">${subject || "-"}</td>
<td style="padding: 8px 0;">${subject || '-'}</td>
</tr>
</table>
@@ -87,7 +86,7 @@ export const createAcademicsResearch = async (req, res) => {
word-break: break-word;
overflow-wrap: anywhere;
">
${message ? message.replace(/\n/g, "<br/>") : "-"}
${message ? message.replace(/\n/g, '<br/>') : '-'}
</div>
</div>
@@ -105,20 +104,20 @@ export const createAcademicsResearch = async (req, res) => {
});
}
} catch (err) {
console.error("Academics email failed:", err);
console.error('Academics email failed:', err);
}
res.status(200).json({
success: true,
status: 200,
data,
message: "Academics & Research added successfully",
message: 'Academics & Research added successfully',
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to add Academics & Research inquiry",
message: 'Failed to add Academics & Research inquiry',
});
}
};
@@ -129,7 +128,7 @@ export const getAcademicsResearch = async (req, res) => {
try {
const data = await prisma.academicsResearch.findMany({
orderBy: {
createdAt: "desc",
createdAt: 'desc',
},
});
@@ -140,7 +139,7 @@ export const getAcademicsResearch = async (req, res) => {
} catch (error) {
res.status(500).json({
success: false,
message: "Failed to fetch records",
message: 'Failed to fetch records',
});
}
};
@@ -160,7 +159,7 @@ export const getSingleAcademicsResearch = async (req, res) => {
if (!data) {
return res.status(404).json({
success: false,
message: "Record not found",
message: 'Record not found',
});
}
@@ -171,7 +170,7 @@ export const getSingleAcademicsResearch = async (req, res) => {
} catch (error) {
res.status(500).json({
success: false,
message: "Failed to fetch record",
message: 'Failed to fetch record',
});
}
};
@@ -190,12 +189,12 @@ export const deleteAcademicsResearch = async (req, res) => {
res.json({
success: true,
message: "Record deleted successfully",
message: 'Record deleted successfully',
});
} catch (error) {
res.status(500).json({
success: false,
message: "Failed to delete record",
message: 'Failed to delete record',
});
}
};
@@ -1,16 +1,15 @@
import prisma from "../prisma/client.js";
import { sendEmail } from "../utils/sendEmail.js";
import { getEmailsByType } from "../utils/getEmailByTypes.js";
import prisma from '../prisma/client.js';
import { sendEmail } from '../utils/sendEmail.js';
import { getEmailsByType } from '../utils/getEmailByTypes.js';
export const createAppointment = async (req, res) => {
try {
const { name, mobileNumber, email, message, date, doctorId, departmentId } =
req.body;
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",
message: 'Required fields missing',
});
}
@@ -31,12 +30,12 @@ export const createAppointment = async (req, res) => {
});
try {
const emailList = await getEmailsByType("APPOINTMENT");
const emailList = await getEmailsByType('APPOINTMENT');
if (emailList) {
await sendEmail({
to: emailList,
subject: "New Appointment Booked",
subject: 'New Appointment Booked',
html: `
<div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;">
@@ -66,7 +65,7 @@ export const createAppointment = async (req, res) => {
</tr>
<tr>
<td style="padding: 8px 0;"><b>Email:</b></td>
<td style="padding: 8px 0;">${email || "-"}</td>
<td style="padding: 8px 0;">${email || '-'}</td>
</tr>
</table>
@@ -75,19 +74,19 @@ export const createAppointment = async (req, res) => {
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0;"><b>Doctor:</b></td>
<td style="padding: 8px 0;">${appointment.doctor?.name || "-"}</td>
<td style="padding: 8px 0;">${appointment.doctor?.name || '-'}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Department:</b></td>
<td style="padding: 8px 0;">${appointment.department?.name || "-"}</td>
<td style="padding: 8px 0;">${appointment.department?.name || '-'}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Date:</b></td>
<td style="padding: 8px 0;">
${new Date(date).toLocaleDateString("en-GB", {
day: "2-digit",
month: "long",
year: "numeric",
${new Date(date).toLocaleDateString('en-GB', {
day: '2-digit',
month: 'long',
year: 'numeric',
})}
</td>
</tr>
@@ -105,7 +104,7 @@ export const createAppointment = async (req, res) => {
word-break: break-word;
overflow-wrap: anywhere;
">
${message ? message.replace(/\n/g, "<br/>") : "-"}
${message ? message.replace(/\n/g, '<br/>') : '-'}
</div>
</div>
@@ -123,19 +122,19 @@ export const createAppointment = async (req, res) => {
});
}
} catch (err) {
console.error("Email failed:", err);
console.error('Email failed:', err);
}
res.status(201).json({
success: true,
message: "Appointment booked successfully",
message: 'Appointment booked successfully',
data: appointment,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to create appointment",
message: 'Failed to create appointment',
});
}
};
@@ -152,11 +151,9 @@ export const getAppointments = async (req, res) => {
const where = {};
const hasSingleDate = date && date.trim() !== "";
const hasSingleDate = date && date.trim() !== '';
const hasRange =
(startDate && startDate.trim() !== "") ||
(endDate && endDate.trim() !== "");
const hasRange = (startDate && startDate.trim() !== '') || (endDate && endDate.trim() !== '');
if (hasSingleDate) {
const start = new Date(date);
@@ -174,14 +171,14 @@ export const getAppointments = async (req, res) => {
if (!hasSingleDate && hasRange) {
const dateFilter = {};
if (startDate && startDate.trim() !== "") {
if (startDate && startDate.trim() !== '') {
const start = new Date(startDate);
start.setHours(0, 0, 0, 0);
dateFilter.gte = start;
}
if (endDate && endDate.trim() !== "") {
if (endDate && endDate.trim() !== '') {
const end = new Date(endDate);
end.setHours(23, 59, 59, 999);
@@ -191,11 +188,11 @@ export const getAppointments = async (req, res) => {
where.date = dateFilter;
}
if (search && search.trim() !== "") {
if (search && search.trim() !== '') {
where.OR = [
{ name: { contains: search, mode: "insensitive" } },
{ name: { contains: search, mode: 'insensitive' } },
{ mobileNumber: { contains: search } },
{ email: { contains: search, mode: "insensitive" } },
{ email: { contains: search, mode: 'insensitive' } },
];
}
@@ -207,7 +204,7 @@ export const getAppointments = async (req, res) => {
department: true,
},
orderBy: {
createdAt: "desc",
createdAt: 'desc',
},
skip,
take: limit,
@@ -233,7 +230,7 @@ export const getAppointments = async (req, res) => {
res.status(500).json({
success: false,
message: "Failed to fetch appointments",
message: 'Failed to fetch appointments',
});
}
};
@@ -257,7 +254,7 @@ export const getAppointment = async (req, res) => {
if (!appointment) {
return res.status(404).json({
success: false,
message: "Appointment not found",
message: 'Appointment not found',
});
}
@@ -269,7 +266,7 @@ export const getAppointment = async (req, res) => {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to fetch appointment",
message: 'Failed to fetch appointment',
});
}
};
@@ -289,7 +286,7 @@ export const getAppointmentsByDoctor = async (req, res) => {
department: true,
},
orderBy: {
date: "asc",
date: 'asc',
},
});
@@ -301,7 +298,7 @@ export const getAppointmentsByDoctor = async (req, res) => {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to fetch doctor appointments",
message: 'Failed to fetch doctor appointments',
});
}
};
@@ -330,7 +327,7 @@ export const getAppointmentsByDepartment = async (req, res) => {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to fetch department appointments",
message: 'Failed to fetch department appointments',
});
}
};
@@ -354,14 +351,14 @@ export const updateAppointment = async (req, res) => {
res.status(200).json({
success: true,
message: "Appointment updated successfully",
message: 'Appointment updated successfully',
data: appointment,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to update appointment",
message: 'Failed to update appointment',
});
}
};
@@ -380,13 +377,13 @@ export const deleteAppointment = async (req, res) => {
res.status(200).json({
success: true,
message: "Appointment deleted successfully",
message: 'Appointment deleted successfully',
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to delete appointment",
message: 'Failed to delete appointment',
});
}
};
+10 -10
View File
@@ -1,6 +1,6 @@
import prisma from "../prisma/client.js";
import {generateToken} from "../utils/jwt.js";
import {hashPassword, comparePassword} from "../utils/password.js";
import prisma from '../prisma/client.js';
import { generateToken } from '../utils/jwt.js';
import { hashPassword, comparePassword } from '../utils/password.js';
/**
* REGISTER
@@ -10,7 +10,7 @@ export async function register(req, res) {
const { username, password, role } = req.body;
if (!username || !password) {
return res.status(400).json({error: "Username and password required"});
return res.status(400).json({ error: 'Username and password required' });
}
const existingUser = await prisma.user.findUnique({
@@ -18,7 +18,7 @@ export async function register(req, res) {
});
if (existingUser) {
return res.status(409).json({error: "Username already exists"});
return res.status(409).json({ error: 'Username already exists' });
}
const hashedPassword = await hashPassword(password);
@@ -27,12 +27,12 @@ export async function register(req, res) {
data: {
username,
password: hashedPassword,
role: role || "admin",
role: role || 'admin',
},
});
res.status(201).json({
message: "User registered successfully",
message: 'User registered successfully',
user: {
id: user.id,
username: user.username,
@@ -49,7 +49,7 @@ export async function login(req, res) {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({error: "Username and password required"});
return res.status(400).json({ error: 'Username and password required' });
}
const user = await prisma.user.findUnique({
@@ -57,13 +57,13 @@ export async function login(req, res) {
});
if (!user) {
return res.status(401).json({error: "Invalid credentials"});
return res.status(401).json({ error: 'Invalid credentials' });
}
const isValid = await comparePassword(password, user.password);
if (!isValid) {
return res.status(401).json({error: "Invalid credentials"});
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = generateToken({
+8 -8
View File
@@ -1,5 +1,5 @@
import prisma from "../prisma/client.js";
import slugify from "slugify";
import prisma from '../prisma/client.js';
import slugify from 'slugify';
/* CREATE BLOG */
@@ -20,7 +20,7 @@ export async function createBlog(req, res) {
res.json(blog);
} catch (error) {
res.status(500).json({error: "Blog creation failed"});
res.status(500).json({ error: 'Blog creation failed' });
}
}
@@ -30,7 +30,7 @@ export async function getBlogs(req, res) {
try {
const blogs = await prisma.blog.findMany({
where: { isActive: true },
orderBy: {createdAt: "desc"},
orderBy: { createdAt: 'desc' },
});
res.json(blogs);
@@ -44,7 +44,7 @@ export async function getBlogs(req, res) {
export async function getAllBlogs(req, res) {
try {
const blogs = await prisma.blog.findMany({
orderBy: {createdAt: "desc"},
orderBy: { createdAt: 'desc' },
});
res.json(blogs);
@@ -64,7 +64,7 @@ export async function getBlog(req, res) {
});
if (!blog) {
return res.status(404).json({error: "Blog not found"});
return res.status(404).json({ error: 'Blog not found' });
}
res.json(blog);
@@ -84,7 +84,7 @@ export async function getBlogForAdmin(req, res) {
});
if (!blog) {
return res.status(404).json({error: "Blog not found"});
return res.status(404).json({ error: 'Blog not found' });
}
res.json(blog);
@@ -125,7 +125,7 @@ export async function deleteBlog(req, res) {
where: { id },
});
res.json({message: "Blog deleted successfully"});
res.json({ message: 'Blog deleted successfully' });
} catch (error) {
res.status(500).json({ error: error.message });
}
+24 -25
View File
@@ -1,19 +1,18 @@
import prisma from "../prisma/client.js";
import prisma from '../prisma/client.js';
import { sendEmail } from "../utils/sendEmail.js";
import { getEmailsByType } from "../utils/getEmailByTypes.js";
import { sendEmail } from '../utils/sendEmail.js';
import { getEmailsByType } from '../utils/getEmailByTypes.js';
// CREATE CANDIDATE
export const createCandidate = async (req, res) => {
try {
const { fullName, mobile, email, subject, coverLetter, careerId } =
req.body;
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",
message: 'Required fields missing',
});
}
@@ -32,12 +31,12 @@ export const createCandidate = async (req, res) => {
});
try {
const emailList = await getEmailsByType("CANDIDATE");
const emailList = await getEmailsByType('CANDIDATE');
if (emailList && emailList.length > 0) {
await sendEmail({
to: emailList,
subject: "New Job Application Received",
subject: 'New Job Application Received',
html: `
<div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;">
@@ -76,15 +75,15 @@ export const createCandidate = async (req, res) => {
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0;"><b>Applied For:</b></td>
<td style="padding: 8px 0;">${candidate.career?.post || "-"}</td>
<td style="padding: 8px 0;">${candidate.career?.post || '-'}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Designation:</b></td>
<td style="padding: 8px 0;">${candidate.career?.designation || "-"}</td>
<td style="padding: 8px 0;">${candidate.career?.designation || '-'}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Subject:</b></td>
<td style="padding: 8px 0;">${subject || "-"}</td>
<td style="padding: 8px 0;">${subject || '-'}</td>
</tr>
</table>
@@ -100,7 +99,7 @@ export const createCandidate = async (req, res) => {
word-break: break-word;
overflow-wrap: anywhere;
">
${coverLetter ? coverLetter.replace(/\n/g, "<br/>") : "-"}
${coverLetter ? coverLetter.replace(/\n/g, '<br/>') : '-'}
</div>
</div>
@@ -118,19 +117,19 @@ export const createCandidate = async (req, res) => {
});
}
} catch (err) {
console.error("Candidate email failed:", err);
console.error('Candidate email failed:', err);
}
res.status(201).json({
success: true,
message: "Application submitted successfully",
message: 'Application submitted successfully',
data: candidate,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to create candidate",
message: 'Failed to create candidate',
});
}
};
@@ -144,7 +143,7 @@ export const getCandidates = async (req, res) => {
career: true,
},
orderBy: {
createdAt: "desc",
createdAt: 'desc',
},
});
@@ -156,7 +155,7 @@ export const getCandidates = async (req, res) => {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to fetch candidates",
message: 'Failed to fetch candidates',
});
}
};
@@ -179,7 +178,7 @@ export const getCandidate = async (req, res) => {
if (!candidate) {
return res.status(404).json({
success: false,
message: "Candidate not found",
message: 'Candidate not found',
});
}
@@ -191,7 +190,7 @@ export const getCandidate = async (req, res) => {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to fetch candidate",
message: 'Failed to fetch candidate',
});
}
};
@@ -210,7 +209,7 @@ export const getCandidatesByCareer = async (req, res) => {
career: true,
},
orderBy: {
createdAt: "desc",
createdAt: 'desc',
},
});
@@ -222,7 +221,7 @@ export const getCandidatesByCareer = async (req, res) => {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to fetch candidates",
message: 'Failed to fetch candidates',
});
}
};
@@ -242,14 +241,14 @@ export const updateCandidate = async (req, res) => {
res.status(200).json({
success: true,
message: "Candidate updated successfully",
message: 'Candidate updated successfully',
data: candidate,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to update candidate",
message: 'Failed to update candidate',
});
}
};
@@ -268,13 +267,13 @@ export const deleteCandidate = async (req, res) => {
res.status(200).json({
success: true,
message: "Candidate deleted successfully",
message: 'Candidate deleted successfully',
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to delete candidate",
message: 'Failed to delete candidate',
});
}
};
+12 -22
View File
@@ -1,4 +1,4 @@
import prisma from "../prisma/client.js";
import prisma from '../prisma/client.js';
// GET ALL CAREERS
@@ -7,8 +7,8 @@ export const getAllCareers = async (req, res) => {
const { admin } = req.query;
const careers = await prisma.career.findMany({
where: admin === "true" ? {} : { isActive: true },
orderBy: [{ sortOrder: "asc" }, { createdAt: "desc" }],
where: admin === 'true' ? {} : { isActive: true },
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }],
});
const response = careers.map((c) => ({
@@ -32,7 +32,7 @@ export const getAllCareers = async (req, res) => {
console.error(error);
return res.status(500).json({
success: false,
message: "Failed to fetch careers",
message: 'Failed to fetch careers',
});
}
};
@@ -41,22 +41,12 @@ export const getAllCareers = async (req, res) => {
export const createCareer = async (req, res) => {
try {
const {
post,
designation,
qualification,
experienceNeed,
email,
number,
status,
isActive,
sortOrder,
} = req.body;
const { post, designation, qualification, experienceNeed, email, number, status, isActive, sortOrder } = req.body;
if (!post || !designation) {
return res.status(400).json({
success: false,
message: "Post and designation are required",
message: 'Post and designation are required',
});
}
@@ -76,14 +66,14 @@ export const createCareer = async (req, res) => {
return res.status(201).json({
success: true,
message: "Career created successfully",
message: 'Career created successfully',
data: career,
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: "Failed to create career",
message: 'Failed to create career',
});
}
};
@@ -106,14 +96,14 @@ export const updateCareer = async (req, res) => {
return res.status(200).json({
success: true,
message: "Career updated successfully",
message: 'Career updated successfully',
data: career,
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: "Failed to update career",
message: 'Failed to update career',
});
}
};
@@ -130,13 +120,13 @@ export const deleteCareer = async (req, res) => {
return res.status(200).json({
success: true,
message: "Career deleted successfully",
message: 'Career deleted successfully',
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: "Failed to delete career",
message: 'Failed to delete career',
});
}
};
@@ -1,23 +1,23 @@
import prisma from "../prisma/client.js";
import prisma from '../prisma/client.js';
export const getAllDepartments = async (req, res) => {
try {
const { admin } = req.query;
const departments = await prisma.department.findMany({
where: admin === "true" ? {} : {isActive: true},
orderBy: [{sortOrder: "asc"}, {name: "asc"}],
where: admin === 'true' ? {} : { isActive: true },
orderBy: [{ sortOrder: 'asc' }, { name: 'asc' }],
});
const response = departments.map((dep) => ({
departmentId: dep.departmentId,
name: dep.name,
image: dep.image ?? "",
para1: dep.para1 ?? "",
para2: dep.para2 ?? "",
para3: dep.para3 ?? "",
facilities: dep.facilities ?? "",
services: dep.services ?? "",
image: dep.image ?? '',
para1: dep.para1 ?? '',
para2: dep.para2 ?? '',
para3: dep.para3 ?? '',
facilities: dep.facilities ?? '',
services: dep.services ?? '',
isActive: dep.isActive,
sortOrder: dep.sortOrder,
}));
@@ -30,7 +30,7 @@ export const getAllDepartments = async (req, res) => {
console.error(error);
return res.status(500).json({
success: false,
message: "Failed to fetch departments",
message: 'Failed to fetch departments',
});
}
};
@@ -42,7 +42,7 @@ export const getDepartmentByName = async (req, res) => {
if (!name) {
return res.status(400).json({
success: false,
message: "Department name is required",
message: 'Department name is required',
});
}
@@ -56,19 +56,19 @@ export const getDepartmentByName = async (req, res) => {
if (!department) {
return res.status(404).json({
success: false,
message: "Department not found or inactive",
message: 'Department not found or inactive',
});
}
const response = {
departmentId: department.departmentId,
name: department.name,
image: department.image ?? "",
para1: department.para1 ?? "",
para2: department.para2 ?? "",
para3: department.para3 ?? "",
facilities: department.facilities ?? "",
services: department.services ?? "",
image: department.image ?? '',
para1: department.para1 ?? '',
para2: department.para2 ?? '',
para3: department.para3 ?? '',
facilities: department.facilities ?? '',
services: department.services ?? '',
isActive: department.isActive,
sortOrder: department.sortOrder,
};
@@ -81,30 +81,17 @@ export const getDepartmentByName = async (req, res) => {
console.error(error);
return res.status(500).json({
success: false,
message: "Failed to fetch department",
message: 'Failed to fetch department',
});
}
};
export async function createDepartment(req, res) {
try {
const {
departmentId,
name,
image,
para1,
para2,
para3,
facilities,
services,
isActive,
sortOrder,
} = req.body;
const { departmentId, name, image, para1, para2, para3, facilities, services, isActive, sortOrder } = req.body;
if (!departmentId || !name) {
return res
.status(400)
.json({error: "departmentId and name are required"});
return res.status(400).json({ error: 'departmentId and name are required' });
}
const department = await prisma.department.create({
@@ -123,15 +110,15 @@ export async function createDepartment(req, res) {
});
res.status(201).json({
message: "Department created successfully",
message: 'Department created successfully',
data: department,
});
} catch (error) {
if (error.code === "P2002") {
return res.status(409).json({error: "Department already exists"});
if (error.code === 'P2002') {
return res.status(409).json({ error: 'Department already exists' });
}
console.error(error);
res.status(500).json({error: "Failed to create department"});
res.status(500).json({ error: 'Failed to create department' });
}
}
@@ -151,14 +138,14 @@ export const updateDepartment = async (req, res) => {
return res.status(200).json({
success: true,
message: "Department updated successfully",
message: 'Department updated successfully',
data: department,
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: "Failed to update department",
message: 'Failed to update department',
});
}
};
@@ -173,13 +160,13 @@ export const deleteDepartment = async (req, res) => {
return res.status(200).json({
success: true,
message: "Department deleted successfully",
message: 'Department deleted successfully',
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: "Failed to delete department",
message: 'Failed to delete department',
});
}
};
+293 -65
View File
@@ -1,4 +1,4 @@
import prisma from "../prisma/client.js";
import prisma from '../prisma/client.js';
// get doctors
@@ -7,28 +7,52 @@ export const getAllDoctors = async (req, res) => {
const { admin } = req.query;
const doctors = await prisma.doctor.findMany({
where: admin === "true" ? {} : {isActive: true},
where: admin === 'true' ? {} : { isActive: true },
include: {
seo: true,
departments: {
include: {
department: true,
timing: true,
},
},
specializations: {
orderBy: {
createdAt: 'asc',
},
orderBy: [{globalSortOrder: "asc"}, {name: "asc"}],
},
},
orderBy: [{ globalSortOrder: 'asc' }, { name: 'asc' }],
});
const formatted = doctors.map((doc, index) => ({
SL_NO: String(index + 1),
doctorId: doc.doctorId,
name: doc.name,
image: doc.image ?? "",
image: doc.image ?? '',
designation: doc.designation,
workingStatus: doc.workingStatus,
qualification: doc.qualification,
isActive: doc.isActive,
experience: doc.experience,
professionalSummary: doc.professionalSummary,
globalSortOrder: doc.globalSortOrder,
specializations: doc.specializations.map((item) => ({
id: item.id,
name: item.name,
description: item.description,
})),
seo: {
seoTitle: doc.seo?.seoTitle ?? '',
metaDescription: doc.seo?.metaDescription ?? '',
focusKeyphrase: doc.seo?.focusKeyphrase ?? '',
slug: doc.seo?.slug ?? '',
tags: doc.seo?.tags ?? [],
ogTitle: doc.seo?.ogTitle ?? '',
ogDescription: doc.seo?.ogDescription ?? '',
ogImage: doc.seo?.ogImage ?? '',
},
departments: doc.departments.map((d) => {
const t = d.timing || {};
const timingArray = [
@@ -45,7 +69,7 @@ export const getAllDoctors = async (req, res) => {
return {
departmentId: d.department.departmentId,
departmentName: d.department.name,
timing: timingArray.join(" & "),
timing: timingArray.join(' & '),
deptSortOrder: d.sortOrder,
};
}),
@@ -59,7 +83,7 @@ export const getAllDoctors = async (req, res) => {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to fetch doctors",
message: 'Failed to fetch doctors',
});
}
};
@@ -73,6 +97,8 @@ export const getDoctorByDoctorId = async (req, res) => {
const doctor = await prisma.doctor.findUnique({
where: { doctorId },
include: {
seo: true,
specializations: true,
departments: {
include: {
department: true,
@@ -85,17 +111,35 @@ export const getDoctorByDoctorId = async (req, res) => {
if (!doctor) {
return res.status(404).json({
success: false,
message: "Doctor not found",
message: 'Doctor not found',
});
}
const response = {
doctorId: doctor.doctorId,
name: doctor.name,
image: doctor.image ?? "",
image: doctor.image ?? '',
designation: doctor.designation,
workingStatus: doctor.workingStatus,
qualification: doctor.qualification,
experience: doctor.experience,
professionalSummary: doctor.professionalSummary,
seo: {
seoTitle: doctor.seo?.seoTitle ?? '',
metaDescription: doctor.seo?.metaDescription ?? '',
focusKeyphrase: doctor.seo?.focusKeyphrase ?? '',
slug: doctor.seo?.slug ?? '',
tags: doctor.seo?.tags ?? [],
ogTitle: doctor.seo?.ogTitle ?? '',
ogDescription: doctor.seo?.ogDescription ?? '',
ogImage: doctor.seo?.ogImage ?? '',
},
specializations:
doctor.specializations?.map((item) => ({
id: item.id,
name: item.name,
description: item.description,
})) ?? [],
departments: doctor.departments.map((d) => ({
departmentId: d.department.departmentId,
departmentName: d.department.name,
@@ -111,7 +155,7 @@ export const getDoctorByDoctorId = async (req, res) => {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to fetch doctor",
message: 'Failed to fetch doctor',
});
}
};
@@ -124,7 +168,7 @@ export const getDoctorsByDepartmentId = async (req, res) => {
if (!Department_ID) {
return res.status(400).json({
success: false,
message: "Department_ID is required",
message: 'Department_ID is required',
});
}
@@ -135,7 +179,7 @@ export const getDoctorsByDepartmentId = async (req, res) => {
if (!department) {
return res.status(404).json({
success: false,
message: "Department not found",
message: 'Department not found',
});
}
@@ -145,17 +189,26 @@ export const getDoctorsByDepartmentId = async (req, res) => {
doctor: { isActive: true },
},
include: {
doctor: true,
doctor: {
include: {
seo: {
select: {
slug: true,
},
orderBy: {sortOrder: "asc"},
},
},
},
},
orderBy: { sortOrder: 'asc' },
});
const result = doctorsInDept.map((d) => ({
GG_ID: d.doctor.doctorId,
Name: d.doctor.name,
image: d.doctor.image ?? "",
image: d.doctor.image ?? '',
designation: d.doctor.designation,
hierarchyOrder: d.sortOrder,
slug: d.doctor.seo?.slug ?? '',
}));
res.status(200).json({
@@ -166,7 +219,7 @@ export const getDoctorsByDepartmentId = async (req, res) => {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to fetch doctors",
message: 'Failed to fetch doctors',
});
}
};
@@ -184,7 +237,48 @@ export const createDoctor = async (req, res) => {
isActive,
globalSortOrder,
departments,
experience,
professionalSummary,
seoTitle,
metaDescription,
focusKeyphrase,
slug,
tags,
specializations,
ogTitle,
ogDescription,
ogImage,
} = req.body;
const messages = [];
if (!doctorId) messages.push('Doctor ID is required');
if (!name?.trim()) messages.push('Doctor name is required');
if (!designation?.trim()) messages.push('Designation is required');
if (!qualification?.trim()) messages.push('Qualification is required');
if (!departments || departments.length === 0) {
messages.push('At least one department is required');
}
if (messages.length > 0) {
return res.status(400).json({
success: false,
message: messages.join(', '),
});
}
const seo = await prisma.seo.create({
data: {
seoTitle,
metaDescription,
focusKeyphrase,
slug: slug ? slug : null,
tags: tags || [],
// Open Graph
ogTitle,
ogDescription,
ogImage,
},
});
const doctor = await prisma.doctor.create({
data: {
@@ -194,9 +288,11 @@ export const createDoctor = async (req, res) => {
designation,
workingStatus,
qualification,
experience: experience ? Number(experience) : null,
professionalSummary,
seoId: seo.id,
isActive: isActive !== undefined ? isActive : true,
globalSortOrder:
globalSortOrder !== undefined ? Number(globalSortOrder) : 0,
globalSortOrder: globalSortOrder !== undefined ? Number(globalSortOrder) : 0,
},
});
@@ -224,16 +320,27 @@ export const createDoctor = async (req, res) => {
});
}
}
if (specializations?.length) {
await prisma.doctorSpecialization.createMany({
data: specializations
.filter((item) => item.name?.trim())
.map((item) => ({
name: item.name.trim(),
description: item.description?.trim() || null,
doctorId: doctor.id,
})),
});
}
res.status(201).json({
success: true,
message: "Doctor created successfully",
message: 'Doctor created successfully',
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to create doctor",
message: 'Failed to create doctor',
});
}
};
@@ -241,7 +348,7 @@ export const createDoctor = async (req, res) => {
//update doctors
export const updateDoctor = async (req, res) => {
try {
const {doctorId} = req.params;
const { doctorId, action } = req.params;
const {
name,
designation,
@@ -251,13 +358,57 @@ export const updateDoctor = async (req, res) => {
isActive,
globalSortOrder,
departments,
experience,
professionalSummary,
seoTitle,
metaDescription,
ogTitle,
ogDescription,
focusKeyphrase,
slug,
tags,
ogImage,
specializations,
} = req.body;
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 not found"});
if (!doctor) return res.status(404).json({ success: false, message: 'Doctor not found' });
if (action === 'toggleStatus') {
await prisma.doctor.update({
where: { id: doctor.id },
data: {
isActive: !doctor.isActive,
},
});
return res.status(200).json({
success: true,
message: `Doctor has been ${doctor.isActive ? 'deactivated' : 'activated'} successfully`,
});
}
const messages = [];
if (!doctorId) messages.push('Doctor ID is required');
if (!name?.trim()) messages.push('Doctor name is required');
if (!qualification?.trim()) messages.push('Qualification is required');
if (!designation?.trim()) messages.push('Designation is required');
if (!departments || departments.length === 0) {
messages.push('At least one department is required');
}
if (messages.length > 0) {
return res.status(400).json({
success: false,
message: messages.join(', '),
});
}
await prisma.doctor.update({
where: { id: doctor.id },
@@ -268,60 +419,137 @@ export const updateDoctor = async (req, res) => {
workingStatus,
qualification,
isActive,
globalSortOrder:
globalSortOrder !== undefined ? Number(globalSortOrder) : undefined,
experience: experience ? Number(experience) : null,
professionalSummary,
globalSortOrder: globalSortOrder !== undefined ? Number(globalSortOrder) : undefined,
},
});
const hasTimingData = departments?.some(
(dep) => dep.timing && Object.keys(dep.timing).length > 0,
);
if (departments && Array.isArray(departments) && hasTimingData) {
const oldRelations = await prisma.doctorDepartment.findMany({
where: {doctorId: doctor.id},
if (doctor.seoId) {
await prisma.seo.update({
where: {
id: doctor.seoId,
},
data: {
seoTitle,
metaDescription,
ogTitle,
ogDescription,
ogImage,
focusKeyphrase,
slug: slug ? slug : null,
tags: tags || [],
},
});
} else {
const seo = await prisma.seo.create({
data: {
ogImage,
metaDescription,
seoTitle,
ogDescription,
ogTitle,
focusKeyphrase,
slug: slug ? slug : null,
tags: tags || [],
},
});
for (const rel of oldRelations) {
await prisma.doctorTiming.deleteMany({
where: {doctorDepartmentId: rel.id},
await prisma.doctor.update({
where: {
id: doctor.id,
},
data: {
seoId: seo.id,
},
});
}
// Update Departments & Timings
if (Array.isArray(departments)) {
const oldRelations = await prisma.doctorDepartment.findMany({
where: {
doctorId: doctor.id,
},
include: {
timing: true,
},
});
// Delete old timings
for (const rel of oldRelations) {
if (rel.timing) {
await prisma.doctorTiming.deleteMany({
where: {
doctorDepartmentId: rel.id,
},
});
}
}
// Delete old departments
await prisma.doctorDepartment.deleteMany({
where: {doctorId: doctor.id},
where: {
doctorId: doctor.id,
},
});
// Recreate departments + timings
for (const dep of departments) {
const targetDept = await prisma.department.findUnique({
where: {departmentId: dep.departmentId},
const department = await prisma.department.findUnique({
where: {
departmentId: dep.departmentId,
},
});
if (!targetDept) continue;
const newDD = await prisma.doctorDepartment.create({
if (!department) continue;
const doctorDepartment = await prisma.doctorDepartment.create({
data: {
doctorId: doctor.id,
departmentId: targetDept.id,
departmentId: department.id,
sortOrder: dep.sortOrder !== undefined ? Number(dep.sortOrder) : 0,
},
});
if (dep.timing) {
const {id, doctorDepartmentId, createdAt, updatedAt, ...cleanTiming} =
dep.timing;
if (dep.timing && Object.keys(dep.timing).length > 0) {
const { id, doctorDepartmentId, createdAt, updatedAt, ...cleanTiming } = dep.timing;
await prisma.doctorTiming.create({
data: {doctorDepartmentId: newDD.id, ...cleanTiming},
data: {
doctorDepartmentId: doctorDepartment.id,
...cleanTiming,
},
});
}
}
}
res
.status(200)
.json({success: true, message: "Doctor updated successfully"});
// Update Specializations
if (Array.isArray(specializations)) {
await prisma.doctorSpecialization.deleteMany({
where: {
doctorId: doctor.id,
},
});
if (specializations.length) {
await prisma.doctorSpecialization.createMany({
data: specializations
.filter((item) => item.name?.trim())
.map((item) => ({
name: item.name.trim(),
description: item.description?.trim() || null,
doctorId: doctor.id,
})),
});
}
}
res.status(200).json({ success: true, message: 'Doctor updated successfully' });
} catch (error) {
console.error("Update Error:", error);
res.status(500).json({success: false, message: "Failed to update doctor"});
console.error('Update Error:', error);
res.status(500).json({ success: false, message: 'Failed to update doctor' });
}
};
//delete doctor
@@ -337,7 +565,7 @@ export const deleteDoctor = async (req, res) => {
if (!doctor) {
return res.status(404).json({
success: false,
message: "Doctor not found",
message: 'Doctor not found',
});
}
@@ -361,13 +589,13 @@ export const deleteDoctor = async (req, res) => {
res.status(200).json({
success: true,
message: "Doctor deleted successfully",
message: 'Doctor deleted successfully',
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to delete doctor",
message: 'Failed to delete doctor',
});
}
};
@@ -392,14 +620,14 @@ export const getDoctorTimings = async (req, res) => {
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 || "",
Monday: timing.monday || '',
Tuesday: timing.tuesday || '',
Wednesday: timing.wednesday || '',
Thursday: timing.thursday || '',
Friday: timing.friday || '',
Saturday: timing.saturday || '',
Sunday: timing.sunday || '',
Additional: timing.additional || '',
};
});
@@ -411,7 +639,7 @@ export const getDoctorTimings = async (req, res) => {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to fetch doctor timings",
message: 'Failed to fetch doctor timings',
});
}
};
@@ -436,7 +664,7 @@ export const getDoctorTimingById = async (req, res) => {
if (!doctor) {
return res.status(404).json({
success: false,
message: "Doctor not found",
message: 'Doctor not found',
});
}
@@ -459,7 +687,7 @@ export const getDoctorTimingById = async (req, res) => {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to fetch doctor timing",
message: 'Failed to fetch doctor timing',
});
}
};
@@ -1,4 +1,4 @@
import prisma from "../prisma/client.js";
import prisma from '../prisma/client.js';
// CREATE
export const createEmailConfig = async (req, res) => {
@@ -8,7 +8,7 @@ export const createEmailConfig = async (req, res) => {
if (!name || !email || !type) {
return res.status(400).json({
success: false,
message: "Name, Email and Type are required",
message: 'Name, Email and Type are required',
});
}
@@ -23,14 +23,14 @@ export const createEmailConfig = async (req, res) => {
res.status(201).json({
success: true,
message: "Email config created",
message: 'Email config created',
data: newEmail,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to create email config",
message: 'Failed to create email config',
});
}
};
@@ -40,7 +40,7 @@ export const getEmailConfigs = async (req, res) => {
try {
const emails = await prisma.emailConfig.findMany({
orderBy: {
createdAt: "desc",
createdAt: 'desc',
},
});
@@ -52,7 +52,7 @@ export const getEmailConfigs = async (req, res) => {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to fetch email configs",
message: 'Failed to fetch email configs',
});
}
};
@@ -71,7 +71,7 @@ export const getEmailConfig = async (req, res) => {
if (!email) {
return res.status(404).json({
success: false,
message: "Email config not found",
message: 'Email config not found',
});
}
@@ -83,7 +83,7 @@ export const getEmailConfig = async (req, res) => {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to fetch email config",
message: 'Failed to fetch email config',
});
}
};
@@ -102,14 +102,14 @@ export const updateEmailConfig = async (req, res) => {
res.status(200).json({
success: true,
message: "Email config updated",
message: 'Email config updated',
data: updated,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to update email config",
message: 'Failed to update email config',
});
}
};
@@ -127,13 +127,13 @@ export const deleteEmailConfig = async (req, res) => {
res.status(200).json({
success: true,
message: "Email config deleted",
message: 'Email config deleted',
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to delete email config",
message: 'Failed to delete email config',
});
}
};
@@ -0,0 +1,488 @@
import prisma from '../prisma/client.js';
import { sendEmail } from '../utils/sendEmail.js';
import { getEmailsByType } from '../utils/getEmailByTypes.js';
export const getAllCategories = async (req, res) => {
try {
const { admin } = req.query;
const categories = await prisma.healthCheckCategory.findMany({
where: admin === 'true' ? {} : { isActive: true },
orderBy: { sortOrder: 'asc' },
include: {
_count: { select: { packages: true } },
},
});
return res.status(200).json({ success: true, data: categories });
} catch (error) {
return res.status(500).json({ success: false, message: 'Failed to fetch categories' });
}
};
export const createCategory = async (req, res) => {
try {
const { name, slug, description, isActive, sortOrder } = req.body;
const category = await prisma.healthCheckCategory.create({
data: {
name,
slug: slug || null,
description,
isActive: isActive ?? true,
sortOrder: sortOrder ? Number(sortOrder) : 1000,
},
});
return res.status(201).json({ success: true, message: 'Category created', data: category });
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Failed to create category' });
}
};
export const updateCategory = async (req, res) => {
try {
const { id } = req.params;
const data = { ...req.body };
delete data.id;
delete data._count;
delete data.createdAt;
delete data.updatedAt;
if (data.sortOrder !== undefined) data.sortOrder = Number(data.sortOrder);
if (data.slug === '') data.slug = null;
const updatedCategory = await prisma.$transaction(async (tx) => {
const category = await tx.healthCheckCategory.update({
where: { id: Number(id) },
data,
});
if (data.isActive === false) {
await tx.healthPackage.updateMany({
where: { categoryId: Number(id) },
data: { isActive: false },
});
}
return category;
});
return res.status(200).json({
success: true,
message: 'Category updated',
data: updatedCategory,
});
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Failed to update category' });
}
};
export const deleteCategory = async (req, res) => {
try {
const { id } = req.params;
await prisma.healthCheckCategory.delete({
where: { id: Number(id) },
});
return res.status(200).json({ success: true, message: 'Category deleted successfully' });
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: 'Failed to delete category. Ensure no packages are linked to it.',
});
}
};
export const getAllPackages = async (req, res) => {
try {
const { admin, categorySlug } = req.query;
const packages = await prisma.healthPackage.findMany({
where: {
AND: [admin === 'true' ? {} : { isActive: true }, categorySlug ? { category: { slug: categorySlug } } : {}],
},
include: {
category: true,
seo: true,
},
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }],
});
return res.status(200).json({ success: true, data: packages });
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Failed to fetch packages' });
}
};
export const createPackage = async (req, res) => {
try {
const {
name,
slug,
description,
price,
image,
discountedPrice,
inclusions,
categoryId,
isActive,
isFeatured,
sortOrder,
seo,
} = req.body;
const healthPackage = await prisma.healthPackage.create({
data: {
name,
slug,
description,
price,
image,
discountedPrice,
inclusions,
categoryId: Number(categoryId),
isActive: isActive ?? true,
isFeatured: isFeatured ?? false,
sortOrder: sortOrder ? Number(sortOrder) : 1000,
...(seo && {
seo: {
create: {
seoTitle: seo.seoTitle,
metaDescription: seo.metaDescription,
focusKeyphrase: seo.focusKeyphrase,
slug: slug,
tags: seo.tags || [],
ogTitle: seo.ogTitle,
ogDescription: seo.ogDescription,
ogImage: seo.ogImage,
},
},
}),
},
include: {
category: true,
seo: true,
},
});
return res.status(201).json({ success: true, message: 'Package created', data: healthPackage });
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Failed to create package' });
}
};
export const updatePackage = async (req, res) => {
try {
const { id } = req.params;
const data = { ...req.body };
delete data.id;
delete data.category;
delete data.createdAt;
delete data.updatedAt;
delete data.seoId;
if (data.categoryId) data.categoryId = Number(data.categoryId);
if (data.sortOrder) data.sortOrder = Number(data.sortOrder);
const existingPackage = await prisma.healthPackage.findUnique({
where: { id: Number(id) },
select: { slug: true },
});
const seoSlug = data.slug || existingPackage.slug;
const updated = await prisma.healthPackage.update({
where: { id: Number(id) },
data: {
...data,
seo: data.seo
? {
upsert: {
create: {
seoTitle: data.seo.seoTitle,
metaDescription: data.seo.metaDescription,
focusKeyphrase: data.seo.focusKeyphrase,
slug: seoSlug,
tags: data.seo.tags || [],
ogTitle: data.seo.ogTitle,
ogDescription: data.seo.ogDescription,
ogImage: data.seo.ogImage,
},
update: {
seoTitle: data.seo.seoTitle,
metaDescription: data.seo.metaDescription,
focusKeyphrase: data.seo.focusKeyphrase,
slug: seoSlug,
tags: data.seo.tags || [],
ogTitle: data.seo.ogTitle,
ogDescription: data.seo.ogDescription,
ogImage: data.seo.ogImage,
},
},
}
: undefined,
},
include: {
category: true,
seo: true,
},
});
return res.status(200).json({ success: true, message: 'Package updated', data: updated });
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Update failed' });
}
};
export const deletePackage = async (req, res) => {
try {
const { id } = req.params;
await prisma.healthPackage.delete({
where: { id: Number(id) },
});
return res.status(200).json({
success: true,
message: 'Package deleted',
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: 'Delete failed',
});
}
};
export const createPackageInquiry = async (req, res) => {
try {
const { fullName, mobileNumber, email, age, gender, preferredDate, packageId, message } = req.body;
const inquiry = await prisma.healthPackageInquiry.create({
data: {
fullName,
mobileNumber,
email,
age: age ? Number(age) : null,
gender,
preferredDate: preferredDate ? new Date(preferredDate) : null,
message,
packageId: Number(packageId),
},
include: {
healthPackage: true,
},
});
try {
const emailList = await getEmailsByType('HCINQUIRY');
if (emailList) {
await sendEmail({
to: emailList,
subject: 'New Health Checkup Package Inquiry',
html: `
<div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;">
<div style="max-width: 600px; margin: auto; background: #ffffff; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 10px rgba(0,0,0,0.05);">
<!-- Header -->
<div style="background-color: #0d6efd; color: #ffffff; padding: 20px;">
<h2 style="margin: 0;">GG Hospital</h2>
<p style="margin: 5px 0 0; font-size: 14px;">
New Health Checkup Package Inquiry
</p>
</div>
<!-- Body -->
<div style="padding: 20px; color: #333;">
<h3 style="margin-top: 0;">Inquirer Details</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; width: 35%;"><b>Name:</b></td>
<td style="padding: 8px 0;">${fullName}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Phone:</b></td>
<td style="padding: 8px 0;">${mobileNumber}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Email:</b></td>
<td style="padding: 8px 0;">${email || '-'}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Age:</b></td>
<td style="padding: 8px 0;">${age || '-'}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Gender:</b></td>
<td style="padding: 8px 0;">${gender || '-'}</td>
</tr>
</table>
<h3 style="margin-top: 20px;">Package Details</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; width: 35%;"><b>Package Name:</b></td>
<td style="padding: 8px 0;">${inquiry.healthPackage?.name || 'Unknown Package'}</td>
</tr>
<tr>
<td style="padding: 8px 0;"><b>Preferred Date:</b></td>
<td style="padding: 8px 0;">
${
preferredDate
? new Date(preferredDate).toLocaleDateString('en-GB', {
day: '2-digit',
month: 'long',
year: 'numeric',
})
: 'Not specified'
}
</td>
</tr>
</table>
<!-- Message Box -->
<div style="margin-top: 20px;">
<h3>Message</h3>
<div style="
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: anywhere;
">
${message ? message.replace(/\n/g, '<br/>') : '-'}
</div>
</div>
</div>
<!-- Footer -->
<div style="background: #f1f1f1; padding: 15px; text-align: center; font-size: 12px; color: #666;">
This inquiry was submitted via the GG Hospital website.
</div>
</div>
</div>
`,
});
}
} catch (err) {
console.error('Email failed:', err);
}
return res.status(201).json({
success: true,
message: 'Booking inquiry sent successfully',
data: inquiry,
});
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Failed to submit inquiry' });
}
};
export const getPackageBySlug = async (req, res) => {
try {
const { slug } = req.params;
const healthPackage = await prisma.healthPackage.findFirst({
where: { slug, isActive: true },
include: {
category: true,
seo: true,
},
});
if (!healthPackage) {
return res.status(404).json({ success: false, message: 'Package not found' });
}
return res.status(200).json({
success: true,
data: healthPackage,
});
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Failed to fetch package' });
}
};
export const getAllInquiries = async (req, res) => {
try {
const { page = 1, limit = 10, filterDate, startDate, endDate } = req.query;
const queryPage = parseInt(page);
const queryLimit = parseInt(limit);
const skip = (queryPage - 1) * queryLimit;
let where = {};
if (filterDate) {
where.preferredDate = {
gte: new Date(`${filterDate}T00:00:00.000Z`),
lte: new Date(`${filterDate}T23:59:59.999Z`),
};
} else if (startDate || endDate) {
where.preferredDate = {};
if (startDate) {
where.preferredDate.gte = new Date(`${startDate}T00:00:00.000Z`);
}
if (endDate) {
where.preferredDate.lte = new Date(`${endDate}T23:59:59.999Z`);
}
}
const [total, inquiries] = await prisma.$transaction([
prisma.healthPackageInquiry.count({ where }),
prisma.healthPackageInquiry.findMany({
where,
skip,
take: queryLimit,
include: {
healthPackage: {
include: {
category: true,
},
},
},
orderBy: { createdAt: 'desc' },
}),
]);
return res.status(200).json({
success: true,
data: inquiries,
pagination: {
total,
page: queryPage,
limit: queryLimit,
totalPages: Math.ceil(total / queryLimit),
},
});
} catch (error) {
console.error(error);
return res.status(500).json({ success: false, message: 'Failed to fetch inquiries' });
}
};
+23 -35
View File
@@ -1,22 +1,12 @@
import { PrismaClient } from "@prisma/client";
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export const bulkImportExcelData = async (req, res) => {
try {
const {
departments,
doctors,
timings,
careers,
inquiries,
academics,
appointments,
candidates,
news,
} = req.body;
const { departments, doctors, timings, careers, inquiries, academics, appointments, candidates, news } = req.body;
console.log("🚀 Starting Robust Data Import...");
console.log('🚀 Starting Robust Data Import...');
// 1. DEPARTMENTS
if (departments) {
@@ -54,14 +44,14 @@ export const bulkImportExcelData = async (req, res) => {
update: {
name: row.Name?.toString(),
designation: row.Designation?.toString() || null,
workingStatus: row["Working Status"]?.toString() || null,
workingStatus: row['Working Status']?.toString() || null,
qualification: row.Qualification?.toString() || null,
},
create: {
doctorId: row.GG_ID.toString(),
name: row.Name?.toString(),
designation: row.Designation?.toString() || null,
workingStatus: row["Working Status"]?.toString() || null,
workingStatus: row['Working Status']?.toString() || null,
qualification: row.Qualification?.toString() || null,
},
});
@@ -100,8 +90,8 @@ export const bulkImportExcelData = async (req, res) => {
if (doctor && doctor.departments.length > 0) {
const doctorDeptId = doctor.departments[0].id;
const rawAdd = row.Additional?.toString() || "";
const rawMon = row.Monday?.toString() || "";
const rawAdd = row.Additional?.toString() || '';
const rawMon = row.Monday?.toString() || '';
const isAppt = (val) => /appointment|basis|on call/i.test(val);
let finalAdd = rawAdd;
@@ -147,7 +137,7 @@ export const bulkImportExcelData = async (req, res) => {
experienceNeed: row.ExperienceNeed?.toString() || null,
email: row.HiringEmail?.toString() || null,
number: row.Number?.toString() || null,
status: row.Status?.toString() || "new",
status: row.Status?.toString() || 'new',
};
if (cId) {
await prisma.career.upsert({
@@ -168,7 +158,7 @@ export const bulkImportExcelData = async (req, res) => {
await prisma.inquiry.create({
data: {
fullName: row.FullName.toString(),
number: row.Number?.toString() || "",
number: row.Number?.toString() || '',
emailId: row.EmailId?.toString() || null,
subject: row.Subject?.toString() || null,
message: row.Message?.toString() || null,
@@ -185,10 +175,10 @@ export const bulkImportExcelData = async (req, res) => {
await prisma.academicsResearch.create({
data: {
fullName: row.FullName.toString(),
number: row.Number?.toString() || "",
number: row.Number?.toString() || '',
emailId: row.EmailId?.toString() || null,
subject: row.Subject?.toString() || null, // Force String
courseName: row["Course Name"]?.toString() || null,
courseName: row['Course Name']?.toString() || null,
message: row.Message?.toString() || null,
createdAt: row.Date ? new Date(row.Date) : new Date(),
},
@@ -201,7 +191,7 @@ export const bulkImportExcelData = async (req, res) => {
for (const row of appointments) {
if (!row.FullName) continue;
const doctorName = row.Doctor?.toString();
const departmentName = row["Department Id"]?.toString();
const departmentName = row['Department Id']?.toString();
const doctor = await prisma.doctor.findFirst({
where: { name: doctorName },
@@ -214,11 +204,11 @@ export const bulkImportExcelData = async (req, res) => {
if (!value) return new Date();
// Excel numeric date
if (typeof value === "number") {
if (typeof value === 'number') {
return new Date((value - 25569) * 86400 * 1000);
}
if (typeof value === "string") {
if (typeof value === 'string') {
const v = value.trim();
// Handle DD/MM/YYYY
@@ -238,15 +228,15 @@ export const bulkImportExcelData = async (req, res) => {
}
}
console.warn("⚠️ Invalid date, using current date:", value);
console.warn('⚠️ Invalid date, using current date:', value);
return new Date();
};
if (doctor && department) {
await prisma.appointment.create({
data: {
name: row.FullName.toString(),
mobileNumber: row.Number?.toString() || "",
email: row["Email Id"]?.toString() || null,
mobileNumber: row.Number?.toString() || '',
email: row['Email Id']?.toString() || null,
message: row.Message?.toString() || null,
date: parseDate(row.Date),
doctorId: doctor.doctorId,
@@ -265,10 +255,10 @@ export const bulkImportExcelData = async (req, res) => {
.create({
data: {
fullName: row.FullName.toString(),
mobile: row.Number?.toString() || "",
email: row.EmailId?.toString() || "",
subject: row.Subject?.toString() || "",
coverLetter: row["Cover Letter"]?.toString() || "",
mobile: row.Number?.toString() || '',
email: row.EmailId?.toString() || '',
subject: row.Subject?.toString() || '',
coverLetter: row['Cover Letter']?.toString() || '',
careerId: parseInt(row.CareerId),
createdAt: row.Date ? new Date(row.Date) : new Date(),
},
@@ -294,11 +284,9 @@ export const bulkImportExcelData = async (req, res) => {
}
}
res
.status(200)
.json({ success: true, message: "✅ Import completed successfully!" });
res.status(200).json({ success: true, message: '✅ Import completed successfully!' });
} catch (error) {
console.error("❌ Bulk Import Error:", error);
console.error('❌ Bulk Import Error:', error);
res.status(500).json({ success: false, error: error.message });
}
};
+16 -16
View File
@@ -1,7 +1,7 @@
import prisma from "../prisma/client.js";
import prisma from '../prisma/client.js';
import {sendEmail} from "../utils/sendEmail.js";
import {getEmailsByType} from "../utils/getEmailByTypes.js";
import { sendEmail } from '../utils/sendEmail.js';
import { getEmailsByType } from '../utils/getEmailByTypes.js';
/* CREATE INQUIRY */
export const createInquiry = async (req, res) => {
@@ -11,7 +11,7 @@ export const createInquiry = async (req, res) => {
if (!fullName || !number) {
return res.status(400).json({
success: false,
message: "Full name and number are required",
message: 'Full name and number are required',
});
}
@@ -25,12 +25,12 @@ export const createInquiry = async (req, res) => {
},
});
try {
const emailList = await getEmailsByType("INQUIRY");
const emailList = await getEmailsByType('INQUIRY');
if (emailList && emailList.length > 0) {
await sendEmail({
to: emailList,
subject: "New Inquiry Received",
subject: 'New Inquiry Received',
html: `
<div style="font-family: Arial, sans-serif; background-color: #f4f6f8; padding: 20px;">
@@ -78,7 +78,7 @@ export const createInquiry = async (req, res) => {
word-break: break-word;
overflow-wrap: anywhere;
">
${message ? message.replace(/\n/g, "<br/>") : "-"}
${message ? message.replace(/\n/g, '<br/>') : '-'}
</div>
</div>
@@ -96,20 +96,20 @@ export const createInquiry = async (req, res) => {
});
}
} catch (err) {
console.error("Inquiry email failed:", err);
console.error('Inquiry email failed:', err);
}
res.status(200).json({
success: true,
status: 200,
data: inquiry,
message: "Inquiry added successfully",
message: 'Inquiry added successfully',
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Failed to add inquiry",
message: 'Failed to add inquiry',
});
}
};
@@ -119,7 +119,7 @@ export const getInquiries = async (req, res) => {
try {
const inquiries = await prisma.inquiry.findMany({
orderBy: {
createdAt: "desc",
createdAt: 'desc',
},
});
@@ -130,7 +130,7 @@ export const getInquiries = async (req, res) => {
} catch (error) {
res.status(500).json({
success: false,
message: "Failed to fetch inquiries",
message: 'Failed to fetch inquiries',
});
}
};
@@ -147,7 +147,7 @@ export const getInquiry = async (req, res) => {
if (!inquiry) {
return res.status(404).json({
success: false,
message: "Inquiry not found",
message: 'Inquiry not found',
});
}
@@ -158,7 +158,7 @@ export const getInquiry = async (req, res) => {
} catch (error) {
res.status(500).json({
success: false,
message: "Failed to fetch inquiry",
message: 'Failed to fetch inquiry',
});
}
};
@@ -174,12 +174,12 @@ export const deleteInquiry = async (req, res) => {
res.json({
success: true,
message: "Inquiry deleted successfully",
message: 'Inquiry deleted successfully',
});
} catch (error) {
res.status(500).json({
success: false,
message: "Failed to delete inquiry",
message: 'Failed to delete inquiry',
});
}
};
+15 -23
View File
@@ -1,4 +1,4 @@
import prisma from "../prisma/client.js";
import prisma from '../prisma/client.js';
// GET ALL NEWS
@@ -6,7 +6,7 @@ export const getAllNews = async (req, res) => {
try {
const page = req.query.page ? parseInt(req.query.page) : null;
const limit = req.query.limit ? parseInt(req.query.limit) : null;
const search = req.query.search?.trim() || "";
const search = req.query.search?.trim() || '';
const includeImages = {
images: true,
@@ -16,7 +16,7 @@ export const getAllNews = async (req, res) => {
? {
headline: {
contains: search,
mode: "insensitive",
mode: 'insensitive',
},
}
: {};
@@ -32,7 +32,7 @@ export const getAllNews = async (req, res) => {
prisma.newsMedia.findMany({
where: whereCondition,
include: includeImages,
orderBy: { createdAt: "desc" },
orderBy: { createdAt: 'desc' },
skip,
take,
}),
@@ -69,7 +69,7 @@ export const getAllNews = async (req, res) => {
console.error(error);
return res.status(500).json({
success: false,
message: "Failed to fetch news",
message: 'Failed to fetch news',
});
}
};
@@ -87,7 +87,7 @@ export const getNewsById = async (req, res) => {
if (!n) {
return res.status(404).json({
success: false,
message: "News not found",
message: 'News not found',
});
}
@@ -113,7 +113,7 @@ export const getNewsById = async (req, res) => {
console.error(error);
return res.status(500).json({
success: false,
message: "Failed to fetch news",
message: 'Failed to fetch news',
});
}
};
@@ -122,20 +122,12 @@ export const getNewsById = async (req, res) => {
export const createNews = async (req, res) => {
try {
const {
headline,
content,
firstPara,
secondPara,
date,
author,
imageUrls,
} = req.body;
const { headline, content, firstPara, secondPara, date, author, imageUrls } = req.body;
if (!headline) {
return res.status(400).json({
success: false,
message: "Headline is required",
message: 'Headline is required',
});
}
@@ -158,14 +150,14 @@ export const createNews = async (req, res) => {
return res.status(201).json({
success: true,
message: "News created successfully",
message: 'News created successfully',
data: news,
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: "Failed to create news",
message: 'Failed to create news',
});
}
};
@@ -194,14 +186,14 @@ export const updateNews = async (req, res) => {
return res.status(200).json({
success: true,
message: "News updated successfully",
message: 'News updated successfully',
data: news,
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: "Failed to update news",
message: 'Failed to update news',
});
}
};
@@ -218,13 +210,13 @@ export const deleteNews = async (req, res) => {
return res.status(200).json({
success: true,
message: "News deleted successfully",
message: 'News deleted successfully',
});
} catch (error) {
console.error(error);
return res.status(500).json({
success: false,
message: "Failed to delete news",
message: 'Failed to delete news',
});
}
};
+3 -3
View File
@@ -1,9 +1,9 @@
import multer from "multer";
import path from "path";
import multer from 'multer';
import path from 'path';
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, "uploads/blog");
cb(null, 'uploads/blog');
},
filename: function (req, file, cb) {
+5 -5
View File
@@ -1,19 +1,19 @@
import {verifyToken} from "../utils/jwt.js";
import { verifyToken } from '../utils/jwt.js';
export default function jwtAuthMiddleware(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return res.status(401).json({error: "No token provided"});
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.split(" ")[1];
const token = authHeader.split(' ')[1];
try {
const user = verifyToken(token);
req.user = user;
next();
} catch (err) {
return res.status(401).json({error: "Invalid or expired token"});
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
import {PrismaClient} from "@prisma/client";
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
@@ -1,18 +1,18 @@
import express from "express";
import express from 'express';
import {
createAcademicsResearch,
getAcademicsResearch,
getSingleAcademicsResearch,
deleteAcademicsResearch,
} from "../controllers/academicsResearch.controller.js";
} from '../controllers/academicsResearch.controller.js';
import jwtAuthMiddleware from "../middleware/auth.js";
import jwtAuthMiddleware from '../middleware/auth.js';
const router = express.Router();
router.post("/", createAcademicsResearch);
router.get("/getAll", jwtAuthMiddleware, getAcademicsResearch);
router.get("/:id", jwtAuthMiddleware, getSingleAcademicsResearch);
router.delete("/:id", jwtAuthMiddleware, deleteAcademicsResearch);
router.post('/', createAcademicsResearch);
router.get('/getAll', jwtAuthMiddleware, getAcademicsResearch);
router.get('/:id', jwtAuthMiddleware, getSingleAcademicsResearch);
router.delete('/:id', jwtAuthMiddleware, deleteAcademicsResearch);
export default router;
+8 -8
View File
@@ -1,23 +1,23 @@
import express from "express";
import express from 'express';
import {
createAppointment,
getAppointments,
getAppointment,
updateAppointment,
deleteAppointment,
} from "../controllers/appointment.controller.js";
} from '../controllers/appointment.controller.js';
import jwtAuthMiddleware from "../middleware/auth.js";
import jwtAuthMiddleware from '../middleware/auth.js';
const router = express.Router();
/* PUBLIC */
router.get("/getall", jwtAuthMiddleware, getAppointments);
router.post("/", createAppointment);
router.get('/getall', jwtAuthMiddleware, getAppointments);
router.post('/', createAppointment);
router.get("/:id", jwtAuthMiddleware, getAppointment);
router.patch("/:id", jwtAuthMiddleware, updateAppointment);
router.delete("/:id", jwtAuthMiddleware, deleteAppointment);
router.get('/:id', jwtAuthMiddleware, getAppointment);
router.patch('/:id', jwtAuthMiddleware, updateAppointment);
router.delete('/:id', jwtAuthMiddleware, deleteAppointment);
export default router;
+3 -3
View File
@@ -1,8 +1,8 @@
import express from "express";
import { login } from "../controllers/auth.controller.js";
import express from 'express';
import { login } from '../controllers/auth.controller.js';
const router = express.Router();
router.post("/login", login);
router.post('/login', login);
export default router;
+10 -10
View File
@@ -1,4 +1,4 @@
import express from "express";
import express from 'express';
import {
createBlog,
getBlogs,
@@ -7,25 +7,25 @@ import {
deleteBlog,
getAllBlogs,
getBlogForAdmin,
} from "../controllers/blog.controller.js";
} from '../controllers/blog.controller.js';
import jwtAuthMiddleware from "../middleware/auth.js";
import jwtAuthMiddleware from '../middleware/auth.js';
const router = express.Router();
/* PUBLIC */
router.get("/", getBlogs);
router.get("/:slug", getBlog);
router.get('/', getBlogs);
router.get('/:slug', getBlog);
// Protected
router.get("/admin/all", jwtAuthMiddleware, getAllBlogs);
router.get('/admin/all', jwtAuthMiddleware, getAllBlogs);
router.get("/admin/:id", jwtAuthMiddleware, getBlogForAdmin);
router.get('/admin/:id', jwtAuthMiddleware, getBlogForAdmin);
router.post("/", jwtAuthMiddleware, createBlog);
router.put("/:id", jwtAuthMiddleware, updateBlog);
router.delete("/:id", jwtAuthMiddleware, deleteBlog);
router.post('/', jwtAuthMiddleware, createBlog);
router.put('/:id', jwtAuthMiddleware, updateBlog);
router.delete('/:id', jwtAuthMiddleware, deleteBlog);
export default router;
+9 -9
View File
@@ -1,4 +1,4 @@
import express from "express";
import express from 'express';
import {
createCandidate,
getCandidates,
@@ -6,20 +6,20 @@ import {
getCandidatesByCareer,
updateCandidate,
deleteCandidate,
} from "../controllers/candidate.controller.js";
} from '../controllers/candidate.controller.js';
import jwtAuthMiddleware from "../middleware/auth.js";
import jwtAuthMiddleware from '../middleware/auth.js';
const router = express.Router();
/* PUBLIC */
router.post("/", createCandidate);
router.post('/', createCandidate);
router.get("/getAll", jwtAuthMiddleware, getCandidates);
router.get("/:id", jwtAuthMiddleware, getCandidate);
router.get("/career/:careerId", jwtAuthMiddleware, getCandidatesByCareer);
router.get('/getAll', jwtAuthMiddleware, getCandidates);
router.get('/:id', jwtAuthMiddleware, getCandidate);
router.get('/career/:careerId', jwtAuthMiddleware, getCandidatesByCareer);
router.patch("/:id", jwtAuthMiddleware, updateCandidate);
router.delete("/:id", jwtAuthMiddleware, deleteCandidate);
router.patch('/:id', jwtAuthMiddleware, updateCandidate);
router.delete('/:id', jwtAuthMiddleware, deleteCandidate);
export default router;
+7 -12
View File
@@ -1,19 +1,14 @@
import express from "express";
import {
getAllCareers,
createCareer,
updateCareer,
deleteCareer,
} from "../controllers/career.controller.js";
import express from 'express';
import { getAllCareers, createCareer, updateCareer, deleteCareer } from '../controllers/career.controller.js';
import jwtAuthMiddleware from "../middleware/auth.js";
import jwtAuthMiddleware from '../middleware/auth.js';
const router = express.Router();
router.get("/getAll", getAllCareers);
router.get('/getAll', getAllCareers);
router.post("/", jwtAuthMiddleware, createCareer);
router.patch("/:id", jwtAuthMiddleware, updateCareer);
router.delete("/:id", jwtAuthMiddleware, deleteCareer);
router.post('/', jwtAuthMiddleware, createCareer);
router.patch('/:id', jwtAuthMiddleware, updateCareer);
router.delete('/:id', jwtAuthMiddleware, deleteCareer);
export default router;
+8 -8
View File
@@ -1,22 +1,22 @@
import express from "express";
import express from 'express';
import {
getAllDepartments,
getDepartmentByName,
createDepartment,
updateDepartment,
deleteDepartment,
} from "../controllers/department.controller.js";
import jwtAuthMiddleware from "../middleware/auth.js";
} from '../controllers/department.controller.js';
import jwtAuthMiddleware from '../middleware/auth.js';
const router = express.Router();
// Public
router.get("/getAll", getAllDepartments);
router.get("/search", getDepartmentByName);
router.get('/getAll', getAllDepartments);
router.get('/search', getDepartmentByName);
// Protected
router.post("/", jwtAuthMiddleware, createDepartment);
router.put("/:departmentId", jwtAuthMiddleware, updateDepartment);
router.delete("/:departmentId", jwtAuthMiddleware, deleteDepartment);
router.post('/', jwtAuthMiddleware, createDepartment);
router.put('/:departmentId', jwtAuthMiddleware, updateDepartment);
router.delete('/:departmentId', jwtAuthMiddleware, deleteDepartment);
export default router;
+11 -11
View File
@@ -1,4 +1,4 @@
import express from "express";
import express from 'express';
import {
getAllDoctors,
createDoctor,
@@ -8,20 +8,20 @@ import {
getDoctorTimingById,
getDoctorByDoctorId,
getDoctorsByDepartmentId,
} from "../controllers/doctor.controller.js";
} from '../controllers/doctor.controller.js';
import jwtAuthMiddleware from "../middleware/auth.js";
import jwtAuthMiddleware from '../middleware/auth.js';
const router = express.Router();
router.get("/getAll", getAllDoctors);
router.get("/search", getDoctorsByDepartmentId);
router.get("/getTimings", getDoctorTimings);
router.get("/getTimings/:doctorId", getDoctorTimingById);
router.get("/:doctorId", getDoctorByDoctorId);
router.get('/getAll', getAllDoctors);
router.get('/search', getDoctorsByDepartmentId);
router.get('/getTimings', getDoctorTimings);
router.get('/getTimings/:doctorId', getDoctorTimingById);
router.get('/:doctorId', getDoctorByDoctorId);
router.post("/", jwtAuthMiddleware, createDoctor);
router.patch("/:doctorId", jwtAuthMiddleware, updateDoctor);
router.delete("/:doctorId", jwtAuthMiddleware, deleteDoctor);
router.post('/', jwtAuthMiddleware, createDoctor);
router.patch('/:doctorId/:action', jwtAuthMiddleware, updateDoctor);
router.delete('/:doctorId', jwtAuthMiddleware, deleteDoctor);
export default router;
+7 -7
View File
@@ -1,19 +1,19 @@
import express from "express";
import express from 'express';
import {
getEmailConfigs,
createEmailConfig,
updateEmailConfig,
deleteEmailConfig,
} from "../controllers/emailConfig.controller.js";
} from '../controllers/emailConfig.controller.js';
import jwtAuthMiddleware from "../middleware/auth.js";
import jwtAuthMiddleware from '../middleware/auth.js';
const router = express.Router();
router.get("/getAll", getEmailConfigs);
router.get('/getAll', getEmailConfigs);
router.post("/", jwtAuthMiddleware, createEmailConfig);
router.patch("/:id", jwtAuthMiddleware, updateEmailConfig);
router.delete("/:id", jwtAuthMiddleware, deleteEmailConfig);
router.post('/', jwtAuthMiddleware, createEmailConfig);
router.patch('/:id', jwtAuthMiddleware, updateEmailConfig);
router.delete('/:id', jwtAuthMiddleware, deleteEmailConfig);
export default router;
+39
View File
@@ -0,0 +1,39 @@
import express from 'express';
import {
// Categories
getAllCategories,
getPackageBySlug,
createCategory,
updateCategory,
deleteCategory,
// Packages
getAllPackages,
createPackage,
updatePackage,
deletePackage,
// Inquiries
createPackageInquiry,
getAllInquiries,
} from '../controllers/healthCheck.controller.js';
import jwtAuthMiddleware from '../middleware/auth.js';
const router = express.Router();
router.get('/packages', getAllPackages);
router.get('/packages/:slug', getPackageBySlug);
router.get('/categories', getAllCategories);
router.post('/inquiry', createPackageInquiry);
router.get('/inquiries', jwtAuthMiddleware, getAllInquiries);
router.post('/', jwtAuthMiddleware, createPackage);
router.patch('/:id', jwtAuthMiddleware, updatePackage);
router.delete('/:id', jwtAuthMiddleware, deletePackage);
router.post('/categories', jwtAuthMiddleware, createCategory);
router.patch('/categories/:id', jwtAuthMiddleware, updateCategory);
router.delete('/categories/:id', jwtAuthMiddleware, deleteCategory);
export default router;
+4 -4
View File
@@ -1,9 +1,9 @@
import express from "express";
import { bulkImportExcelData } from "../controllers/importController.js";
import jwtAuthMiddleware from "../middleware/auth.js";
import express from 'express';
import { bulkImportExcelData } from '../controllers/importController.js';
import jwtAuthMiddleware from '../middleware/auth.js';
const router = express.Router();
router.post("/bulk", jwtAuthMiddleware, bulkImportExcelData);
router.post('/bulk', jwtAuthMiddleware, bulkImportExcelData);
export default router;
+7 -12
View File
@@ -1,19 +1,14 @@
import express from "express";
import {
createInquiry,
getInquiries,
getInquiry,
deleteInquiry,
} from "../controllers/inquiry.controller.js";
import express from 'express';
import { createInquiry, getInquiries, getInquiry, deleteInquiry } from '../controllers/inquiry.controller.js';
import jwtAuthMiddleware from "../middleware/auth.js";
import jwtAuthMiddleware from '../middleware/auth.js';
const router = express.Router();
router.post("/", createInquiry);
router.post('/', createInquiry);
router.get("/getAll", jwtAuthMiddleware, getInquiries);
router.get("/:id", jwtAuthMiddleware, getInquiry);
router.delete("/:id", jwtAuthMiddleware, deleteInquiry);
router.get('/getAll', jwtAuthMiddleware, getInquiries);
router.get('/:id', jwtAuthMiddleware, getInquiry);
router.delete('/:id', jwtAuthMiddleware, deleteInquiry);
export default router;
+8 -14
View File
@@ -1,23 +1,17 @@
import express from "express";
import {
createNews,
getAllNews,
getNewsById,
updateNews,
deleteNews,
} from "../controllers/newsMedia.controller.js";
import express from 'express';
import { createNews, getAllNews, getNewsById, updateNews, deleteNews } from '../controllers/newsMedia.controller.js';
import jwtAuthMiddleware from "../middleware/auth.js";
import jwtAuthMiddleware from '../middleware/auth.js';
const router = express.Router();
// PUBLIC ROUTES
router.get("/getAll", getAllNews);
router.get("/:id", getNewsById);
router.get('/getAll', getAllNews);
router.get('/:id', getNewsById);
// PROTECTED ROUTES
router.post("/", jwtAuthMiddleware, createNews);
router.patch("/:id", jwtAuthMiddleware, updateNews);
router.delete("/:id", jwtAuthMiddleware, deleteNews);
router.post('/', jwtAuthMiddleware, createNews);
router.patch('/:id', jwtAuthMiddleware, updateNews);
router.delete('/:id', jwtAuthMiddleware, deleteNews);
export default router;
+6 -6
View File
@@ -1,6 +1,6 @@
import express from "express";
import * as Bytescale from "@bytescale/sdk";
import multer from "multer";
import express from 'express';
import * as Bytescale from '@bytescale/sdk';
import multer from 'multer';
const router = express.Router();
@@ -11,7 +11,7 @@ const uploadManager = new Bytescale.UploadManager({
const storage = multer.memoryStorage();
const upload = multer({ storage });
router.post("/", upload.single("file"), async (req, res) => {
router.post('/', upload.single('file'), async (req, res) => {
try {
const file = req.file;
const { folderPath } = req.body;
@@ -21,14 +21,14 @@ router.post("/", upload.single("file"), async (req, res) => {
name: file.originalname,
mime: file.mimetype,
path: {
folderPath: folderPath || "/general",
folderPath: folderPath || '/general',
},
});
res.json({ fileUrl: result.fileUrl });
} catch (error) {
console.error(error);
res.status(500).json({error: "Upload failed"});
res.status(500).json({ error: 'Upload failed' });
}
});
+6 -8
View File
@@ -1,14 +1,12 @@
import prisma from "../prisma/client.js";
import { hashPassword } from "./password.js";
import prisma from '../prisma/client.js';
import { hashPassword } from './password.js';
async function main() {
const username = process.argv[2];
const password = process.argv[3];
const role = process.argv[4] || "admin";
const role = process.argv[4] || 'admin';
if (!username || !password) {
console.log(
"Usage: node scripts/createUser.js <username> <password> [role]",
);
console.log('Usage: node scripts/createUser.js <username> <password> [role]');
process.exit(1);
}
@@ -17,7 +15,7 @@ async function main() {
});
if (existingUser) {
console.log("User already exists");
console.log('User already exists');
process.exit(1);
}
@@ -31,7 +29,7 @@ async function main() {
},
});
console.log("User created:", {
console.log('User created:', {
id: user.id,
username: user.username,
role: user.role,
+4 -4
View File
@@ -1,4 +1,4 @@
import prisma from "../prisma/client.js";
import prisma from '../prisma/client.js';
export const getEmailsByType = async (type) => {
try {
@@ -9,9 +9,9 @@ export const getEmailsByType = async (type) => {
},
});
return emails.map((e) => e.email).join(",");
return emails.map((e) => e.email).join(',');
} catch (error) {
console.error("Fetch email config error:", error);
return "";
console.error('Fetch email config error:', error);
return '';
}
};
+3 -3
View File
@@ -1,10 +1,10 @@
import jwt from "jsonwebtoken";
import "dotenv/config";
import jwt from 'jsonwebtoken';
import 'dotenv/config';
const SECRET = process.env.JWT_SECRET;
export function generateToken(payload) {
return jwt.sign(payload, SECRET, {expiresIn: "24h"});
return jwt.sign(payload, SECRET, { expiresIn: '24h' });
}
export function verifyToken(token) {
+1 -1
View File
@@ -1,4 +1,4 @@
import bcrypt from "bcryptjs";
import bcrypt from 'bcryptjs';
export async function hashPassword(password) {
return bcrypt.hash(password, 10);
+4 -4
View File
@@ -1,4 +1,4 @@
import postmark from "postmark";
import postmark from 'postmark';
const client = new postmark.ServerClient(process.env.POSTMARK_API_KEY);
@@ -9,10 +9,10 @@ export const sendEmail = async ({to, subject, html, text}) => {
To: to,
Subject: subject,
HtmlBody: html,
TextBody: text || "",
MessageStream: "outbound",
TextBody: text || '',
MessageStream: 'outbound',
});
} catch (error) {
console.error("Email send error:", error);
console.error('Email send error:', error);
}
};
+4 -4
View File
@@ -1,4 +1,4 @@
version: "3.8"
version: '3.8'
services:
backend:
@@ -6,7 +6,7 @@ services:
context: .
dockerfile: docker/dev/Dockerfile.main
ports:
- "127.0.0.1:5008:5008"
- '127.0.0.1:5008:5008'
env_file:
- ./backend/.env
depends_on:
@@ -19,7 +19,7 @@ services:
context: .
dockerfile: docker/dev/Dockerfile.frontend
ports:
- "127.0.0.1:3008:3000"
- '127.0.0.1:3008:3000'
env_file:
- ./frontend/.env
restart: unless-stopped
@@ -33,7 +33,7 @@ services:
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
test: ['CMD-SHELL', 'pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB']
interval: 5s
timeout: 5s
retries: 5
+3 -3
View File
@@ -4,7 +4,7 @@ services:
context: .
dockerfile: docker/dev/Dockerfile.main
ports:
- "127.0.0.1:5008:5008"
- '127.0.0.1:5008:5008'
env_file:
- ./backend/.env
depends_on:
@@ -17,7 +17,7 @@ services:
context: .
dockerfile: docker/dev/Dockerfile.frontend
ports:
- "127.0.0.1:3008:3000"
- '127.0.0.1:3008:3000'
env_file:
- ./frontend/.env
restart: always
@@ -31,7 +31,7 @@ services:
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
test: ['CMD-SHELL', 'pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB']
interval: 5s
timeout: 5s
retries: 5
+2 -4
View File
@@ -4,10 +4,8 @@ set -e # Exit immediately if a command exits with a non-zero status
echo "Generating Prisma Client..."
npx prisma generate
# echo "Running migrate..."
# npx prisma migrate deploy
echo "Running PUSH..."
npx prisma db push
echo "Running migrate..."
npx prisma migrate deploy
echo "Executing command: $@"
exec "$@"
+7 -7
View File
@@ -1,9 +1,9 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';
import { defineConfig, globalIgnores } from 'eslint/config';
export default defineConfig([
globalIgnores(['dist']),
@@ -20,4 +20,4 @@ export default defineConfig([
globals: globals.browser,
},
},
])
]);
+1 -1
View File
@@ -1,5 +1,5 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
'@tailwindcss/postcss': {},
},
};
+22 -20
View File
@@ -1,28 +1,29 @@
import {BrowserRouter, Routes, Route, Navigate} from "react-router-dom";
import {Toaster} from "react-hot-toast";
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { Toaster } from 'react-hot-toast';
import Login from "@/pages/Login";
import Login from '@/pages/Login';
import DashboardLayout from "./layouts/DashboardLayout";
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";
import Appointment from "./pages/Appointment";
import EmailPage from "./pages/email";
import CareerPage from "./pages/Career";
import CandidatePage from "./pages/candidates";
import InquiryPage from "./pages/inquiry";
import AcademicsPage from "./pages/Academics";
import NewsPage from "./pages/newsMedia";
import BlogDetail from "./pages/BlogDetails";
import ImportData from "./pages/ImportData";
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';
import Appointment from './pages/Appointment';
import EmailPage from './pages/email';
import CareerPage from './pages/Career';
import CandidatePage from './pages/candidates';
import InquiryPage from './pages/inquiry';
import AcademicsPage from './pages/Academics';
import NewsPage from './pages/newsMedia';
import BlogDetail from './pages/BlogDetails';
import ImportData from './pages/ImportData';
import HealthPackagePage from './pages/HealthPackagePage';
export default function App() {
return (
@@ -51,6 +52,7 @@ export default function App() {
<Route path="/academics" element={<AcademicsPage />} />
<Route path="/news" element={<NewsPage />} />
<Route path="/import" element={<ImportData />} />
<Route path="/health-check" element={<HealthPackagePage />} />
</Route>
</Route>
+2 -2
View File
@@ -1,7 +1,7 @@
import apiClient from "@/api/client";
import apiClient from '@/api/client';
export const getAcademicsApi = async () => {
const res = await apiClient.get("/academics/getAll");
const res = await apiClient.get('/academics/getAll');
return res.data;
};
+5 -5
View File
@@ -1,12 +1,12 @@
import apiClient from "@/api/client";
import apiClient from '@/api/client';
export const getAppointmentsApi = async (
page = 1,
limit = 10,
date = "",
startDate = "",
endDate = "",
search = "",
date = '',
startDate = '',
endDate = '',
search = ''
) => {
const params = new URLSearchParams({
page: String(page),
+3 -6
View File
@@ -1,10 +1,7 @@
import apiClient from "./client";
import apiClient from './client';
export const loginApi = async (
username: string,
password: string,
): Promise<any> => {
const response = await apiClient.post("/auth/login/", {
export const loginApi = async (username: string, password: string): Promise<any> => {
const response = await apiClient.post('/auth/login/', {
username,
password,
});
+6 -6
View File
@@ -1,4 +1,4 @@
import apiClient from "@/api/client";
import apiClient from '@/api/client';
export interface Blog {
id?: number;
@@ -9,7 +9,7 @@ export interface Blog {
}
export const getAllBlogsApi = async () => {
const res = await apiClient.get("/blogs");
const res = await apiClient.get('/blogs');
return res.data;
};
@@ -19,7 +19,7 @@ export const getBlogByIdApi = async (id: number) => {
};
export const createBlogApi = async (data: Blog) => {
const res = await apiClient.post("/blogs", data);
const res = await apiClient.post('/blogs', data);
return res.data;
};
@@ -36,11 +36,11 @@ export const deleteBlogApi = async (id: number) => {
/* IMAGE UPLOAD */
export const uploadImageApi = async (file: File) => {
const formData = new FormData();
formData.append("image", file);
formData.append('image', file);
const res = await apiClient.post("/upload/image", formData, {
const res = await apiClient.post('/upload/image', formData, {
headers: {
"Content-Type": "multipart/form-data",
'Content-Type': 'multipart/form-data',
},
});
+2 -2
View File
@@ -1,7 +1,7 @@
import apiClient from "@/api/client";
import apiClient from '@/api/client';
export const getCandidatesApi = async () => {
const res = await apiClient.get("/candidates/getAll");
const res = await apiClient.get('/candidates/getAll');
return res.data;
};
+10 -10
View File
@@ -1,20 +1,20 @@
import apiClient from "@/api/client";
import toast from "react-hot-toast";
import apiClient from '@/api/client';
import toast from 'react-hot-toast';
export const getCareersApi = async () => {
const res = await apiClient.get("/careers/getAll?admin=true");
const res = await apiClient.get('/careers/getAll?admin=true');
return res.data;
};
export const createCareerApi = async (data: any) => {
try {
const res = await apiClient.post("/careers", data);
const res = await apiClient.post('/careers', data);
toast.success("Career created successfully");
toast.success('Career created successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || "Failed to create career");
toast.error(error?.response?.data?.message || 'Failed to create career');
throw error;
}
@@ -24,11 +24,11 @@ export const updateCareerApi = async (id: number, data: any) => {
try {
const res = await apiClient.patch(`/careers/${id}`, data);
toast.success("Career updated successfully");
toast.success('Career updated successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || "Failed to update career");
toast.error(error?.response?.data?.message || 'Failed to update career');
throw error;
}
@@ -38,11 +38,11 @@ export const deleteCareerApi = async (id: number) => {
try {
const res = await apiClient.delete(`/careers/${id}`);
toast.success("Career deleted successfully");
toast.success('Career deleted successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || "Failed to delete career");
toast.error(error?.response?.data?.message || 'Failed to delete career');
throw error;
}
+12 -12
View File
@@ -1,48 +1,48 @@
import axios from "axios";
import type {InternalAxiosRequestConfig} from "axios";
import axios from 'axios';
import type { InternalAxiosRequestConfig } from 'axios';
const baseURL: string = import.meta.env.VITE_API_URL;
const apiClient = axios.create({
baseURL: baseURL,
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
});
export const setAxiosAuthToken = (token: string | null): void => {
if (token) {
apiClient.defaults.headers.common["Authorization"] = `Bearer ${token}`;
apiClient.defaults.headers.common['Authorization'] = `Bearer ${token}`;
} else {
delete apiClient.defaults.headers.common["Authorization"];
delete apiClient.defaults.headers.common['Authorization'];
}
};
apiClient.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = localStorage.getItem("token");
const token = localStorage.getItem('token');
if (token && config.headers) {
config.headers["Authorization"] = `Bearer ${token}`;
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
},
(error: any) => Promise.reject(error),
(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");
console.error('Unauthorized - token missing or invalid');
localStorage.removeItem("token");
window.location.href = "/login";
localStorage.removeItem('token');
window.location.href = '/login';
}
return Promise.reject(error);
},
}
);
export default apiClient;
+11 -17
View File
@@ -1,5 +1,5 @@
import apiClient from "@/api/client";
import toast from "react-hot-toast";
import apiClient from '@/api/client';
import toast from 'react-hot-toast';
export interface Department {
departmentId: string;
@@ -15,7 +15,7 @@ export interface Department {
}
export const getDepartmentsApi = async () => {
const res = await apiClient.get("/departments/getAll?admin=true");
const res = await apiClient.get('/departments/getAll?admin=true');
return res.data;
};
@@ -29,15 +29,13 @@ export const createDepartmentApi = async (data: {
services?: string;
}) => {
try {
const res = await apiClient.post("/departments", data);
const res = await apiClient.post('/departments', data);
toast.success("Department created successfully");
toast.success('Department created successfully');
return res.data;
} catch (error: any) {
toast.error(
error?.response?.data?.message || "Failed to create department",
);
toast.error(error?.response?.data?.message || 'Failed to create department');
throw error;
}
@@ -52,18 +50,16 @@ export const updateDepartmentApi = async (
para3?: string;
facilities?: string;
services?: string;
},
}
) => {
try {
const res = await apiClient.put(`/departments/${departmentId}`, data);
toast.success("Department updated successfully");
toast.success('Department updated successfully');
return res.data;
} catch (error: any) {
toast.error(
error?.response?.data?.message || "Failed to update department",
);
toast.error(error?.response?.data?.message || 'Failed to update department');
throw error;
}
@@ -73,13 +69,11 @@ export const deleteDepartmentApi = async (departmentId: string) => {
try {
const res = await apiClient.delete(`/departments/${departmentId}`);
toast.success("Department deleted successfully");
toast.success('Department deleted successfully');
return res.data;
} catch (error: any) {
toast.error(
error?.response?.data?.message || "Failed to delete department",
);
toast.error(error?.response?.data?.message || 'Failed to delete department');
throw error;
}
+12 -11
View File
@@ -1,5 +1,5 @@
import apiClient from "@/api/client";
import toast from "react-hot-toast";
import apiClient from '@/api/client';
import toast from 'react-hot-toast';
export interface Doctor {
doctorId: string;
@@ -27,7 +27,7 @@ export interface Doctor {
}
export const getDoctorsApi = async () => {
const res = await apiClient.get("/doctors/getAll?admin=true");
const res = await apiClient.get('/doctors/getAll?admin=true');
return res.data;
};
@@ -38,13 +38,13 @@ export const getDoctorByIdApi = async (doctorId: string) => {
export const createDoctorApi = async (data: Doctor) => {
try {
const res = await apiClient.post("/doctors", data);
const res = await apiClient.post('/doctors', data);
toast.success("Doctor created successfully");
toast.success('Doctor created successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || "Failed to create doctor");
toast.error(error?.response?.data?.message || 'Failed to create doctor');
throw error;
}
@@ -53,15 +53,16 @@ export const createDoctorApi = async (data: Doctor) => {
export const updateDoctorApi = async (
doctorId: string,
data: Partial<Doctor>,
action: 'toggleStatus' | 'updateDetails' = 'updateDetails'
) => {
try {
const res = await apiClient.patch(`/doctors/${doctorId}`, data);
const res = await apiClient.patch(`/doctors/${doctorId}/${action}`, data);
toast.success("Doctor updated successfully");
toast.success('Doctor updated successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || "Failed to update doctor");
toast.error(error?.response?.data?.message || 'Failed to update doctor');
throw error;
}
@@ -71,11 +72,11 @@ export const deleteDoctorApi = async (doctorId: string) => {
try {
const res = await apiClient.delete(`/doctors/${doctorId}`);
toast.success("Doctor deleted successfully");
toast.success('Doctor deleted successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || "Failed to delete doctor");
toast.error(error?.response?.data?.message || 'Failed to delete doctor');
throw error;
}
+4 -7
View File
@@ -1,4 +1,4 @@
import apiClient from "@/api/client";
import apiClient from '@/api/client';
export interface EmailConfig {
id?: number;
@@ -10,21 +10,18 @@ export interface EmailConfig {
// GET ALL
export const getEmailConfigsApi = async () => {
const res = await apiClient.get("/email/getAll");
const res = await apiClient.get('/email/getAll');
return res.data;
};
// CREATE
export const createEmailConfigApi = async (data: EmailConfig) => {
const res = await apiClient.post("/email", data);
const res = await apiClient.post('/email', data);
return res.data;
};
// UPDATE
export const updateEmailConfigApi = async (
id: number,
data: Partial<EmailConfig>,
) => {
export const updateEmailConfigApi = async (id: number, data: Partial<EmailConfig>) => {
const res = await apiClient.patch(`/email/${id}`, data);
return res.data;
};
+155
View File
@@ -0,0 +1,155 @@
import apiClient from '@/api/client';
import toast from 'react-hot-toast';
export interface SeoData {
seoTitle?: string;
metaDescription?: string;
focusKeyphrase?: string;
tags?: string[];
ogTitle?: string;
ogDescription?: string;
ogImage?: string;
}
export interface HealthPackage {
id?: number;
name: string;
slug: string;
description?: string;
price?: number;
image?: string;
discountedPrice?: number;
inclusions: Record<string, string[]>;
categoryId: number;
isActive: boolean;
isFeatured: boolean;
sortOrder: number;
seo?: SeoData | null;
category?: {
name: string;
};
}
export interface HealthCategory {
id: number;
name: string;
slug: string;
sortOrder: number;
isActive: boolean;
}
export interface HealthInquiry {
id: number;
fullName: string;
mobileNumber: string;
email?: string;
age: string;
gender: string;
preferredDate: string;
message?: string;
createdAt: string;
healthPackage?: {
name: string;
category?: {
name: string;
};
};
}
export const getHealthCategoriesApi = async () => {
const res = await apiClient.get('/health-check/categories?admin=true');
return res.data;
};
export const getHealthPackagesApi = async () => {
const res = await apiClient.get('/health-check/packages?admin=true');
return res.data;
};
export const createHealthPackageApi = async (data: Partial<HealthPackage>) => {
try {
const res = await apiClient.post('/health-check', data);
toast.success('Package created successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to create package');
throw error;
}
};
export const updateHealthPackageApi = async (id: number, data: Partial<HealthPackage>) => {
try {
const res = await apiClient.patch(`/health-check/${id}`, data);
toast.success('Package updated successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to update package');
throw error;
}
};
export const deleteHealthPackageApi = async (id: number) => {
try {
const res = await apiClient.delete(`/health-check/${id}`);
toast.success('Package deleted successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to delete package');
throw error;
}
};
export const createCategoryApi = async (data: { name: string; slug: string; sortOrder: number }) => {
try {
const res = await apiClient.post('/health-check/categories', data);
toast.success('Category created successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to create category');
throw error;
}
};
export const updateCategoryApi = async (id: number, data: any) => {
try {
const res = await apiClient.patch(`/health-check/categories/${id}`, data);
toast.success('Category updated successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to update category');
throw error;
}
};
export const deleteCategoryApi = async (id: number) => {
try {
const res = await apiClient.delete(`/health-check/categories/${id}`);
toast.success('Category deleted successfully');
return res.data;
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to delete category');
throw error;
}
};
export const getAllInquiriesApi = async (page = 1, limit = 10, filterDate = '', startDate = '', endDate = '') => {
const params = new URLSearchParams({
page: page.toString(),
limit: limit.toString(),
});
if (filterDate) params.append('filterDate', filterDate);
if (startDate) params.append('startDate', startDate);
if (endDate) params.append('endDate', endDate);
const res = await apiClient.get(`/health-check/inquiries?${params.toString()}`);
return res.data;
};
+2 -2
View File
@@ -1,7 +1,7 @@
import apiClient from "@/api/client";
import apiClient from '@/api/client';
export const getInquiriesApi = async () => {
const res = await apiClient.get("/inquiry/getAll");
const res = await apiClient.get('/inquiry/getAll');
return res.data;
};
+4 -6
View File
@@ -1,14 +1,12 @@
import apiClient from "@/api/client";
import apiClient from '@/api/client';
export const getNewsApi = async (page = 1, limit = 10, search = "") => {
const res = await apiClient.get(
`/newsMedia/getAll?page=${page}&limit=${limit}&search=${search}`,
);
export const getNewsApi = async (page = 1, limit = 10, search = '') => {
const res = await apiClient.get(`/newsMedia/getAll?page=${page}&limit=${limit}&search=${search}`);
return res.data;
};
export const createNewsApi = async (data: any) => {
const res = await apiClient.post("/newsMedia", data);
const res = await apiClient.post('/newsMedia', data);
return res.data;
};
+2 -2
View File
@@ -1,5 +1,5 @@
import {Navigate, Outlet} from "react-router-dom";
import {useAuth} from "@/context/AuthContext";
import { Navigate, Outlet } from 'react-router-dom';
import { useAuth } from '@/context/AuthContext';
export default function ProtectedRoute() {
const { token } = useAuth();
+2 -2
View File
@@ -1,5 +1,5 @@
import {Navigate, Outlet} from "react-router-dom";
import {useAuth} from "@/context/AuthContext";
import { Navigate, Outlet } from 'react-router-dom';
import { useAuth } from '@/context/AuthContext';
export default function PublicRoute() {
const { token } = useAuth();
@@ -1,19 +1,15 @@
import {useState, useRef} from "react";
import {Button} from "@/components/ui/button";
import {User, X, Loader2} from "lucide-react";
import axios from "axios";
import { useState, useRef } from 'react';
import { Button } from '@/components/ui/button';
import { User, X, Loader2 } from 'lucide-react';
import axios from 'axios';
interface BytescaleUploaderProps {
value: string;
onChange: (url: string) => void;
folderPath: "/doctors" | "/departments" | "/news" | "/blog";
folderPath: '/health-packages' | '/seo' | '/doctors' | '/departments' | '/news' | '/blog' | '/doctor-og';
}
export function BytescaleUploader({
value,
onChange,
folderPath,
}: BytescaleUploaderProps) {
export function BytescaleUploader({ value, onChange, folderPath }: BytescaleUploaderProps) {
const baseURL = import.meta.env.VITE_API_URL;
const [isUploading, setIsUploading] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
@@ -23,33 +19,32 @@ export function BytescaleUploader({
if (!file) return;
if (file.size > 5 * 1024 * 1024) {
alert("File is too large (Max 5MB)");
alert('File is too large (Max 5MB)');
return;
}
setIsUploading(true);
const formData = new FormData();
formData.append("file", file);
formData.append("folderPath", folderPath);
formData.append('file', file);
formData.append('folderPath', folderPath);
try {
const response = await axios.post(`${baseURL}/upload`, formData, {
headers: {
"Content-Type": "multipart/form-data",
'Content-Type': 'multipart/form-data',
},
});
const { fileUrl } = response.data;
onChange(fileUrl);
} catch (e: any) {
console.error("Upload Error:", e);
const errorMessage =
e.response?.data?.error || e.message || "Upload failed";
console.error('Upload Error:', e);
const errorMessage = e.response?.data?.error || e.message || 'Upload failed';
alert(`Upload Error: ${errorMessage}`);
} finally {
setIsUploading(false);
if (fileInputRef.current) fileInputRef.current.value = "";
if (fileInputRef.current) fileInputRef.current.value = '';
}
};
@@ -66,7 +61,7 @@ export function BytescaleUploader({
/>
<button
type="button"
onClick={() => onChange("")}
onClick={() => onChange('')}
className="absolute -top-1 -right-1 bg-destructive text-white rounded-full p-0.5 shadow-sm hover:scale-110 transition-transform"
>
<X className="w-3 h-3" />
@@ -104,17 +99,16 @@ export function BytescaleUploader({
Uploading...
</>
) : value ? (
"Change Photo"
'Change Photo'
) : (
"Upload Photo"
'Upload Photo'
)}
</Button>
</div>
{value && (
<p className="text-xs text-amber-600 pl-[72px]">
Make sure to save the changes by clicking the "Save Changes"
button.
Make sure to save the changes by clicking the "Save Changes" button.
</p>
)}
</div>
@@ -0,0 +1,379 @@
import { BytescaleUploader } from '@/components/BytescaleUploader/BytescaleUploader';
import SeoFields from '@/components/SeoFields/SeoFields';
import { useEffect } from 'react';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { Switch } from '@/components/ui/switch';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';
import { Plus, Trash2 } from 'lucide-react';
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
editingPackage: any;
pkgForm: any;
setPkgForm: any;
inclusionsList: any[];
setInclusionsList: any;
categories: any[];
onSave: () => void;
}
export default function HealthPackageModal({
open,
onOpenChange,
editingPackage,
pkgForm,
setPkgForm,
inclusionsList,
setInclusionsList,
categories,
onSave,
}: Props) {
useEffect(() => {
if (!editingPackage && pkgForm.name) {
setPkgForm((prev: any) => ({
...prev,
slug: prev.slug
? prev.slug
: pkgForm.name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, ''),
}));
}
}, [pkgForm.name]);
const handleAddInclusionField = () => {
setInclusionsList([
...inclusionsList,
{
id: Date.now(),
category: '',
items: '',
},
]);
};
const handleRemoveInclusionField = (id: number) => {
setInclusionsList(inclusionsList.filter((item) => item.id !== id));
};
const handleUpdateInclusionField = (id: number, field: string, value: string) => {
setInclusionsList(
inclusionsList.map((item) =>
item.id === id
? {
...item,
[field]: value,
}
: item
)
);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="w-full !max-w-7xl h-[92vh] flex flex-col p-0 overflow-hidden">
<DialogHeader className="px-6 py-5 border-b bg-background sticky top-0 z-20">
<DialogTitle className="text-2xl font-bold">
{editingPackage ? 'Edit Health Package' : 'Create Health Package'}
</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-y-auto">
<div className="grid grid-cols-1 xl:grid-cols-[1.2fr_0.8fr] gap-8 p-6">
{/* LEFT COLUMN */}
<div className="space-y-8">
<div className="space-y-5">
<div className="sticky top-0 bg-background z-10 pb-2">
<h3 className="text-lg font-bold">Profile & Pricing</h3>
<p className="text-sm text-muted-foreground">Main package information</p>
</div>
<div className="space-y-5">
<div className="space-y-2">
<Label className="font-semibold">Package Image</Label>
<p className="text-xs text-muted-foreground">Recommended size: 650 × 250</p>
<BytescaleUploader
value={pkgForm.image || ''}
folderPath="/health-packages"
onChange={(url) =>
setPkgForm({
...pkgForm,
image: url,
})
}
/>
</div>
<div className="flex items-center justify-between border rounded-xl p-4 bg-muted/30">
<div>
<p className="font-semibold">Active Visibility</p>
<p className="text-sm text-muted-foreground">Show this package publicly</p>
</div>
<Switch
checked={pkgForm.isActive}
onCheckedChange={(val) =>
setPkgForm({
...pkgForm,
isActive: val,
})
}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="font-semibold">Package Name</Label>
<Input
value={pkgForm.name}
onChange={(e) =>
setPkgForm({
...pkgForm,
name: e.target.value,
})
}
/>
</div>
<div className="space-y-2">
<Label className="font-semibold">URL Slug</Label>
<Input
value={pkgForm.slug}
onChange={(e) =>
setPkgForm({
...pkgForm,
slug: e.target.value,
})
}
/>
</div>
</div>
<div className="space-y-2">
<Label className="font-semibold">Category</Label>
<Select
value={pkgForm.categoryId?.toString()}
onValueChange={(v) =>
setPkgForm({
...pkgForm,
categoryId: Number(v),
})
}
>
<SelectTrigger>
<SelectValue placeholder="Select category" />
</SelectTrigger>
<SelectContent>
{categories.map((c) => (
<SelectItem key={c.id} value={c.id.toString()}>
{c.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label className="font-semibold">Regular Price ()</Label>
<Input
type="number"
value={pkgForm.price || ''}
onChange={(e) => {
const value = e.target.value ? Number(e.target.value) : undefined;
setPkgForm({
...pkgForm,
price: value,
});
}}
/>
</div>
<div className="space-y-2">
<Label className="font-semibold">Discounted Price ()</Label>
<Input
type="number"
disabled={!pkgForm.price}
value={pkgForm.discountedPrice || ''}
onChange={(e) =>
setPkgForm({
...pkgForm,
discountedPrice: e.target.value ? Number(e.target.value) : undefined,
})
}
/>
</div>
<div className="space-y-2">
<Label className="font-semibold">Sort Priority</Label>
<Input
type="number"
value={pkgForm.sortOrder}
onChange={(e) =>
setPkgForm({
...pkgForm,
sortOrder: Number(e.target.value),
})
}
/>
</div>
</div>
<div className="space-y-2">
<Label className="font-semibold">Description</Label>
<Textarea
rows={5}
value={pkgForm.description}
onChange={(e) =>
setPkgForm({
...pkgForm,
description: e.target.value,
})
}
/>
</div>
</div>
</div>
{/* INCLUSIONS */}
<div className="space-y-5">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-bold">Tests & Inclusions</h3>
<p className="text-sm text-muted-foreground">Group tests into categories</p>
</div>
<Badge variant="outline">{inclusionsList.length} Groups</Badge>
</div>
<Accordion type="multiple" className="space-y-4">
{inclusionsList.map((inc, index) => {
const testCount = inc.items?.split(',').filter(Boolean).length;
return (
<AccordionItem
key={inc.id}
value={inc.id.toString()}
className="border rounded-xl bg-background px-5 shadow-sm"
>
<AccordionTrigger className="hover:no-underline w-full">
<div className="flex w-full items-center justify-between">
<div className="flex flex-col items-start text-left">
<p className="font-semibold">{inc.category || `Group ${index + 1}`}</p>
<p className="text-xs text-muted-foreground">{testCount || 0} tests included</p>
</div>
<Button
variant="ghost"
size="sm"
className="text-red-500 hover:text-red-600"
onClick={() => handleRemoveInclusionField(inc.id)}
>
<Trash2 className="h-4 w-4 mr-1" />
Remove
</Button>
</div>
</AccordionTrigger>
<AccordionContent className="pt-4">
<div className="space-y-4">
<div className="space-y-2">
<Label>Category Title</Label>
<Input
placeholder="Routine Blood Tests"
value={inc.category}
onChange={(e) => handleUpdateInclusionField(inc.id, 'category', e.target.value)}
/>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label>Included Tests</Label>
</div>
<Textarea
rows={4}
placeholder="CBC, LFT, RFT, TSH"
value={inc.items}
onChange={(e) => handleUpdateInclusionField(inc.id, 'items', e.target.value)}
/>
<p className="text-xs text-muted-foreground">Separate each test using commas</p>
</div>
</div>
</AccordionContent>
</AccordionItem>
);
})}
</Accordion>
<Button
variant="outline"
className="w-full border-dashed border-2 h-12"
onClick={handleAddInclusionField}
>
<Plus className="h-4 w-4 mr-2" />
Add New Inclusion Group
</Button>
</div>
</div>
{/* RIGHT COLUMN */}
<div className="space-y-6">
<SeoFields
value={pkgForm.seo}
slug={pkgForm.slug}
folderPath="/seo"
onChange={(seo) =>
setPkgForm({
...pkgForm,
seo,
})
}
/>
</div>
</div>
</div>
<DialogFooter className="p-6 border-t bg-background sticky bottom-0 z-20">
<Button variant="ghost" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button className="px-10" onClick={onSave}>
{editingPackage ? 'Save Changes' : 'Create Package'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,219 @@
import { useState, useEffect, useCallback } from 'react';
import { getAllInquiriesApi, HealthInquiry } from '@/api/healthCheck';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Loader2, RefreshCw, ChevronLeft, ChevronRight } from 'lucide-react';
export default function PackageInquiriesTab() {
const [inquiries, setInquiries] = useState<HealthInquiry[]>([]);
const [loading, setLoading] = useState(true);
const [filterDate, setFilterDate] = useState('');
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
const [totalItems, setTotalItems] = useState(0);
const [totalPages, setTotalPages] = useState(1);
const fetchInquiries = useCallback(async () => {
setLoading(true);
try {
const res = await getAllInquiriesApi(currentPage, itemsPerPage, filterDate, startDate, endDate);
setInquiries(res.data || []);
setTotalItems(res.pagination?.total || 0);
setTotalPages(res.pagination?.totalPages || 1);
} catch (err) {
console.error('Failed to fetch inquiries', err);
} finally {
setLoading(false);
}
}, [currentPage, itemsPerPage, filterDate, startDate, endDate]);
useEffect(() => {
fetchInquiries();
}, [fetchInquiries]);
const handleFilterChange = (setter: React.Dispatch<React.SetStateAction<string>>, value: string) => {
setter(value);
setCurrentPage(1);
};
const indexOfFirstItem = (currentPage - 1) * itemsPerPage;
const indexOfLastItem = Math.min(currentPage * itemsPerPage, totalItems);
return (
<Card>
<CardHeader className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
<CardTitle className="text-xl">Package Inquiries</CardTitle>
<div className="flex flex-wrap items-end gap-3">
<div className="flex flex-col gap-1">
<label className="text-xs font-medium text-muted-foreground">Specific Date</label>
<Input
type="date"
value={filterDate}
onChange={(e) => handleFilterChange(setFilterDate, e.target.value)}
className="w-[140px] text-sm"
disabled={!!startDate || !!endDate}
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs font-medium text-muted-foreground">From</label>
<Input
type="date"
value={startDate}
onChange={(e) => handleFilterChange(setStartDate, e.target.value)}
className="w-[140px] text-sm"
disabled={!!filterDate}
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs font-medium text-muted-foreground">To</label>
<Input
type="date"
value={endDate}
onChange={(e) => handleFilterChange(setEndDate, e.target.value)}
className="w-[140px] text-sm"
disabled={!!filterDate}
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs font-medium text-muted-foreground">Rows</label>
<select
value={itemsPerPage}
onChange={(e) => {
setItemsPerPage(Number(e.target.value));
setCurrentPage(1);
}}
className="flex h-10 rounded-md border border-input bg-background px-3 py-2 text-sm focus:ring-2 focus:ring-primary"
>
<option value={5}>5 / page</option>
<option value={10}>10 / page</option>
<option value={20}>20 / page</option>
<option value={50}>50 / page</option>
</select>
</div>
<Button variant="outline" onClick={fetchInquiries} disabled={loading}>
<RefreshCw className={`mr-2 h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
Refresh
</Button>
</div>
</CardHeader>
<CardContent className="p-0 sm:p-6 sm:pt-0">
<div className="rounded-md border overflow-x-auto overflow-y-auto max-h-[650px] relative">
<Table className="w-full min-w-[1000px] table-fixed border-separate border-spacing-0">
<TableHeader className="sticky top-0 z-20 bg-background shadow-sm">
<TableRow>
<TableHead className="w-[150px] font-bold bg-background">Requested Date</TableHead>
<TableHead className="w-[220px] font-bold bg-background">Patient Details</TableHead>
<TableHead className="w-[250px] font-bold bg-background">Requested Package</TableHead>
<TableHead className="w-[120px] font-bold bg-background">Age/Gender</TableHead>
<TableHead className="w-[250px] font-bold bg-background">Message</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={5} className="text-center py-10">
<Loader2 className="h-8 w-8 animate-spin mx-auto" />
</TableCell>
</TableRow>
) : inquiries.length === 0 ? (
<TableRow>
<TableCell colSpan={5} className="text-center text-muted-foreground py-10">
No inquiries found for the selected criteria
</TableCell>
</TableRow>
) : (
inquiries.map((inq) => (
<TableRow key={inq.id} className="hover:bg-muted/50">
<TableCell>
<div className="font-semibold text-primary">
{new Date(inq.preferredDate).toLocaleDateString()}
</div>
<div className="text-[11px] text-muted-foreground mt-1">
Submitted: {new Date(inq.createdAt).toLocaleDateString()}
</div>
</TableCell>
<TableCell>
<div className="font-semibold text-base">{inq.fullName}</div>
<div className="text-sm">{inq.mobileNumber}</div>
<div className="text-xs text-muted-foreground">{inq.email || '-'}</div>
</TableCell>
<TableCell>
<div className="font-semibold text-sm truncate">{inq.healthPackage?.name || 'N/A'}</div>
</TableCell>
<TableCell>
<div className="font-medium">
{inq.age} yrs / {inq.gender}
</div>
</TableCell>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="text-sm italic line-clamp-3 text-muted-foreground whitespace-pre-wrap cursor-pointer">
{inq.message || 'No message provided.'}
</div>
</TooltipTrigger>
<TooltipContent className="max-w-md whitespace-pre-wrap">
{inq.message || 'No message provided.'}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{!loading && totalItems > 0 && (
<div className="flex flex-col sm:flex-row items-center justify-between px-2 py-4 border-t gap-4 mt-2">
<div className="text-sm text-muted-foreground">
Showing <span className="font-semibold">{indexOfFirstItem + 1}</span> to{' '}
<span className="font-semibold">{indexOfLastItem}</span> of{' '}
<span className="font-semibold">{totalItems}</span> inquiries
</div>
<div className="flex items-center gap-6">
<div className="text-sm font-semibold">
Page {currentPage} of {totalPages || 1}
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="icon"
className="h-9 w-9"
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
className="h-9 w-9"
onClick={() => setCurrentPage((prev) => Math.min(prev + 1, totalPages))}
disabled={currentPage === totalPages || totalPages === 0}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
</div>
)}
</CardContent>
</Card>
);
}
@@ -1,5 +1,5 @@
import {Navigate} from "react-router-dom";
import {useAuth} from "@/context/AuthContext";
import { Navigate } from 'react-router-dom';
import { useAuth } from '@/context/AuthContext';
export default function ProtectedRoute({ children }: any) {
const { token } = useAuth();
@@ -0,0 +1,186 @@
import { BytescaleUploader } from '@/components/BytescaleUploader/BytescaleUploader';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { X } from 'lucide-react';
interface SeoData {
seoTitle?: string;
metaDescription?: string;
focusKeyphrase?: string;
tags?: string[];
ogTitle?: string;
ogDescription?: string;
ogImage?: string;
}
interface SeoFieldsProps {
value?: SeoData;
onChange: (seo: SeoData) => void;
slug?: string;
folderPath?: '/seo';
}
export default function SeoFields({ value, onChange, slug, folderPath = '/seo' }: SeoFieldsProps) {
const seo = value || {};
const updateSeo = (field: keyof SeoData, fieldValue: any) => {
onChange({
...seo,
[field]: fieldValue,
});
};
const removeTag = (index: number) => {
updateSeo(
'tags',
(seo.tags || []).filter((_, i) => i !== index)
);
};
return (
<div className="space-y-5 p-5 border rounded-xl bg-muted/20">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-bold">SEO Settings</h3>
<p className="text-sm text-muted-foreground">Optimize for Google & social sharing</p>
</div>
<Badge variant="secondary">Optional</Badge>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-sm font-semibold">SEO Title</Label>
<span className="text-xs text-muted-foreground">{seo.seoTitle?.length || 0}/60</span>
</div>
<Input
placeholder="Best Health Checkup Package in Kochi"
value={seo.seoTitle || ''}
onChange={(e) => updateSeo('seoTitle', e.target.value)}
/>
<p className="text-xs text-muted-foreground">Recommended: 5060 characters</p>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-sm font-semibold">Meta Description</Label>
<span className="text-xs text-muted-foreground">{seo.metaDescription?.length || 0}/160</span>
</div>
<Textarea
rows={4}
placeholder="Short description shown in Google search results"
value={seo.metaDescription || ''}
onChange={(e) => updateSeo('metaDescription', e.target.value)}
/>
<p className="text-xs text-muted-foreground">Recommended: 150160 characters</p>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">Focus Keyphrase</Label>
<Input
placeholder="health checkup package kochi"
value={seo.focusKeyphrase || ''}
onChange={(e) => updateSeo('focusKeyphrase', e.target.value)}
/>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">Tags / Keywords</Label>
<div className="flex flex-wrap gap-2 border rounded-md p-3 min-h-[48px] bg-background">
{(seo.tags || []).map((tag, index) => (
<div
key={index}
className="bg-primary/10 text-primary px-3 py-1 rounded-full text-sm flex items-center gap-2"
>
<span>{tag}</span>
<button type="button" onClick={() => removeTag(index)} className="hover:text-red-500 transition-colors">
<X className="h-3 w-3" />
</button>
</div>
))}
<Input
placeholder="Type keyword and press Enter"
className="border-0 shadow-none focus-visible:ring-0 min-w-[220px] flex-1"
onKeyDown={(e) => {
if (e.key === 'Enter' && e.currentTarget.value.trim()) {
e.preventDefault();
const newTag = e.currentTarget.value.trim();
if (!(seo.tags || []).includes(newTag)) {
updateSeo('tags', [...(seo.tags || []), newTag]);
}
e.currentTarget.value = '';
}
}}
/>
</div>
<p className="text-xs text-muted-foreground">Press Enter to add tags</p>
</div>
<div className="border-t pt-5 space-y-5">
<div className="flex items-center justify-between">
<div>
<h4 className="font-bold">Open Graph (Social Preview)</h4>
<p className="text-sm text-muted-foreground">Facebook, WhatsApp & Twitter sharing</p>
</div>
<Badge variant="secondary">Optional</Badge>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">OG Title</Label>
<Input
placeholder="Title for social sharing"
value={seo.ogTitle || ''}
onChange={(e) => updateSeo('ogTitle', e.target.value)}
/>
<p className="text-xs text-muted-foreground">If empty, SEO title will be used</p>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">OG Description</Label>
<Textarea
rows={4}
placeholder="Description for social sharing"
value={seo.ogDescription || ''}
onChange={(e) => updateSeo('ogDescription', e.target.value)}
/>
<p className="text-xs text-muted-foreground">If empty, meta description will be used</p>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">OG Image</Label>
<BytescaleUploader
value={seo.ogImage || ''}
folderPath={folderPath}
onChange={(url) => updateSeo('ogImage', url)}
/>
</div>
</div>
</div>
);
}
@@ -0,0 +1,113 @@
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
interface SeoPreviewData {
seo?: {
ogImage?: string;
ogTitle?: string;
seoTitle?: string;
ogDescription?: string;
metaDescription?: string;
slug?: string;
};
doctorId?: string;
name?: string;
}
interface SeoPreviewProps {
open: boolean;
onOpenChange: (open: boolean) => void;
previewData?: SeoPreviewData | null;
url?: string;
title?: string;
}
export default function SeoPreview({ open, onOpenChange, previewData, url, title = 'SEO Preview' }: SeoPreviewProps) {
const previewUrl = url || 'https://www.gg-hospital.com';
const hasSeoData =
!!previewData?.seo &&
!!(
previewData.seo.ogImage ||
previewData.seo.ogTitle ||
previewData.seo.seoTitle ||
previewData.seo.ogDescription ||
previewData.seo.metaDescription
);
const imageUrl = previewData?.seo?.ogImage || 'https://placehold.co/1200x630?text=GG+Hospital';
const ogTitle = previewData?.seo?.ogTitle || previewData?.seo?.seoTitle || 'GG Hospital';
const ogDescription =
previewData?.seo?.ogDescription || previewData?.seo?.metaDescription || 'No description available';
const searchTitle = previewData?.seo?.seoTitle || previewData?.seo?.ogTitle || 'SEO title preview';
const searchDescription =
previewData?.seo?.metaDescription || previewData?.seo?.ogDescription || 'No meta description available';
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:!max-w-4xl overflow-hidden">
<DialogHeader>
<DialogTitle className="text-xl">{title}</DialogTitle>
</DialogHeader>
{hasSeoData ? (
<div className="space-y-10 py-2">
{/* Social Preview */}
<div>
<p className="mb-4 text-sm font-semibold text-muted-foreground">
Social Media Preview (WhatsApp / Facebook)
</p>
<a
href={previewUrl}
target="_blank"
rel="noopener noreferrer"
className="block max-w-[560px] overflow-hidden rounded-xl border bg-white shadow-sm transition hover:shadow-md"
>
<div className="aspect-[1.91/1] overflow-hidden bg-muted">
<img src={imageUrl} alt="OG Preview" className="h-full w-full object-cover" />
</div>
<div className="border-t bg-[#f0f2f5] px-4 py-3">
<h3 className="mt-1 line-clamp-2 text-[18px] font-semibold leading-snug text-[#1c1e21]">{ogTitle}</h3>
<p className="mt-1 line-clamp-2 text-[14px] text-[#65676b]">{ogDescription}</p>
<p className="mt-1 truncate text-[11px] tracking-wide text-[#65676b]">{previewUrl}</p>
</div>
</a>
</div>
{/* Google Preview */}
<div>
<p className="mb-4 text-sm font-semibold text-muted-foreground">Google Search Preview</p>
<div className="rounded-xl border bg-white p-6">
<a href={previewUrl} target="_blank" rel="noopener noreferrer" className="block">
<p className="truncate text-[14px] text-[#202124] hover:underline">{previewUrl}</p>
<h3 className="mt-1 text-[22px] leading-tight text-[#1a0dab] hover:underline">{searchTitle}</h3>
</a>
<p className="mt-2 line-clamp-3 text-[14px] leading-6 text-[#4d5156]">{searchDescription}</p>
</div>
</div>
</div>
) : (
<div className="flex items-center justify-center py-16 text-sm text-muted-foreground">
No preview data available.
</div>
)}
<DialogFooter className="mt-0 border-t bg-background p-6">
<Button variant="outline" onClick={() => onOpenChange(false)}>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+7 -7
View File
@@ -1,16 +1,16 @@
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";
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");
if (darkMode) document.documentElement.classList.add('dark');
else document.documentElement.classList.remove('dark');
}, [darkMode]);
return (
+28 -26
View File
@@ -1,51 +1,55 @@
import { Link, useLocation } from "react-router-dom";
import { Link, useLocation } from 'react-router-dom';
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
export default function Sidebar() {
const location = useLocation();
const navItems = [
{
name: "Department",
path: "/department",
name: 'Department',
path: '/department',
},
{
name: "Doctor",
path: "/doctor",
name: 'Doctor',
path: '/doctor',
},
{
name: "Appointments",
path: "/appointment",
name: 'Health Check',
path: '/health-check',
},
{
name: "Career",
path: "/career",
name: 'Appointments',
path: '/appointment',
},
{
name: "Candidates",
path: "/candidate",
name: 'Career',
path: '/career',
},
{
name: "Inquiry",
path: "/inquiry",
name: 'Candidates',
path: '/candidate',
},
{
name: "Academics & Research",
path: "/academics",
name: 'Inquiry',
path: '/inquiry',
},
{
name: "News & Media",
path: "/news",
name: 'Academics & Research',
path: '/academics',
},
{
name: "Email",
path: "/email",
name: 'News & Media',
path: '/news',
},
{
name: "Blog",
path: "/blog",
name: 'Email',
path: '/email',
},
{
name: 'Blog',
path: '/blog',
},
];
@@ -63,9 +67,7 @@ export default function Sidebar() {
return (
<Link key={item.path} to={item.path}>
<Button
variant={active ? "secondary" : "ghost"}
className="w-full justify-start">
<Button variant={active ? 'secondary' : 'ghost'} className="w-full justify-start">
{item.name}
</Button>
</Link>
+61
View File
@@ -0,0 +1,61 @@
import * as React from 'react';
import { Accordion as AccordionPrimitive } from 'radix-ui';
import { cn } from '@/lib/utils';
import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react';
function Accordion({ className, ...props }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" className={cn('flex w-full flex-col', className)} {...props} />;
}
function AccordionItem({ className, ...props }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item data-slot="accordion-item" className={cn('not-last:border-b', className)} {...props} />
);
}
function AccordionTrigger({ className, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
'group/accordion-trigger relative flex flex-1 items-start justify-between rounded-lg border border-transparent py-2.5 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:after:border-ring disabled:pointer-events-none disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 **:data-[slot=accordion-trigger-icon]:text-muted-foreground',
className
)}
{...props}
>
{children}
<ChevronDownIcon
data-slot="accordion-trigger-icon"
className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden"
/>
<ChevronUpIcon
data-slot="accordion-trigger-icon"
className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline"
/>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
);
}
function AccordionContent({ className, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="overflow-hidden text-sm data-open:animate-accordion-down data-closed:animate-accordion-up"
{...props}
>
<div
className={cn(
'h-(--radix-accordion-content-height) pt-0 pb-2.5 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4',
className
)}
>
{children}
</div>
</AccordionPrimitive.Content>
);
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
+19 -28
View File
@@ -1,49 +1,40 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { Slot } from 'radix-ui';
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils';
const badgeVariants = cva(
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
'group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!',
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
secondary:
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
secondary: 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80',
destructive:
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
outline:
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
ghost:
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
link: "text-primary underline-offset-4 hover:underline",
'bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20',
outline: 'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground',
ghost: 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50',
link: 'text-primary underline-offset-4 hover:underline',
},
},
defaultVariants: {
variant: "default",
variant: 'default',
},
}
)
);
function Badge({
className,
variant = "default",
variant = 'default',
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "span"
}: React.ComponentProps<'span'> & VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : 'span';
return (
<Comp
data-slot="badge"
data-variant={variant}
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
<Comp data-slot="badge" data-variant={variant} className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
export { Badge, badgeVariants }
export { Badge, badgeVariants };
+27 -29
View File
@@ -1,57 +1,55 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { Slot } from 'radix-ui';
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils';
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
outline:
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
ghost:
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
destructive:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline",
'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
default: 'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
icon: "size-8",
"icon-xs":
lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
icon: 'size-8',
'icon-xs':
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
"icon-sm":
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
"icon-lg": "size-9",
'icon-sm': 'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
'icon-lg': 'size-9',
},
},
defaultVariants: {
variant: "default",
size: "default",
variant: 'default',
size: 'default',
},
}
)
);
function Button({
className,
variant = "default",
size = "default",
variant = 'default',
size = 'default',
asChild = false,
...props
}: React.ComponentProps<"button"> &
}: React.ComponentProps<'button'> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
asChild?: boolean;
}) {
const Comp = asChild ? Slot.Root : "button"
const Comp = asChild ? Slot.Root : 'button';
return (
<Comp
@@ -61,7 +59,7 @@ function Button({
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
);
}
export { Button, buttonVariants }
export { Button, buttonVariants };
+22 -55
View File
@@ -1,103 +1,70 @@
import * as React from "react"
import * as React from 'react';
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils';
function Card({
className,
size = "default",
...props
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
function Card({ className, size = 'default', ...props }: React.ComponentProps<'div'> & { size?: 'default' | 'sm' }) {
return (
<div
data-slot="card"
data-size={size}
className={cn(
"group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
'group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl',
className
)}
{...props}
/>
)
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-header"
className={cn(
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
'group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3',
className
)}
{...props}
/>
)
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-title"
className={cn(
"text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
className
)}
className={cn('text-base leading-snug font-medium group-data-[size=sm]/card:text-sm', className)}
{...props}
/>
)
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
return <div data-slot="card-description" className={cn('text-sm text-muted-foreground', className)} {...props} />;
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
className={cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', className)}
{...props}
/>
)
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
{...props}
/>
)
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
return <div data-slot="card-content" className={cn('px-4 group-data-[size=sm]/card:px-3', className)} {...props} />;
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-footer"
className={cn(
"flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3",
className
)}
className={cn('flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3', className)}
{...props}
/>
)
);
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}
export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };
+38 -81
View File
@@ -1,48 +1,36 @@
import * as React from "react"
import { Command as CommandPrimitive } from "cmdk"
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"
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>) {
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",
'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...",
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
title?: string;
description?: string;
className?: string;
showCloseButton?: boolean;
}) {
return (
<Dialog {...props}>
@@ -51,31 +39,22 @@ function CommandDialog({
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent
className={cn(
"top-1/3 translate-y-0 overflow-hidden rounded-xl! p-0",
className
)}
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>) {
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
)}
className={cn('w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50', className)}
{...props}
/>
<InputGroupAddon>
@@ -83,72 +62,53 @@ function CommandInput({
</InputGroupAddon>
</InputGroup>
</div>
)
);
}
function CommandList({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
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
)}
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>) {
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)}
className={cn('py-6 text-center text-sm', className)}
{...props}
/>
)
);
}
function CommandGroup({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
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",
'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>) {
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)}
className={cn('-mx-1 h-px bg-border', className)}
{...props}
/>
)
);
}
function CommandItem({
className,
children,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
function CommandItem({ className, children, ...props }: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
@@ -161,23 +121,20 @@ function CommandItem({
{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">) {
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",
'ml-auto text-xs tracking-widest text-muted-foreground group-data-selected/command-item:text-foreground',
className
)}
{...props}
/>
)
);
}
export {
@@ -190,4 +147,4 @@ export {
CommandItem,
CommandShortcut,
CommandSeparator,
}
};
+34 -62
View File
@@ -1,48 +1,37 @@
import * as React from "react"
import { Dialog as DialogPrimitive } from "radix-ui"
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"
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 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 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 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 DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
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",
'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({
@@ -51,7 +40,7 @@ function DialogContent({
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
showCloseButton?: boolean;
}) {
return (
<DialogPortal>
@@ -59,7 +48,7 @@ function DialogContent({
<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",
'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-popover p-4 text-sm text-popover-foreground 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}
@@ -67,30 +56,19 @@ function DialogContent({
{children}
{showCloseButton && (
<DialogPrimitive.Close data-slot="dialog-close" asChild>
<Button
variant="ghost"
className="absolute top-2 right-2"
size="icon-sm"
>
<XIcon
/>
<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 DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
return <div data-slot="dialog-header" className={cn('flex flex-col gap-2', className)} {...props} />;
}
function DialogFooter({
@@ -98,14 +76,14 @@ function DialogFooter({
showCloseButton = false,
children,
...props
}: React.ComponentProps<"div"> & {
showCloseButton?: boolean
}: 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",
'-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}
@@ -117,36 +95,30 @@ function DialogFooter({
</DialogPrimitive.Close>
)}
</div>
)
);
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
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)}
className={cn('text-base leading-none font-medium', className)}
{...props}
/>
)
);
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
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",
'text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground',
className
)}
{...props}
/>
)
);
}
export {
@@ -160,4 +132,4 @@ export {
DialogPortal,
DialogTitle,
DialogTrigger,
}
};
+43 -64
View File
@@ -1,25 +1,25 @@
"use client"
'use client';
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
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"
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">) {
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",
'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(
@@ -27,27 +27,24 @@ const inputGroupAddonVariants = cva(
{
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",
'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",
align: 'inline-start',
},
}
)
);
function InputGroupAddon({
className,
align = "inline-start",
align = 'inline-start',
...props
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
}: React.ComponentProps<'div'> & VariantProps<typeof inputGroupAddonVariants>) {
return (
<div
role="group"
@@ -55,42 +52,37 @@ function InputGroupAddon({
data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => {
if ((e.target as HTMLElement).closest("button")) {
return
if ((e.target as HTMLElement).closest('button')) {
return;
}
e.currentTarget.parentElement?.querySelector("input")?.focus()
e.currentTarget.parentElement?.querySelector('input')?.focus();
}}
{...props}
/>
)
);
}
const inputGroupButtonVariants = cva(
"flex items-center gap-2 text-sm shadow-none",
{
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",
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",
size: 'xs',
},
}
)
});
function InputGroupButton({
className,
type = "button",
variant = "ghost",
size = "xs",
type = 'button',
variant = 'ghost',
size = 'xs',
...props
}: Omit<React.ComponentProps<typeof Button>, "size"> &
VariantProps<typeof inputGroupButtonVariants>) {
}: Omit<React.ComponentProps<typeof Button>, 'size'> & VariantProps<typeof inputGroupButtonVariants>) {
return (
<Button
type={type}
@@ -99,10 +91,10 @@ function InputGroupButton({
className={cn(inputGroupButtonVariants({ size }), className)}
{...props}
/>
)
);
}
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {
return (
<span
className={cn(
@@ -111,46 +103,33 @@ function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
)}
{...props}
/>
)
);
}
function InputGroupInput({
className,
...props
}: React.ComponentProps<"input">) {
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",
'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">) {
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",
'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,
}
export { InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupInput, InputGroupTextarea };
+6 -6
View File
@@ -1,19 +1,19 @@
import * as React from "react"
import * as React from 'react';
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils';
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
return (
<input
type={type}
data-slot="input"
className={cn(
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none 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",
'h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none 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 { Input }
export { Input };
+7 -10
View File
@@ -1,22 +1,19 @@
import * as React from "react"
import { Label as LabelPrimitive } from "radix-ui"
import * as React from 'react';
import { Label as LabelPrimitive } from 'radix-ui';
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils';
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
className
)}
{...props}
/>
)
);
}
export { Label }
export { Label };
+19 -54
View File
@@ -1,23 +1,19 @@
import * as React from "react"
import { Popover as PopoverPrimitive } from "radix-ui"
import * as React from 'react';
import { Popover as PopoverPrimitive } from 'radix-ui';
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils';
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
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 PopoverTrigger({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
}
function PopoverContent({
className,
align = "center",
align = 'center',
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
@@ -28,60 +24,29 @@ function PopoverContent({
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",
'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 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 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 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}
/>
)
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,
}
export { Popover, PopoverAnchor, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger };
+10 -18
View File
@@ -1,19 +1,11 @@
import * as React from "react"
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"
import * as React from 'react';
import { ScrollArea as ScrollAreaPrimitive } from 'radix-ui';
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils';
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
function ScrollArea({ className, children, ...props }: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<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"
@@ -23,12 +15,12 @@ function ScrollArea({
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
);
}
function ScrollBar({
className,
orientation = "vertical",
orientation = 'vertical',
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
@@ -37,7 +29,7 @@ function 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",
'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}
@@ -47,7 +39,7 @@ function ScrollBar({
className="relative flex-1 rounded-full bg-border"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
);
}
export { ScrollArea, ScrollBar }
export { ScrollArea, ScrollBar };
+167
View File
@@ -0,0 +1,167 @@
import * as React from 'react';
import { Select as SelectPrimitive } from 'radix-ui';
import { cn } from '@/lib/utils';
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from 'lucide-react';
function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />;
}
function SelectGroup({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" className={cn('scroll-my-1 p-1', className)} {...props} />;
}
function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
}
function SelectTrigger({
className,
size = 'default',
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: 'sm' | 'default';
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
}
function SelectContent({
className,
children,
position = 'item-aligned',
align = 'center',
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
data-align-trigger={position === 'item-aligned'}
className={cn(
'relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none 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',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className
)}
position={position}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
data-position={position}
className={cn(
'data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)',
position === 'popper' && ''
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
);
}
function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn('px-1.5 py-1 text-xs text-muted-foreground', className)}
{...props}
/>
);
}
function SelectItem({ className, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="pointer-events-none" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
}
function SelectSeparator({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn('pointer-events-none -mx-1 my-1 h-px bg-border', className)}
{...props}
/>
);
}
function SelectScrollUpButton({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<ChevronUpIcon />
</SelectPrimitive.ScrollUpButton>
);
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownButton>
);
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
};
+7 -7
View File
@@ -1,11 +1,11 @@
import * as React from "react"
import { Separator as SeparatorPrimitive } from "radix-ui"
import * as React from 'react';
import { Separator as SeparatorPrimitive } from 'radix-ui';
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils';
function Separator({
className,
orientation = "horizontal",
orientation = 'horizontal',
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
@@ -15,12 +15,12 @@ function Separator({
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
'shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch',
className
)}
{...props}
/>
)
);
}
export { Separator }
export { Separator };
+7 -7
View File
@@ -1,22 +1,22 @@
import * as React from "react";
import {Switch as SwitchPrimitive} from "radix-ui";
import * as React from 'react';
import { Switch as SwitchPrimitive } from 'radix-ui';
import {cn} from "@/lib/utils";
import { cn } from '@/lib/utils';
function Switch({
className,
size = "default",
size = 'default',
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root> & {
size?: "sm" | "default";
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,
'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}
>
+26 -70
View File
@@ -1,114 +1,70 @@
import * as React from "react"
import * as React from 'react';
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils';
function Table({ className, ...props }: React.ComponentProps<"table">) {
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 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 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 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">) {
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
)}
className={cn('border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', className)}
{...props}
/>
)
);
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
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
)}
className={cn('border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted', className)}
{...props}
/>
)
);
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
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",
'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">) {
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
)}
className={cn('p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0', className)}
{...props}
/>
)
);
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
function TableCaption({ className, ...props }: React.ComponentProps<'caption'>) {
return (
<caption
data-slot="table-caption"
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
)
<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,
}
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
+74
View File
@@ -0,0 +1,74 @@
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { Tabs as TabsPrimitive } from 'radix-ui';
import { cn } from '@/lib/utils';
function Tabs({ className, orientation = 'horizontal', ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
data-orientation={orientation}
className={cn('group/tabs flex gap-2 data-horizontal:flex-col', className)}
{...props}
/>
);
}
const tabsListVariants = cva(
'group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none',
{
variants: {
variant: {
default: 'bg-muted',
line: 'gap-1 bg-transparent',
},
},
defaultVariants: {
variant: 'default',
},
}
);
function TabsList({
className,
variant = 'default',
...props
}: React.ComponentProps<typeof TabsPrimitive.List> & VariantProps<typeof tabsListVariants>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
data-variant={variant}
className={cn(tabsListVariants({ variant }), className)}
{...props}
/>
);
}
function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent',
'data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground',
'after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100',
className
)}
{...props}
/>
);
}
function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn('flex-1 text-sm outline-none', className)}
{...props}
/>
);
}
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };
+6 -6
View File
@@ -1,18 +1,18 @@
import * as React from "react"
import * as React from 'react';
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils';
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
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",
'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 }
export { Textarea };
+42
View File
@@ -0,0 +1,42 @@
import * as React from 'react';
import { Tooltip as TooltipPrimitive } from 'radix-ui';
import { cn } from '@/lib/utils';
function TooltipProvider({ delayDuration = 0, ...props }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return <TooltipPrimitive.Provider data-slot="tooltip-provider" delayDuration={delayDuration} {...props} />;
}
function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />;
}
function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
'z-50 inline-flex w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 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-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 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}
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
);
}
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
+7 -9
View File
@@ -1,5 +1,5 @@
import {createContext, useContext, useState} from "react";
import api from "@/services/api";
import { createContext, useContext, useState } from 'react';
import api from '@/services/api';
type AuthContextType = {
user: any;
@@ -11,27 +11,25 @@ type AuthContextType = {
const AuthContext = createContext<AuthContextType | null>(null);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [token, setToken] = useState<string | null>(
localStorage.getItem("token"),
);
const [token, setToken] = useState<string | null>(localStorage.getItem('token'));
const [user, setUser] = useState(null);
async function login(username: string, password: string) {
const response = await api.post("/auth/login", {
const response = await api.post('/auth/login', {
username,
password,
});
const token = response.data.token;
localStorage.setItem("token", token);
localStorage.setItem('token', token);
setToken(token);
}
function logout() {
localStorage.removeItem("token");
localStorage.removeItem('token');
setToken(null);
setUser(null);
@@ -55,7 +53,7 @@ export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used inside AuthProvider");
throw new Error('useAuth must be used inside AuthProvider');
}
return context;
+3 -3
View File
@@ -1,6 +1,6 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@import 'tailwindcss';
@import 'tw-animate-css';
@import 'shadcn/tailwind.css';
@custom-variant dark (&:is(.dark *));

Some files were not shown because too many files have changed in this diff Show More