Compare commits

..

3 Commits

12 changed files with 747 additions and 5 deletions
@@ -0,0 +1,37 @@
/*
Warnings:
- You are about to drop the `HealthCheckCategory` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `HealthPackage` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `HealthPackageInquiry` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "HealthPackage" DROP CONSTRAINT "HealthPackage_categoryId_fkey";
-- DropForeignKey
ALTER TABLE "HealthPackageInquiry" DROP CONSTRAINT "HealthPackageInquiry_packageId_fkey";
-- DropTable
DROP TABLE "HealthCheckCategory";
-- DropTable
DROP TABLE "HealthPackage";
-- DropTable
DROP TABLE "HealthPackageInquiry";
-- 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")
);
-- 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 "professionalSummary" TEXT;
@@ -0,0 +1,7 @@
-- AlterTable
ALTER TABLE "Doctor" ADD COLUMN "experience" INTEGER,
ADD COLUMN "focusKeyphrase" TEXT,
ADD COLUMN "metaDescription" TEXT,
ADD COLUMN "seoTitle" TEXT,
ADD COLUMN "slug" TEXT,
ADD COLUMN "tags" TEXT[];
@@ -0,0 +1,4 @@
-- AlterTable
ALTER TABLE "Doctor" ADD COLUMN "ogDescription" TEXT,
ADD COLUMN "ogImage" TEXT,
ADD COLUMN "ogTitle" TEXT;
@@ -0,0 +1,19 @@
-- 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");
@@ -0,0 +1,30 @@
/*
Warnings:
- You are about to drop the column `focusKeyphrase` on the `Doctor` table. All the data in the column will be lost.
- You are about to drop the column `metaDescription` on the `Doctor` table. All the data in the column will be lost.
- You are about to drop the column `ogDescription` on the `Doctor` table. All the data in the column will be lost.
- You are about to drop the column `ogImage` on the `Doctor` table. All the data in the column will be lost.
- You are about to drop the column `ogTitle` on the `Doctor` table. All the data in the column will be lost.
- You are about to drop the column `seoTitle` on the `Doctor` table. All the data in the column will be lost.
- You are about to drop the column `slug` on the `Doctor` table. All the data in the column will be lost.
- You are about to drop the column `tags` on the `Doctor` table. All the data in the column will be lost.
- 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" DROP COLUMN "focusKeyphrase",
DROP COLUMN "metaDescription",
DROP COLUMN "ogDescription",
DROP COLUMN "ogImage",
DROP COLUMN "ogTitle",
DROP COLUMN "seoTitle",
DROP COLUMN "slug",
DROP COLUMN "tags",
ADD COLUMN "seoId" INTEGER;
-- 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;
@@ -0,0 +1,66 @@
-- CreateTable
CREATE TABLE "HealthCheckCategory" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT,
"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),
"image" TEXT,
"discountedPrice" DECIMAL(10,2),
"inclusions" JSONB NOT NULL DEFAULT '{}',
"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,
"age" INTEGER,
"gender" 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;
+32 -3
View File
@@ -23,14 +23,17 @@ model Doctor {
name String name String
image String? image String?
designation String? designation String?
experience Int?
workingStatus String? workingStatus String?
qualification String? qualification String?
isActive Boolean @default(true) isActive Boolean @default(true)
globalSortOrder Int @default(1000) globalSortOrder Int @default(1000)
departments DoctorDepartment[] departments DoctorDepartment[]
appointments Appointment[] appointments Appointment[]
specializations DoctorSpecialization[]
professionalSummary String? @db.Text
seoId Int? @unique
seo Seo? @relation(fields: [seoId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
@@ -220,7 +223,6 @@ model NewsImage {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
} }
model HealthCheckCategory { model HealthCheckCategory {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
name String @unique name String @unique
@@ -276,3 +278,30 @@ model HealthPackageInquiry {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt 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?
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
}
@@ -9,12 +9,18 @@ export const getAllDoctors = async (req, res) => {
const doctors = await prisma.doctor.findMany({ const doctors = await prisma.doctor.findMany({
where: admin === "true" ? {} : {isActive: true}, where: admin === "true" ? {} : {isActive: true},
include: { include: {
seo: true,
departments: { departments: {
include: { include: {
department: true, department: true,
timing: true, timing: true,
}, },
}, },
specializations: {
orderBy: {
createdAt: "asc",
},
},
}, },
orderBy: [{globalSortOrder: "asc"}, {name: "asc"}], orderBy: [{globalSortOrder: "asc"}, {name: "asc"}],
}); });
@@ -28,7 +34,25 @@ export const getAllDoctors = async (req, res) => {
workingStatus: doc.workingStatus, workingStatus: doc.workingStatus,
qualification: doc.qualification, qualification: doc.qualification,
isActive: doc.isActive, isActive: doc.isActive,
experience: doc.experience,
professionalSummary: doc.professionalSummary,
globalSortOrder: doc.globalSortOrder, 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) => { departments: doc.departments.map((d) => {
const t = d.timing || {}; const t = d.timing || {};
const timingArray = [ const timingArray = [
@@ -73,6 +97,8 @@ export const getDoctorByDoctorId = async (req, res) => {
const doctor = await prisma.doctor.findUnique({ const doctor = await prisma.doctor.findUnique({
where: {doctorId}, where: {doctorId},
include: { include: {
seo: true,
specializations: true,
departments: { departments: {
include: { include: {
department: true, department: true,
@@ -96,6 +122,24 @@ export const getDoctorByDoctorId = async (req, res) => {
designation: doctor.designation, designation: doctor.designation,
workingStatus: doctor.workingStatus, workingStatus: doctor.workingStatus,
qualification: doctor.qualification, 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) => ({ departments: doctor.departments.map((d) => ({
departmentId: d.department.departmentId, departmentId: d.department.departmentId,
departmentName: d.department.name, departmentName: d.department.name,
@@ -184,7 +228,32 @@ export const createDoctor = async (req, res) => {
isActive, isActive,
globalSortOrder, globalSortOrder,
departments, departments,
experience,
professionalSummary,
seoTitle,
metaDescription,
focusKeyphrase,
slug,
tags,
specializations,
ogTitle,
ogDescription,
ogImage,
} = req.body; } = req.body;
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({ const doctor = await prisma.doctor.create({
data: { data: {
@@ -194,6 +263,9 @@ export const createDoctor = async (req, res) => {
designation, designation,
workingStatus, workingStatus,
qualification, qualification,
experience: experience ? Number(experience) : null,
professionalSummary,
seoId: seo.id,
isActive: isActive !== undefined ? isActive : true, isActive: isActive !== undefined ? isActive : true,
globalSortOrder: globalSortOrder:
globalSortOrder !== undefined ? Number(globalSortOrder) : 0, globalSortOrder !== undefined ? Number(globalSortOrder) : 0,
@@ -224,6 +296,17 @@ 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({ res.status(201).json({
success: true, success: true,
@@ -251,6 +334,14 @@ export const updateDoctor = async (req, res) => {
isActive, isActive,
globalSortOrder, globalSortOrder,
departments, departments,
experience,
professionalSummary,
seoTitle,
metaDescription,
focusKeyphrase,
slug,
tags,
specializations,
} = req.body; } = req.body;
const doctor = await prisma.doctor.findUnique({where: {doctorId}}); const doctor = await prisma.doctor.findUnique({where: {doctorId}});
@@ -268,15 +359,71 @@ export const updateDoctor = async (req, res) => {
workingStatus, workingStatus,
qualification, qualification,
isActive, isActive,
experience: experience ? Number(experience) : null,
professionalSummary,
globalSortOrder: globalSortOrder:
globalSortOrder !== undefined ? Number(globalSortOrder) : undefined, globalSortOrder !== undefined ? Number(globalSortOrder) : undefined,
}, },
}); });
if (doctor.seoId) {
await prisma.seo.update({
where: {
id: doctor.seoId,
},
data: {
seoTitle,
metaDescription,
focusKeyphrase,
slug: slug ? slug : null,
tags: tags || [],
},
});
} else {
const seo = await prisma.seo.create({
data: {
seoTitle,
metaDescription,
focusKeyphrase,
slug: slug ? slug : null,
tags: tags || [],
},
});
await prisma.doctor.update({
where: {
id: doctor.id,
},
data: {
seoId: seo.id,
},
});
}
const hasTimingData = departments?.some( const hasTimingData = departments?.some(
(dep) => dep.timing && Object.keys(dep.timing).length > 0, (dep) => dep.timing && Object.keys(dep.timing).length > 0,
); );
// 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,
})),
});
}
}
if (departments && Array.isArray(departments) && hasTimingData) { if (departments && Array.isArray(departments) && hasTimingData) {
const oldRelations = await prisma.doctorDepartment.findMany({ const oldRelations = await prisma.doctorDepartment.findMany({
where: {doctorId: doctor.id}, where: {doctorId: doctor.id},
@@ -11,7 +11,8 @@ interface BytescaleUploaderProps {
| "/departments" | "/departments"
| "/news" | "/news"
| "/blog" | "/blog"
| "/health-packages"; | "/health-packages"
| "/doctor-og";
} }
export function BytescaleUploader({ export function BytescaleUploader({
+400
View File
@@ -41,6 +41,7 @@ import {
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
} from "lucide-react"; } from "lucide-react";
import { Textarea } from "@/components/ui/textarea";
interface Department { interface Department {
departmentId: string; departmentId: string;
@@ -83,6 +84,21 @@ export default function DoctorPage() {
isActive: true, isActive: true,
globalSortOrder: 0, globalSortOrder: 0,
departments: [], departments: [],
professionalSummary: "",
seoTitle: "",
metaDescription: "",
ogTitle: "",
ogDescription: "",
ogImage: "",
specializations: [
{
name: "",
description: "",
},
],
focusKeyphrase: "",
slug: "",
tags: [],
}); });
const fetchAll = useCallback(async () => { const fetchAll = useCallback(async () => {
@@ -221,9 +237,25 @@ export default function DoctorPage() {
designation: "", designation: "",
workingStatus: "", workingStatus: "",
qualification: "", qualification: "",
experience: "",
professionalSummary: "",
isActive: true, isActive: true,
globalSortOrder: 0, globalSortOrder: 0,
specializations: [
{
name: "",
description: "",
},
],
departments: [], departments: [],
seoTitle: "",
metaDescription: "",
focusKeyphrase: "",
slug: "",
tags: [],
ogTitle: "",
ogDescription: "",
ogImage: "",
}); });
setOpenModal(true); setOpenModal(true);
} }
@@ -243,6 +275,27 @@ export default function DoctorPage() {
qualification: doc.qualification, qualification: doc.qualification,
isActive: doc.isActive ?? true, isActive: doc.isActive ?? true,
globalSortOrder: doc.globalSortOrder ?? 0, globalSortOrder: doc.globalSortOrder ?? 0,
experience: doc.experience || "",
professionalSummary: doc.professionalSummary || "",
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 || "",
specializations: doc.specializations?.length
? doc.specializations.map((item: any) => ({
name: item.name || "",
description: item.description || "",
}))
: [
{
name: "",
description: "",
},
],
departments: timingData.map((d: any) => ({ departments: timingData.map((d: any) => ({
departmentId: d.departmentId, departmentId: d.departmentId,
sortOrder: d.deptSortOrder ?? 0, sortOrder: d.deptSortOrder ?? 0,
@@ -255,6 +308,8 @@ export default function DoctorPage() {
} }
} }
console.log("Current form state:", form); // Debug log to check form state
async function handleSubmit() { async function handleSubmit() {
try { try {
if (editing) { if (editing) {
@@ -594,6 +649,24 @@ export default function DoctorPage() {
className="text-base" className="text-base"
/> />
</div> </div>
<div className="space-y-1">
<label className="text-sm font-semibold">
Brief Professional Summary
</label>
<Textarea
name="professionalSummary"
placeholder="Write a brief professional summary about the doctor..."
value={form.professionalSummary || ""}
onChange={(e) =>
setForm({
...form,
professionalSummary: e.target.value,
})
}
className="min-h-[120px] text-base"
/>
</div>
<div className="space-y-1"> <div className="space-y-1">
<label className="text-sm font-semibold"> <label className="text-sm font-semibold">
Qualification Qualification
@@ -606,6 +679,25 @@ export default function DoctorPage() {
className="text-base" className="text-base"
/> />
</div> </div>
<div className="space-y-1">
<label className="text-sm font-semibold">
Years of Experience
</label>
<Input
name="experience"
type="number"
min={0}
placeholder="e.g. 15"
value={form.experience || ""}
onChange={handleChange}
className="text-base"
/>
<p className="text-xs text-muted-foreground">
Enter total years of professional experience
</p>
</div>
</div> </div>
<div className="p-5 border rounded-md bg-muted/20"> <div className="p-5 border rounded-md bg-muted/20">
@@ -632,6 +724,314 @@ export default function DoctorPage() {
})} })}
</div> </div>
</div> </div>
<div className="space-y-4 p-5 border rounded-md bg-muted/20">
<div className="flex items-center justify-between">
<p className="text-base font-bold">Specializations</p>
<Button
type="button"
variant="outline"
size="sm"
onClick={() =>
setForm({
...form,
specializations: [
...(form.specializations || []),
{
name: "",
description: "",
},
],
})
}
>
+ Add
</Button>
</div>
{form.specializations?.length === 0 ? (
<p className="text-sm text-muted-foreground italic">
No specializations added
</p>
) : (
<div className="space-y-4">
{form.specializations?.map(
(specialization: any, index: number) => (
<div
key={index}
className="border rounded-lg p-4 space-y-3 bg-background"
>
<div className="flex items-center justify-between">
<p className="font-medium text-sm">
Specialization {index + 1}
</p>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => {
const updated = form.specializations.filter(
(_: any, i: number) => i !== index,
);
setForm({
...form,
specializations: updated,
});
}}
>
Remove
</Button>
</div>
<div className="space-y-2">
<Label>Name</Label>
<Input
placeholder="e.g. Cardiology"
value={specialization.name}
onChange={(e) => {
const updated = [...form.specializations];
updated[index].name = e.target.value;
setForm({
...form,
specializations: updated,
});
}}
/>
</div>
<div className="space-y-2">
<Label>Doctor-specific Description</Label>
<Textarea
placeholder="Describe how this doctor specializes in this area..."
value={specialization.description}
onChange={(e) => {
const updated = [...form.specializations];
updated[index].description = e.target.value;
setForm({
...form,
specializations: updated,
});
}}
/>
</div>
</div>
),
)}
</div>
)}
</div>
<div className="space-y-4 p-5 border rounded-md bg-muted/20">
<div className="flex items-center justify-between">
<p className="text-base font-bold">SEO Settings</p>
<Badge variant="secondary">Optional</Badge>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">SEO Title</Label>
<Input
name="seoTitle"
placeholder="Best Cardiologist in Kochi | Dr John Doe"
value={form.seoTitle || ""}
onChange={handleChange}
className="text-base"
/>
<p className="text-xs text-muted-foreground">
Title shown in Google search results
</p>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">
Meta Description
</Label>
<Textarea
name="metaDescription"
placeholder="Short description shown in Google search..."
value={form.metaDescription || ""}
onChange={(e) =>
setForm({
...form,
metaDescription: e.target.value,
})
}
className="min-h-[100px] text-base"
/>
<p className="text-xs text-muted-foreground">
Recommended: 150160 characters
</p>
</div>
<div className="border-t pt-5 space-y-4">
<div className="flex items-center justify-between">
<p className="text-base font-bold">
Open Graph (Social Preview)
</p>
<Badge variant="secondary">Optional</Badge>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">OG Title</Label>
<Input
name="ogTitle"
placeholder="Title for WhatsApp / Facebook sharing"
value={form.ogTitle || ""}
onChange={handleChange}
className="text-base"
/>
<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
name="ogDescription"
placeholder="Description for social sharing..."
value={form.ogDescription || ""}
onChange={(e) =>
setForm({
...form,
ogDescription: e.target.value,
})
}
className="min-h-[100px] text-base"
/>
<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={form.ogImage}
folderPath="/doctor-og"
onChange={(url) =>
setForm({
...form,
ogImage: url,
})
}
/>
</div>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">
Focus Keyphrase
</Label>
<Input
name="focusKeyphrase"
placeholder="best cardiologist in kochi"
value={form.focusKeyphrase || ""}
onChange={handleChange}
className="text-base"
/>
<p className="text-xs text-muted-foreground">
Main keyword people may search in Google
</p>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">URL Slug</Label>
<Input
name="slug"
placeholder="dr-john-doe"
value={form.slug || ""}
onChange={handleChange}
className="text-base"
/>
<p className="text-xs text-muted-foreground">
URL:
<span className="font-medium">
{" "}
/doctors/
{form.slug || "doctor-slug"}
</span>
</p>
</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">
{form.tags?.map((tag: string, index: number) => (
<div
key={index}
className="bg-primary/10 text-primary px-3 py-1 rounded-full text-sm flex items-center gap-2"
>
{tag}
<button
type="button"
onClick={() => {
const updated = form.tags.filter(
(_: string, i: number) => i !== index,
);
setForm({
...form,
tags: updated,
});
}}
>
×
</button>
</div>
))}
<Input
placeholder="Type keyword and press Enter"
className="border-0 shadow-none focus-visible:ring-0 min-w-[220px]"
onKeyDown={(e) => {
if (
e.key === "Enter" &&
e.currentTarget.value.trim()
) {
e.preventDefault();
setForm({
...form,
tags: [
...(form.tags || []),
e.currentTarget.value.trim(),
],
});
e.currentTarget.value = "";
}
}}
/>
</div>
</div>
</div>
</div> </div>
<div className="space-y-6"> <div className="space-y-6">
+1 -1
View File
@@ -689,7 +689,7 @@ export default function HealthPackagePage() {
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-sm font-semibold"> <Label className="text-sm font-semibold">
Package Image(Dimensions: 650w x 250h) Package Image
</Label> </Label>
<BytescaleUploader <BytescaleUploader