feat: seo preview #44
@@ -1,131 +1,159 @@
|
|||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
interface SeoPreviewData {
|
interface SeoPreviewData {
|
||||||
seo?: {
|
seo?: {
|
||||||
ogImage?: string;
|
ogImage?: string;
|
||||||
ogTitle?: string;
|
ogTitle?: string;
|
||||||
seoTitle?: string;
|
seoTitle?: string;
|
||||||
ogDescription?: string;
|
ogDescription?: string;
|
||||||
metaDescription?: string;
|
metaDescription?: string;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
};
|
};
|
||||||
doctorId?: string;
|
doctorId?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SeoPreviewProps {
|
interface SeoPreviewProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
previewData?: SeoPreviewData | null;
|
previewData?: SeoPreviewData | null;
|
||||||
url?: string;
|
url?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SeoPreview({
|
export default function SeoPreview({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
previewData,
|
previewData,
|
||||||
url,
|
url,
|
||||||
title = "SEO Preview",
|
title = "SEO Preview",
|
||||||
}: SeoPreviewProps) {
|
}: SeoPreviewProps) {
|
||||||
const previewUrl = url || "#";
|
const previewUrl = url || "#";
|
||||||
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 (
|
const hasSeoData =
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
!!previewData?.seo &&
|
||||||
<DialogContent className="sm:!max-w-4xl overflow-hidden">
|
!!(
|
||||||
<DialogHeader>
|
previewData.seo.ogImage ||
|
||||||
<DialogTitle className="text-xl">{title}</DialogTitle>
|
previewData.seo.ogTitle ||
|
||||||
</DialogHeader>
|
previewData.seo.seoTitle ||
|
||||||
|
previewData.seo.ogDescription ||
|
||||||
|
previewData.seo.metaDescription
|
||||||
|
);
|
||||||
|
|
||||||
{previewData ? (
|
const imageUrl =
|
||||||
<div className="space-y-10 py-2">
|
previewData?.seo?.ogImage ||
|
||||||
<div>
|
"https://placehold.co/1200x630?text=GG+Hospital";
|
||||||
<p className="mb-4 text-sm font-semibold text-muted-foreground">
|
|
||||||
Social Media Preview (WhatsApp / Facebook)
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<a
|
const ogTitle =
|
||||||
href={previewUrl}
|
previewData?.seo?.ogTitle || previewData?.seo?.seoTitle || "GG Hospital";
|
||||||
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">
|
const ogDescription =
|
||||||
<p className="truncate text-[11px] uppercase tracking-wide text-[#65676b]">
|
previewData?.seo?.ogDescription ||
|
||||||
gg-hospital.com
|
previewData?.seo?.metaDescription ||
|
||||||
</p>
|
"No description available";
|
||||||
|
|
||||||
<h3 className="mt-1 line-clamp-2 text-[18px] font-semibold leading-snug text-[#1c1e21]">
|
const searchTitle =
|
||||||
{ogTitle}
|
previewData?.seo?.seoTitle ||
|
||||||
</h3>
|
previewData?.seo?.ogTitle ||
|
||||||
|
"SEO title preview";
|
||||||
|
|
||||||
<p className="mt-1 line-clamp-2 text-[14px] text-[#65676b]">
|
const searchDescription =
|
||||||
{ogDescription}
|
previewData?.seo?.metaDescription ||
|
||||||
</p>
|
previewData?.seo?.ogDescription ||
|
||||||
</div>
|
"No meta description available";
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
return (
|
||||||
<p className="mb-4 text-sm font-semibold text-muted-foreground">
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
Google Search Preview
|
<DialogContent className="sm:!max-w-4xl overflow-hidden">
|
||||||
</p>
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-xl">{title}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="rounded-xl border bg-white p-6">
|
{hasSeoData ? (
|
||||||
<a
|
<div className="space-y-10 py-2">
|
||||||
href={previewUrl}
|
{/* Social Preview */}
|
||||||
target="_blank"
|
<div>
|
||||||
rel="noopener noreferrer"
|
<p className="mb-4 text-sm font-semibold text-muted-foreground">
|
||||||
className="block"
|
Social Media Preview (WhatsApp / Facebook)
|
||||||
>
|
</p>
|
||||||
<p className="truncate text-[14px] text-[#202124] hover:underline">
|
|
||||||
{previewUrl}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h3 className="mt-1 text-[22px] leading-tight text-[#1a0dab] hover:underline">
|
<a
|
||||||
{searchTitle}
|
href={previewUrl}
|
||||||
</h3>
|
target="_blank"
|
||||||
</a>
|
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>
|
||||||
|
|
||||||
<p className="mt-2 line-clamp-3 text-[14px] leading-6 text-[#4d5156]">
|
<div className="border-t bg-[#f0f2f5] px-4 py-3">
|
||||||
{searchDescription}
|
<p className="truncate text-[11px] uppercase tracking-wide text-[#65676b]">
|
||||||
</p>
|
gg-hospital.com
|
||||||
</div>
|
</p>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="p-6 text-sm text-muted-foreground">
|
|
||||||
No preview data available.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DialogFooter className="p-6 border-t bg-background z-10 mt-0">
|
<h3 className="mt-1 line-clamp-2 text-[18px] font-semibold leading-snug text-[#1c1e21]">
|
||||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
{ogTitle}
|
||||||
Close
|
</h3>
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
<p className="mt-1 line-clamp-2 text-[14px] text-[#65676b]">
|
||||||
</DialogContent>
|
{ogDescription}
|
||||||
</Dialog>
|
</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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ import {
|
|||||||
createHealthPackageApi,
|
createHealthPackageApi,
|
||||||
createCategoryApi,
|
createCategoryApi,
|
||||||
updateCategoryApi,
|
updateCategoryApi,
|
||||||
deleteCategoryApi,
|
|
||||||
HealthPackage,
|
HealthPackage,
|
||||||
HealthCategory,
|
HealthCategory,
|
||||||
} from "@/api/healthCheck";
|
} from "@/api/healthCheck";
|
||||||
|
|
||||||
import PackageInquiriesTab from "@/components/PackageInquiriesTab/PackageInquiriesTab";
|
import PackageInquiriesTab from "@/components/PackageInquiriesTab/PackageInquiriesTab";
|
||||||
import HealthPackageModal from "@/components/HealthPackageModal/HealthPackageModal";
|
import HealthPackageModal from "@/components/HealthPackageModal/HealthPackageModal";
|
||||||
|
import SeoPreview from "@/components/SeoPreview/SeoPreview";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@@ -771,86 +771,12 @@ export default function HealthPackagePage() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* --- VIEW MODAL --- */}
|
<SeoPreview
|
||||||
<Dialog open={viewModal} onOpenChange={setViewModal}>
|
open={viewModal}
|
||||||
<DialogContent className="w-full !max-w-5xl h-[90vh] flex flex-col p-0 overflow-hidden">
|
onOpenChange={setViewModal}
|
||||||
<DialogHeader className="p-6 border-b bg-background z-10">
|
previewData={selectedPackage}
|
||||||
<DialogTitle className="text-2xl">
|
url="asdfasdfasD"
|
||||||
{selectedPackage?.name}
|
/>
|
||||||
</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="flex-1 overflow-y-auto p-6 space-y-6">
|
|
||||||
<div className="flex justify-between items-center bg-muted/20 p-4 rounded-lg">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-muted-foreground uppercase font-bold">
|
|
||||||
Category
|
|
||||||
</p>
|
|
||||||
<p className="text-base font-medium">
|
|
||||||
{selectedPackage?.category?.name}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-right">
|
|
||||||
<p className="text-sm text-muted-foreground uppercase font-bold">
|
|
||||||
Pricing
|
|
||||||
</p>
|
|
||||||
<p className="text-xl font-bold">
|
|
||||||
{selectedPackage?.discountedPrice != null
|
|
||||||
? `₹${selectedPackage.discountedPrice}`
|
|
||||||
: selectedPackage?.price != null
|
|
||||||
? `₹${selectedPackage.price}`
|
|
||||||
: "Not Entered"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-bold text-base border-b pb-2 mb-4">
|
|
||||||
Inclusions
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-6">
|
|
||||||
{selectedPackage?.inclusions &&
|
|
||||||
typeof selectedPackage.inclusions === "object" &&
|
|
||||||
!Array.isArray(selectedPackage.inclusions) ? (
|
|
||||||
Object.entries(selectedPackage.inclusions).map(
|
|
||||||
([category, tests], idx) => (
|
|
||||||
<div key={idx}>
|
|
||||||
<h4 className="font-semibold text-sm text-primary mb-3 uppercase tracking-wider">
|
|
||||||
{category}
|
|
||||||
</h4>
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
|
||||||
{Array.isArray(tests) &&
|
|
||||||
tests.map((item, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="text-sm border p-3 rounded bg-background shadow-sm"
|
|
||||||
>
|
|
||||||
✓ {item}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
|
||||||
{Array.isArray(selectedPackage?.inclusions) &&
|
|
||||||
selectedPackage.inclusions.map((item, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="text-sm border p-3 rounded bg-background shadow-sm"
|
|
||||||
>
|
|
||||||
✓ {item}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogFooter className="p-6 border-t">
|
|
||||||
<Button onClick={() => setViewModal(false)}>Close</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user