Replaces the old logo image path with the new logo image path in multiple components and pages including Header, Footer, LoginModal, and various content pages. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9a264234-c5d7-4dcc-adf3-a954b149b30d Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3df548ff-50ae-432f-9be4-25d34eccc983/9a264234-c5d7-4dcc-adf3-a954b149b30d/uLfJUnK
640 lines
23 KiB
TypeScript
640 lines
23 KiB
TypeScript
import { useRoute, useLocation } from "wouter";
|
|
import { FileText, Presentation, Info, Search, Settings, User, LogOut, IdCard } from "lucide-react";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
|
import { Slider } from "@/components/ui/slider";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { useAuth } from "@/hooks/useAuth";
|
|
import { useState, useEffect, useRef, useMemo } from "react";
|
|
import type { MediaOutlet } from "@shared/schema";
|
|
import Footer from "@/components/Footer";
|
|
import SearchModal from "@/components/SearchModal";
|
|
import { Document, Page, pdfjs } from 'react-pdf';
|
|
|
|
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.mjs';
|
|
|
|
const reportContent: Record<string, { htmlPath: string; pdfPath?: string; pptPath?: string; customComponent?: boolean }> = {
|
|
'chayan-asli': {
|
|
htmlPath: '/attached_assets/chayan asli report_1759208054055.html',
|
|
pptPath: '/attached_assets/chayan asli slides_1759213492580.pptx'
|
|
},
|
|
'krysh-parker': {
|
|
htmlPath: '/attached_assets/krysh parker report_1759209671294.html',
|
|
pptPath: '/attached_assets/krysh parker slides_1759209102236.pptx'
|
|
},
|
|
'mohamed-salah': {
|
|
htmlPath: '/attached_assets/Mohamed Salah_Report_en_1760420154846.html',
|
|
pdfPath: '/attached_assets/mohamed_salah_pdf_en_1760419721874.pdf',
|
|
customComponent: true
|
|
}
|
|
};
|
|
|
|
function MohamedSalahSlides() {
|
|
const [currentSlide, setCurrentSlide] = useState(1);
|
|
const [numPages, setNumPages] = useState(0);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [hoverSlide, setHoverSlide] = useState<number | null>(null);
|
|
const [hoverPosition, setHoverPosition] = useState({ x: 0, y: 0 });
|
|
|
|
const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => {
|
|
setNumPages(numPages);
|
|
setCurrentSlide(1); // Reset to first slide after load
|
|
setIsLoading(false);
|
|
};
|
|
|
|
const pdfOptions = useMemo(() => ({
|
|
cMapUrl: 'https://unpkg.com/pdfjs-dist@3.11.174/cmaps/',
|
|
cMapPacked: true,
|
|
standardFontDataUrl: 'https://unpkg.com/pdfjs-dist@3.11.174/standard_fonts/',
|
|
}), []);
|
|
|
|
const handleSliderChange = (value: number[]) => {
|
|
if (numPages > 0) {
|
|
setCurrentSlide(value[0]);
|
|
}
|
|
};
|
|
|
|
const handleSliderCommit = (value: number[]) => {
|
|
if (numPages > 0) {
|
|
setCurrentSlide(value[0]);
|
|
}
|
|
};
|
|
|
|
const handleSliderClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
if (numPages === 0) return;
|
|
|
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
const x = e.clientX - rect.left;
|
|
const percentage = x / rect.width;
|
|
const slideNumber = Math.max(1, Math.min(numPages, Math.round(percentage * numPages)));
|
|
|
|
setCurrentSlide(slideNumber);
|
|
};
|
|
|
|
const handleSliderHover = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
if (numPages === 0) return;
|
|
|
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
const x = e.clientX - rect.left;
|
|
const percentage = x / rect.width;
|
|
const slideNumber = Math.max(1, Math.min(numPages, Math.round(percentage * numPages)));
|
|
|
|
setHoverSlide(slideNumber);
|
|
setHoverPosition({ x: e.clientX, y: rect.top - 60 });
|
|
};
|
|
|
|
const handleSliderLeave = () => {
|
|
setHoverSlide(null);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="w-full flex justify-center relative" data-testid="slide-container-16-9">
|
|
{isLoading && (
|
|
<div className="absolute inset-0 flex items-center justify-center py-20 z-10 bg-white">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
|
<p className="text-gray-600" data-testid="text-loading-slides">Loading slides...</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<Document
|
|
file="/attached_assets/mohamed_salah_pdf_en_1760419721874.pdf"
|
|
onLoadSuccess={onDocumentLoadSuccess}
|
|
options={pdfOptions}
|
|
loading=""
|
|
>
|
|
<Page
|
|
pageNumber={currentSlide}
|
|
width={1000}
|
|
renderTextLayer={false}
|
|
renderAnnotationLayer={false}
|
|
data-testid="canvas-pdf-slide"
|
|
loading=""
|
|
/>
|
|
</Document>
|
|
</div>
|
|
|
|
<div className="space-y-4 flex flex-col items-center">
|
|
{/* Progress Bar */}
|
|
<div className="relative w-full max-w-[1000px]" data-testid="slide-progress-container">
|
|
<div
|
|
className="relative py-2"
|
|
onClick={handleSliderClick}
|
|
onMouseMove={handleSliderHover}
|
|
onMouseLeave={handleSliderLeave}
|
|
data-testid="slider-hover-area"
|
|
>
|
|
<Slider
|
|
value={[currentSlide]}
|
|
onValueChange={handleSliderChange}
|
|
onValueCommit={handleSliderCommit}
|
|
min={1}
|
|
max={numPages || 1}
|
|
step={1}
|
|
disabled={numPages === 0}
|
|
className="cursor-pointer"
|
|
data-testid="slider-progress"
|
|
/>
|
|
{/* Slide markers */}
|
|
<div className="absolute top-0 left-0 right-0 h-full pointer-events-none">
|
|
<div className="relative h-full">
|
|
{numPages > 0 && Array.from({ length: numPages }).map((_, i) => (
|
|
<div
|
|
key={i}
|
|
className="absolute top-1/2 -translate-y-1/2 w-1 h-1 bg-gray-300 rounded-full"
|
|
style={{
|
|
left: `${(i / (numPages - 1)) * 100}%`,
|
|
transform: 'translate(-50%, -50%)'
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Hover tooltip */}
|
|
{hoverSlide !== null && (
|
|
<div
|
|
className="fixed z-50 bg-gray-900 text-white px-3 py-2 rounded-lg text-sm font-medium shadow-lg pointer-events-none"
|
|
style={{
|
|
left: `${hoverPosition.x}px`,
|
|
top: `${hoverPosition.y}px`,
|
|
transform: 'translateX(-50%)'
|
|
}}
|
|
data-testid="tooltip-slide-preview"
|
|
>
|
|
Slide {hoverSlide}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Control buttons */}
|
|
<div className="flex items-center justify-between w-full max-w-[1000px]">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setCurrentSlide(Math.max(1, currentSlide - 1))}
|
|
disabled={numPages === 0 || currentSlide === 1}
|
|
data-testid="button-previous-slide"
|
|
>
|
|
Previous
|
|
</Button>
|
|
<span className="text-sm text-gray-600" data-testid="text-slide-counter">
|
|
Slide {currentSlide} of {numPages}
|
|
</span>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setCurrentSlide(Math.min(numPages || 1, currentSlide + 1))}
|
|
disabled={numPages === 0 || currentSlide === numPages}
|
|
data-testid="button-next-slide"
|
|
>
|
|
Next
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function Report() {
|
|
const [, params] = useRoute("/media/:slug/report");
|
|
const [, setLocation] = useLocation();
|
|
const [enlargedImage, setEnlargedImage] = useState<string | null>(null);
|
|
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false);
|
|
const { user, isAuthenticated } = useAuth();
|
|
|
|
const slug = params?.slug || '';
|
|
const content = reportContent[slug] || reportContent['chayan-asli'];
|
|
|
|
const { data: outlet, isLoading: outletLoading } = useQuery<MediaOutlet>({
|
|
queryKey: ["/api/media-outlets", slug],
|
|
enabled: !!slug
|
|
});
|
|
|
|
const handleLogout = async () => {
|
|
try {
|
|
const response = await fetch("/api/logout", {
|
|
method: "POST",
|
|
credentials: "include",
|
|
});
|
|
if (response.ok) {
|
|
window.location.href = "/";
|
|
}
|
|
} catch (error) {
|
|
console.error("Logout failed:", error);
|
|
}
|
|
};
|
|
|
|
const handleAdminPage = () => {
|
|
if (user?.role === "admin" || user?.role === "superadmin") {
|
|
setLocation("/admin");
|
|
}
|
|
};
|
|
|
|
// Get full URL for PPT file
|
|
const getFullPptUrl = () => {
|
|
const baseUrl = window.location.origin;
|
|
return `${baseUrl}${content.pptPath}`;
|
|
};
|
|
|
|
// Enhance report styling
|
|
const handleReportLoad = (e: React.SyntheticEvent<HTMLIFrameElement>) => {
|
|
try {
|
|
const iframe = e.currentTarget;
|
|
const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
|
|
if (iframeDoc && iframeDoc.body) {
|
|
// Wrap all body content in a container if not already wrapped
|
|
if (!iframeDoc.querySelector('.sapiens-report-wrapper')) {
|
|
const wrapper = iframeDoc.createElement('div');
|
|
wrapper.className = 'sapiens-report-wrapper';
|
|
while (iframeDoc.body.firstChild) {
|
|
wrapper.appendChild(iframeDoc.body.firstChild);
|
|
}
|
|
iframeDoc.body.appendChild(wrapper);
|
|
}
|
|
|
|
const style = iframeDoc.createElement('style');
|
|
style.textContent = `
|
|
html, body {
|
|
margin: 0 !important;
|
|
padding: 0 !important;
|
|
background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%) !important;
|
|
max-width: none !important;
|
|
width: 100% !important;
|
|
}
|
|
|
|
body {
|
|
padding: 48px 32px !important;
|
|
}
|
|
|
|
.sapiens-report-wrapper {
|
|
background: white !important;
|
|
border-radius: 16px !important;
|
|
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1) !important;
|
|
padding: 56px !important;
|
|
max-width: 1000px !important;
|
|
margin: 0 auto !important;
|
|
text-align: center !important;
|
|
}
|
|
|
|
.sapiens-report-wrapper p {
|
|
text-align: justify !important;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 36px !important;
|
|
font-weight: 700 !important;
|
|
color: #1a202c !important;
|
|
margin-bottom: 28px !important;
|
|
margin-top: 0 !important;
|
|
padding-bottom: 20px !important;
|
|
border-bottom: 3px solid #3b82f6 !important;
|
|
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
|
|
-webkit-background-clip: text !important;
|
|
-webkit-text-fill-color: transparent !important;
|
|
background-clip: text !important;
|
|
}
|
|
|
|
h2 {
|
|
font-size: 26px !important;
|
|
font-weight: 600 !important;
|
|
color: #2d3748 !important;
|
|
margin-top: 44px !important;
|
|
margin-bottom: 18px !important;
|
|
padding-bottom: 10px !important;
|
|
border-bottom: 2px solid #e2e8f0 !important;
|
|
}
|
|
|
|
h3 {
|
|
font-size: 22px !important;
|
|
font-weight: 600 !important;
|
|
color: #374151 !important;
|
|
margin-top: 32px !important;
|
|
margin-bottom: 14px !important;
|
|
}
|
|
|
|
h4 {
|
|
font-size: 19px !important;
|
|
font-weight: 600 !important;
|
|
color: #4b5563 !important;
|
|
margin-top: 26px !important;
|
|
margin-bottom: 12px !important;
|
|
}
|
|
|
|
p {
|
|
font-size: 17px !important;
|
|
line-height: 1.8 !important;
|
|
color: #374151 !important;
|
|
margin-bottom: 18px !important;
|
|
text-align: justify !important;
|
|
}
|
|
|
|
p + img,
|
|
img + p {
|
|
margin-top: 24px !important;
|
|
}
|
|
|
|
.metadata {
|
|
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%) !important;
|
|
padding: 24px !important;
|
|
border-radius: 12px !important;
|
|
border-left: 4px solid #3b82f6 !important;
|
|
margin-bottom: 36px !important;
|
|
font-size: 15px !important;
|
|
color: #1e40af !important;
|
|
text-align: center !important;
|
|
}
|
|
|
|
.section {
|
|
margin-bottom: 36px !important;
|
|
}
|
|
|
|
ul, ol {
|
|
margin-bottom: 20px !important;
|
|
padding-left: 32px !important;
|
|
text-align: left !important;
|
|
}
|
|
|
|
li {
|
|
font-size: 17px !important;
|
|
line-height: 1.8 !important;
|
|
color: #4b5563 !important;
|
|
margin-bottom: 10px !important;
|
|
text-align: left !important;
|
|
}
|
|
|
|
.highlight {
|
|
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%) !important;
|
|
padding: 24px !important;
|
|
border-left: 4px solid #f59e0b !important;
|
|
border-radius: 10px !important;
|
|
margin: 28px 0 !important;
|
|
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.08) !important;
|
|
}
|
|
|
|
.quote {
|
|
background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%) !important;
|
|
padding: 24px 28px !important;
|
|
border-left: 4px solid #6b7280 !important;
|
|
border-radius: 10px !important;
|
|
margin: 28px 0 !important;
|
|
font-style: italic !important;
|
|
color: #374151 !important;
|
|
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.08) !important;
|
|
}
|
|
|
|
strong {
|
|
font-weight: 700 !important;
|
|
color: #1f2937 !important;
|
|
}
|
|
|
|
img {
|
|
max-width: 100% !important;
|
|
height: auto !important;
|
|
border-radius: 12px !important;
|
|
box-shadow: 0 4px 12px -2px rgba(0, 0, 0, 0.15) !important;
|
|
margin: 32px auto !important;
|
|
display: block !important;
|
|
clear: both !important;
|
|
float: none !important;
|
|
}
|
|
|
|
table {
|
|
max-width: 100% !important;
|
|
width: 100% !important;
|
|
border-collapse: collapse !important;
|
|
margin: 20px 0 !important;
|
|
}
|
|
|
|
table, iframe {
|
|
max-width: 100% !important;
|
|
}
|
|
|
|
@page {
|
|
margin: 0 !important;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.sapiens-report-wrapper {
|
|
padding: 32px 24px !important;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 28px !important;
|
|
}
|
|
|
|
h2 {
|
|
font-size: 22px !important;
|
|
}
|
|
|
|
h3 {
|
|
font-size: 19px !important;
|
|
}
|
|
}
|
|
`;
|
|
iframeDoc.head.appendChild(style);
|
|
}
|
|
} catch (e) {
|
|
console.error('Could not inject styles into iframe:', e);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col min-h-screen bg-gray-50">
|
|
{/* Header */}
|
|
<header className="bg-white border-b border-gray-200 sticky top-0 z-50">
|
|
<div className="max-w-7xl mx-auto px-6 py-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-3">
|
|
{outletLoading ? (
|
|
<>
|
|
<div className="w-10 h-10 bg-gray-200 rounded-full animate-pulse"></div>
|
|
<div className="flex flex-col mt-1">
|
|
<div className="h-2.5 w-16 bg-gray-200 rounded animate-pulse mb-0.5"></div>
|
|
<div className="h-5 w-24 bg-gray-200 rounded animate-pulse"></div>
|
|
</div>
|
|
</>
|
|
) : outlet ? (
|
|
<>
|
|
{outlet.imageUrl ? (
|
|
<img
|
|
src={outlet.imageUrl}
|
|
alt={outlet.name}
|
|
className="w-10 h-10 rounded-full object-cover cursor-pointer hover:ring-2 hover:ring-blue-400 transition-all"
|
|
onClick={() => setEnlargedImage(outlet.imageUrl!)}
|
|
data-testid="image-outlet-header-profile"
|
|
/>
|
|
) : (
|
|
<div className="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center">
|
|
<span className="text-gray-600 font-bold text-sm">
|
|
{outlet.name.charAt(0)}
|
|
</span>
|
|
</div>
|
|
)}
|
|
<div className="flex items-center space-x-2">
|
|
<span className="text-3xl font-bold text-gray-900" data-testid="text-outlet-name-header">
|
|
{outlet.name}
|
|
</span>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setLocation(`/media/${outlet.slug}/report`);
|
|
}}
|
|
className="h-6 px-2 flex items-center gap-1 text-xs border-gray-300 hover:bg-gray-100"
|
|
aria-label="View complete profile"
|
|
data-testid="button-profile"
|
|
>
|
|
<IdCard className="h-3 w-3" />
|
|
<span>About</span>
|
|
</Button>
|
|
</div>
|
|
</>
|
|
) : null}
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-4">
|
|
<div
|
|
className="relative cursor-pointer flex items-center"
|
|
onClick={() => setIsSearchModalOpen(true)}
|
|
data-testid="search-container"
|
|
>
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400 pointer-events-none" />
|
|
<div className="pl-10 pr-16 py-1.5 bg-gray-50 border border-gray-200 rounded-md flex items-center space-x-2 hover:bg-gray-100 transition-colors">
|
|
<span className="text-sm text-gray-500">search in</span>
|
|
<img
|
|
src="/attached_assets/검은색 로고_1760521542271.png"
|
|
alt="SAPIENS"
|
|
className="h-2 w-auto"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{isAuthenticated && user ? (
|
|
<>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={handleAdminPage}
|
|
data-testid="button-admin-dashboard"
|
|
>
|
|
Admin Dashboard
|
|
</Button>
|
|
|
|
<div className="flex items-center space-x-2 px-3 py-1 bg-gray-100 rounded-md">
|
|
<User className="h-4 w-4 text-gray-600" />
|
|
<span className="text-sm font-medium text-gray-700" data-testid="user-name">
|
|
{user.firstName} {user.lastName}
|
|
</span>
|
|
</div>
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={handleLogout}
|
|
data-testid="button-logout"
|
|
>
|
|
<LogOut className="h-4 w-4" />
|
|
</Button>
|
|
</>
|
|
) : (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setLocation("/login")}
|
|
data-testid="button-login"
|
|
>
|
|
Login
|
|
</Button>
|
|
)}
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
data-testid="button-settings"
|
|
>
|
|
<Settings className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Main Content */}
|
|
<main className="flex-1 max-w-[1600px] mx-auto px-8 py-2 pb-32 w-full">
|
|
<Tabs defaultValue="report" className="w-full">
|
|
<TabsList className="grid w-full max-w-md mx-auto grid-cols-2 mb-4">
|
|
<TabsTrigger value="report" data-testid="tab-report">
|
|
<FileText className="h-4 w-4 mr-2" />
|
|
Report
|
|
</TabsTrigger>
|
|
<TabsTrigger value="slides" data-testid="tab-slides">
|
|
<Presentation className="h-4 w-4 mr-2" />
|
|
Slides
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="report">
|
|
<Card className="bg-white">
|
|
<CardContent className="p-0">
|
|
<iframe
|
|
src={content.htmlPath}
|
|
className="w-full border-none"
|
|
style={{ display: 'block', minHeight: '100vh', height: 'auto' }}
|
|
title="Comprehensive Report"
|
|
data-testid="iframe-report"
|
|
onLoad={handleReportLoad}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="slides" className="w-full">
|
|
{slug === 'mohamed-salah' && content.customComponent ? (
|
|
<MohamedSalahSlides />
|
|
) : content.pptPath ? (
|
|
<div className="w-full bg-white rounded-lg overflow-hidden shadow-lg">
|
|
<div className="relative w-full" style={{ paddingTop: '56.25%' }}>
|
|
<iframe
|
|
src={`https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(getFullPptUrl())}`}
|
|
className="absolute top-0 left-0 w-full h-full border-none"
|
|
title="Presentation Slides"
|
|
data-testid="iframe-slides"
|
|
allowFullScreen
|
|
style={{
|
|
backgroundColor: 'white',
|
|
display: 'block'
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<Card>
|
|
<CardContent className="p-12 text-center">
|
|
<Presentation className="h-16 w-16 text-gray-400 mx-auto mb-4" />
|
|
<h2 className="text-2xl font-bold mb-2">No Slides Available</h2>
|
|
<p className="text-gray-600">
|
|
There are no presentation slides available for this media outlet yet.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</TabsContent>
|
|
</Tabs>
|
|
</main>
|
|
|
|
<Footer />
|
|
|
|
{/* Search Modal */}
|
|
<SearchModal
|
|
isOpen={isSearchModalOpen}
|
|
onClose={() => setIsSearchModalOpen(false)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|