feat:add doc seo and content
This commit is contained in:
@@ -11,7 +11,8 @@ interface BytescaleUploaderProps {
|
||||
| "/departments"
|
||||
| "/news"
|
||||
| "/blog"
|
||||
| "/health-packages";
|
||||
| "/health-packages"
|
||||
| "/doctor-og";
|
||||
}
|
||||
|
||||
export function BytescaleUploader({
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
} from "lucide-react";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
interface Department {
|
||||
departmentId: string;
|
||||
@@ -83,6 +84,21 @@ export default function DoctorPage() {
|
||||
isActive: true,
|
||||
globalSortOrder: 0,
|
||||
departments: [],
|
||||
professionalSummary: "",
|
||||
seoTitle: "",
|
||||
metaDescription: "",
|
||||
ogTitle: "",
|
||||
ogDescription: "",
|
||||
ogImage: "",
|
||||
specializations: [
|
||||
{
|
||||
name: "",
|
||||
description: "",
|
||||
},
|
||||
],
|
||||
focusKeyphrase: "",
|
||||
slug: "",
|
||||
tags: [],
|
||||
});
|
||||
|
||||
const fetchAll = useCallback(async () => {
|
||||
@@ -221,9 +237,25 @@ export default function DoctorPage() {
|
||||
designation: "",
|
||||
workingStatus: "",
|
||||
qualification: "",
|
||||
experience: "",
|
||||
professionalSummary: "",
|
||||
isActive: true,
|
||||
globalSortOrder: 0,
|
||||
specializations: [
|
||||
{
|
||||
name: "",
|
||||
description: "",
|
||||
},
|
||||
],
|
||||
departments: [],
|
||||
seoTitle: "",
|
||||
metaDescription: "",
|
||||
focusKeyphrase: "",
|
||||
slug: "",
|
||||
tags: [],
|
||||
ogTitle: "",
|
||||
ogDescription: "",
|
||||
ogImage: "",
|
||||
});
|
||||
setOpenModal(true);
|
||||
}
|
||||
@@ -243,6 +275,27 @@ export default function DoctorPage() {
|
||||
qualification: doc.qualification,
|
||||
isActive: doc.isActive ?? true,
|
||||
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) => ({
|
||||
departmentId: d.departmentId,
|
||||
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() {
|
||||
try {
|
||||
if (editing) {
|
||||
@@ -594,6 +649,24 @@ export default function DoctorPage() {
|
||||
className="text-base"
|
||||
/>
|
||||
</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">
|
||||
<label className="text-sm font-semibold">
|
||||
Qualification
|
||||
@@ -606,6 +679,25 @@ export default function DoctorPage() {
|
||||
className="text-base"
|
||||
/>
|
||||
</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 className="p-5 border rounded-md bg-muted/20">
|
||||
@@ -632,6 +724,314 @@ export default function DoctorPage() {
|
||||
})}
|
||||
</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: 150–160 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 className="space-y-6">
|
||||
|
||||
Reference in New Issue
Block a user