Merge branch 'hemant' of http://git.wdipl.com/CityCards/CityCards-Website into arya-branch
This commit is contained in:
@@ -36,6 +36,7 @@ import { CartPage } from './pages/CartPage';
|
||||
import { PaymentDetailsPage } from './pages/PaymentDetailsPage';
|
||||
import { CartPageDesign } from './pages/CartPageDesign';
|
||||
import { CheckoutPage2 } from './pages/CheckoutPage2';
|
||||
import { SuperSavingsDetailsPage } from './pages/SuperSavingsDetailsPage';
|
||||
|
||||
// User type definition
|
||||
interface User {
|
||||
@@ -295,6 +296,12 @@ export function AppRouter({
|
||||
<PaymentDetailsPage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
<Route path="/super-savings/:id" element={
|
||||
<motion.div key="super-savings" {...pageTransition}>
|
||||
<SuperSavingsDetailsPage {...commonNavHandlers}
|
||||
onBackClick={() => navigate(-1)} />
|
||||
</motion.div>
|
||||
} />
|
||||
</Routes>
|
||||
</AnimatePresence>
|
||||
</>
|
||||
|
||||
@@ -41,7 +41,13 @@ export const citiesApi = createApi({
|
||||
return `/website/super-savings/list/offers?${params.toString()}`;
|
||||
}
|
||||
}),
|
||||
|
||||
getOfferDetailsById: builder.query({
|
||||
query: (id: number) => `/website/super-savings/list/offers/${id}`,
|
||||
}),
|
||||
|
||||
|
||||
}),
|
||||
});
|
||||
|
||||
export const { useGetCityListWithBannerQuery, useGetUpcomingCitiesQuery, useGetSelectedCityDetailsQuery, useGetSelectedCityOffersQuery } = citiesApi;
|
||||
export const { useGetCityListWithBannerQuery, useGetUpcomingCitiesQuery, useGetSelectedCityDetailsQuery, useGetSelectedCityOffersQuery, useGetOfferDetailsByIdQuery } = citiesApi;
|
||||
@@ -186,12 +186,9 @@ export function MelbournePage({
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="bg-gray-50/30 min-h-screen relative">
|
||||
{/* Sticky Page Navigation */}
|
||||
<div className="sticky top-[78px] lg:top-[94px] z-40 bg-white/80 backdrop-blur-xl border-b border-gray-100 shadow-sm">
|
||||
{/* <div className="sticky top-[78px] lg:top-[94px] z-40 bg-white/80 backdrop-blur-xl border-b border-gray-100 shadow-sm">
|
||||
<div className="container mx-auto px-4">
|
||||
{/* horizontal scroll wrapper */}
|
||||
<div className="overflow-x-auto no-scrollbar">
|
||||
{/* actual flex row */}
|
||||
<div className="flex items-center justify-center gap-2 py-3 min-w-max">
|
||||
{[
|
||||
{ id: 'overview', label: 'Overview', icon: MapPin },
|
||||
@@ -219,7 +216,7 @@ export function MelbournePage({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
|
||||
<div className="container mx-auto px-4 py-12 space-y-24">
|
||||
|
||||
387
src/pages/SuperSavingsDetailsPage.tsx
Normal file
387
src/pages/SuperSavingsDetailsPage.tsx
Normal file
@@ -0,0 +1,387 @@
|
||||
import { ArrowLeft, Check, Clock, MapPin, Users, X } from 'lucide-react';
|
||||
import { motion } from 'motion/react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
||||
import LoadingSpinner from '../components/LoadingSpinner';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Card } from '../components/ui/card';
|
||||
import { Layout } from '../Layout';
|
||||
import { useGetOfferDetailsByIdQuery } from '../Redux/services/cities.service';
|
||||
|
||||
interface SuperSavingsDetailsPageProps {
|
||||
onBackClick: () => void;
|
||||
onCheckoutClick: () => void;
|
||||
onSignInClick: () => void;
|
||||
onSignOutClick?: () => void;
|
||||
user?: { email: string; name: string } | null;
|
||||
}
|
||||
|
||||
export function SuperSavingsDetailsPage({
|
||||
onBackClick,
|
||||
onCheckoutClick,
|
||||
onSignInClick,
|
||||
onSignOutClick,
|
||||
user,
|
||||
}: SuperSavingsDetailsPageProps) {
|
||||
const { id } = useParams();
|
||||
const { data: offer, isLoading } = useGetOfferDetailsByIdQuery(Number(id));
|
||||
const baseUrl = import.meta.env.VITE_BASE_URL;
|
||||
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
// Guard against missing data – but keep all UI elements
|
||||
const safeOffer = offer || {
|
||||
id: 0,
|
||||
title: 'Offer Details',
|
||||
description: 'No description available.',
|
||||
cityXid: 0,
|
||||
cardXid: 0,
|
||||
cardTypeXid: 0,
|
||||
categoryXid: 0,
|
||||
partnerName: '',
|
||||
offerCode: '',
|
||||
websiteBannerImage: '',
|
||||
mobileBannerImage: '',
|
||||
redemptionLink: '',
|
||||
passType: '',
|
||||
startDateTime: null,
|
||||
endDateTime: null,
|
||||
applyToPasses: false,
|
||||
stepsForBooking: null,
|
||||
offerStatus: '',
|
||||
isActive: true,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
city: { id: 0, cityName: 'Unknown City' },
|
||||
card: { id: 0, title: 'Unknown Card' },
|
||||
cardType: { id: 0, cardTypeDisplayName: 'Standard' },
|
||||
category: { id: 0, categoryName: 'General' },
|
||||
};
|
||||
|
||||
// Build badges from available API data (preserves the badge UI section)
|
||||
const superSavingsBadges = [
|
||||
safeOffer.category && { badgeXid: safeOffer.category.id, badge: { badgeName: safeOffer.category.categoryName } },
|
||||
safeOffer.cardType && { badgeXid: safeOffer.cardType.id, badge: { badgeName: safeOffer.cardType.cardTypeDisplayName } },
|
||||
safeOffer.offerCode && { badgeXid: -1, badge: { badgeName: `Code: ${safeOffer.offerCode}` } },
|
||||
safeOffer.offerStatus && { badgeXid: -2, badge: { badgeName: safeOffer.offerStatus.toUpperCase() } },
|
||||
].filter(Boolean);
|
||||
|
||||
// Build gallery array from banner images (original expected superSavingsGalleries)
|
||||
const superSavingsGalleries = [];
|
||||
if (safeOffer.websiteBannerImage) {
|
||||
superSavingsGalleries.push({ id: 1, filePathUrl: safeOffer.websiteBannerImage });
|
||||
}
|
||||
if (safeOffer.mobileBannerImage) {
|
||||
superSavingsGalleries.push({ id: 2, filePathUrl: safeOffer.mobileBannerImage });
|
||||
}
|
||||
// If no images, add a placeholder
|
||||
if (superSavingsGalleries.length === 0) {
|
||||
superSavingsGalleries.push({ id: 0, filePathUrl: 'https://placehold.co/1200x800?text=No+Image' });
|
||||
}
|
||||
|
||||
// Mock data for sections not present in API (preserve structure but show empty/fallback)
|
||||
const durations = safeOffer.startDateTime && safeOffer.endDateTime
|
||||
? Math.round((new Date(safeOffer.endDateTime).getTime() - new Date(safeOffer.startDateTime).getTime()) / (1000 * 60))
|
||||
: 'Not specified';
|
||||
const groupSize = 'Not specified';
|
||||
const ageRange = 'All ages';
|
||||
const superSavingsLanguages: any[] = []; // API has no language data
|
||||
const superSavingsHighlights: any[] = []; // API has no highlights
|
||||
// Inclusions: API has none, so show empty state (or we could derive from redemptionLink etc.)
|
||||
const superSavingsInclusions: any[] = [];
|
||||
const address = safeOffer.city?.cityName || 'Location not specified';
|
||||
|
||||
return (
|
||||
<Layout
|
||||
activeCity=""
|
||||
onSignInClick={onSignInClick}
|
||||
onSignOutClick={onSignOutClick}
|
||||
user={user}
|
||||
>
|
||||
<div className="container mx-auto px-4 pt-40 pb-16 max-w-6xl">
|
||||
{/* Back Button */}
|
||||
<motion.div
|
||||
className="mb-8"
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={onBackClick}
|
||||
className="font-poppins font-medium text-base text-gray-600 hover:text-primary transition-colors duration-200"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Back to Super-Savings Page
|
||||
</Button>
|
||||
</motion.div>
|
||||
|
||||
{/* Title and Badges Section */}
|
||||
<div className="mb-8">
|
||||
<div className="flex flex-wrap gap-3 mb-6">
|
||||
{superSavingsBadges.map((badge: any, index: number) => (
|
||||
<Badge
|
||||
key={badge.badgeXid}
|
||||
variant={index === 0 ? 'default' : 'secondary'}
|
||||
className={`px-6 py-2 rounded-full text-sm transition-all duration-200 ${
|
||||
index === 0
|
||||
? 'bg-primary text-white shadow-lg'
|
||||
: 'bg-primary/10 text-primary border border-primary/20'
|
||||
}`}
|
||||
>
|
||||
{badge.badge.badgeName}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl font-bold text-[#2d3134] leading-tight">
|
||||
<span className="bg-gradient-to-r from-primary to-primary/80 bg-clip-text text-transparent">
|
||||
{safeOffer.title}
|
||||
</span>{' '}
|
||||
<span className="text-[#2d3134]">
|
||||
Day Trip by {safeOffer.partnerName || safeOffer.card?.title || 'Partner'}
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Image Gallery Section - preserved exactly as original */}
|
||||
<div className="grid grid-cols-4 grid-rows-2 gap-4 h-[510px] mb-12">
|
||||
{/* Main large image */}
|
||||
<div className="col-span-2 row-span-2">
|
||||
<ImageWithFallback
|
||||
src={ `${baseUrl}/${superSavingsGalleries[0]?.filePathUrl}` }
|
||||
alt="Main attraction image"
|
||||
className="w-full h-full object-cover rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Gallery images - use remaining images or repeat first if needed */}
|
||||
{superSavingsGalleries.slice(1, 5).map((image: any) => (
|
||||
<div key={image.id} className="col-span-1 row-span-1">
|
||||
<ImageWithFallback
|
||||
src={ `${baseUrl}/${image.filePathUrl}` }
|
||||
alt={`Gallery image ${image.id}`}
|
||||
className="w-full h-full object-cover rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{/* If less than 4 extra images, fill with placeholders to maintain grid */}
|
||||
{superSavingsGalleries.slice(1, 5).length < 4 &&
|
||||
Array(4 - superSavingsGalleries.slice(1, 5).length)
|
||||
.fill(null)
|
||||
.map((_, idx) => (
|
||||
<div key={`placeholder-${idx}`} className="col-span-1 row-span-1">
|
||||
<div className="w-full h-full bg-gray-100 rounded-lg flex items-center justify-center text-gray-400">
|
||||
No Image
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Main Content Grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12">
|
||||
{/* Left Content - Tour Details */}
|
||||
<div className="lg:col-span-2 space-y-12">
|
||||
{/* Overview Cards - preserved */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
{/* Duration */}
|
||||
<Card className="p-4 text-center bg-white border border-primary/10 hover:border-primary/20 transition-all duration-200 hover:shadow-lg group">
|
||||
<div className="w-12 h-12 bg-primary/10 rounded-xl mx-auto mb-3 flex items-center justify-center group-hover:bg-primary/20 transition-colors duration-200">
|
||||
<Clock className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h3 className="font-normal text-primary capitalize mb-1">Duration</h3>
|
||||
<p className="text-sm text-[#717171] font-light">
|
||||
{typeof durations === 'number' ? `${durations} mins` : durations}
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
{/* Group Size */}
|
||||
<Card className="p-4 text-center bg-white border border-primary/10 hover:border-primary/20 transition-all duration-200 hover:shadow-lg group">
|
||||
<div className="w-12 h-12 bg-primary/10 rounded-xl mx-auto mb-3 flex items-center justify-center group-hover:bg-primary/20 transition-colors duration-200">
|
||||
<Users className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h3 className="font-normal text-primary capitalize mb-1">Group Size</h3>
|
||||
<p className="text-sm text-[#717171] font-light">{groupSize}</p>
|
||||
</Card>
|
||||
|
||||
{/* Age Range */}
|
||||
<Card className="p-4 text-center bg-white border border-primary/10 hover:border-primary/20 transition-all duration-200 hover:shadow-lg group">
|
||||
<div className="w-12 h-12 bg-primary/10 rounded-xl mx-auto mb-3 flex items-center justify-center group-hover:bg-primary/20 transition-colors duration-200">
|
||||
<Users className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h3 className="font-normal text-primary capitalize mb-1">Age Range</h3>
|
||||
<p className="text-sm text-[#717171] font-light">{ageRange}</p>
|
||||
</Card>
|
||||
|
||||
{/* Languages */}
|
||||
<Card className="p-4 text-center bg-white border border-primary/10 hover:border-primary/20 transition-all duration-200 hover:shadow-lg group">
|
||||
<div className="w-12 h-12 bg-primary/10 rounded-xl mx-auto mb-3 flex items-center justify-center group-hover:bg-primary/20 transition-colors duration-200">
|
||||
<MapPin className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h3 className="font-normal text-primary capitalize mb-1">Languages</h3>
|
||||
<p className="text-sm text-[#717171] font-light">
|
||||
{superSavingsLanguages?.length > 0
|
||||
? superSavingsLanguages?.map((lang: any) => lang.language.name).join(', ')
|
||||
: 'English (default)'}
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Tour Overview */}
|
||||
<div>
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className="h-1 w-12 bg-primary rounded-full"></div>
|
||||
<h2 className="text-3xl font-semibold text-[#2d3134]">
|
||||
Tour <span className="text-primary">Overview</span>
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-[#2d3134] leading-relaxed text-lg font-light">
|
||||
{safeOffer.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tour Highlights - preserved even if empty */}
|
||||
<div>
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className="h-1 w-12 bg-primary rounded-full"></div>
|
||||
<h3 className="text-2xl font-medium text-[#2d3134]">
|
||||
Tour <span className="text-primary">Highlights</span>
|
||||
</h3>
|
||||
</div>
|
||||
{superSavingsHighlights.length > 0 ? (
|
||||
<ul className="space-y-4">
|
||||
{superSavingsHighlights.map((highlight: any) => (
|
||||
<li key={highlight.id} className="flex items-start gap-3 group">
|
||||
<div className="w-6 h-6 bg-primary/10 rounded-full mt-1 flex items-center justify-center flex-shrink-0 group-hover:bg-primary/20 transition-colors duration-200">
|
||||
<div className="w-2 h-2 bg-primary rounded-full"></div>
|
||||
</div>
|
||||
<span className="text-[#2d3134] leading-relaxed font-light">{highlight.title}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="text-gray-500 italic">No highlights listed for this offer.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* What's Included/Not Included - preserved */}
|
||||
<div>
|
||||
<div className="flex items-center gap-4 mb-8">
|
||||
<div className="h-1 w-12 bg-primary rounded-full"></div>
|
||||
<h3 className="text-3xl font-semibold text-[#2d3134]">
|
||||
What's <span className="text-primary">included</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{/* Included */}
|
||||
<div className="space-y-4">
|
||||
<h4 className="font-medium text-primary mb-4 flex items-center gap-2">
|
||||
<Check className="w-5 h-5" />
|
||||
Included
|
||||
</h4>
|
||||
{superSavingsInclusions.filter((inc: any) => inc.isInclusion === true).length > 0 ? (
|
||||
superSavingsInclusions
|
||||
.filter((inclusion: any) => inclusion.isInclusion === true)
|
||||
.map((inclusion: any) => (
|
||||
<div key={inclusion.id} className="flex items-start gap-3 group">
|
||||
<div className="w-6 h-6 bg-primary/10 rounded-full flex items-center justify-center flex-shrink-0 mt-1 group-hover:bg-primary/20 transition-colors duration-200">
|
||||
<Check className="w-3 h-3 text-primary" />
|
||||
</div>
|
||||
<span className="text-[#2d3134] font-light">{inclusion.title}</span>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-gray-500 italic">No included items specified.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Not Included */}
|
||||
<div className="space-y-4">
|
||||
<h4 className="font-medium text-gray-600 mb-4 flex items-center gap-2">
|
||||
<X className="w-5 h-5" />
|
||||
Not Included
|
||||
</h4>
|
||||
{superSavingsInclusions.filter((inc: any) => inc.isInclusion === false).length > 0 ? (
|
||||
superSavingsInclusions
|
||||
.filter((inclusion: any) => inclusion.isInclusion === false)
|
||||
.map((inclusion: any) => (
|
||||
<div key={inclusion.id} className="flex items-start gap-3 group">
|
||||
<div className="w-6 h-6 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0 mt-1 group-hover:bg-gray-200 transition-colors duration-200">
|
||||
<X className="w-3 h-3 text-gray-500" />
|
||||
</div>
|
||||
<span className="text-[#2d3134] font-light">{inclusion.title}</span>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-gray-500 italic">No excluded items specified.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Location on map - preserved */}
|
||||
<div>
|
||||
<div className="flex items-center gap-4 mb-8">
|
||||
<div className="h-1 w-12 bg-primary rounded-full"></div>
|
||||
<h3 className="text-3xl font-semibold text-[#2d3134]">
|
||||
Location on <span className="text-primary">map</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div className="h-80 bg-gradient-to-br from-primary/5 to-primary/10 rounded-lg flex items-center justify-center border border-primary/10 hover:border-primary/20 transition-colors duration-200">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<MapPin className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<p className="text-lg font-medium text-primary mb-2">Interactive Map</p>
|
||||
<p className="text-sm text-gray-600 font-light">{safeOffer.title}</p>
|
||||
<p className="text-sm text-gray-600 font-light">{address}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Sidebar - Calendar and Booking (preserved, but you can add a real calendar if needed) */}
|
||||
<div className="lg:col-span-1">
|
||||
<Card className="sticky top-32 p-6 bg-white border border-primary/20 shadow-xl rounded-2xl">
|
||||
<h3 className="text-2xl font-bold text-[#2d3134] mb-4">Book This Offer</h3>
|
||||
<div className="space-y-4 mb-6">
|
||||
<div className="flex justify-between items-center border-b pb-2">
|
||||
<span className="text-gray-600">Availability</span>
|
||||
<span className="font-medium text-green-600">
|
||||
{safeOffer.offerStatus === 'active' ? 'Available' : 'Unavailable'}
|
||||
</span>
|
||||
</div>
|
||||
{safeOffer.startDateTime && (
|
||||
<div className="flex justify-between items-center border-b pb-2">
|
||||
<span className="text-gray-600">Valid from</span>
|
||||
<span className="font-medium">
|
||||
{new Date(safeOffer.startDateTime).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{safeOffer.endDateTime && (
|
||||
<div className="flex justify-between items-center border-b pb-2">
|
||||
<span className="text-gray-600">Valid until</span>
|
||||
<span className="font-medium">
|
||||
{new Date(safeOffer.endDateTime).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
onClick={onCheckoutClick}
|
||||
className="w-full bg-primary hover:bg-primary/90 text-white font-semibold py-6 text-lg rounded-xl transition-all duration-200"
|
||||
>
|
||||
Proceed to Checkout
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { TrustedCompanies } from '../components/TrustedCompanies';
|
||||
import { Layout } from '../Layout';
|
||||
import { useGetSelectedCityOffersQuery } from '../Redux/services/cities.service';
|
||||
import LoadingSpinner from '../components/LoadingSpinner';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
interface SuperSavingsPageProps {
|
||||
onBackClick: () => void;
|
||||
@@ -113,6 +114,7 @@ export function SuperSavingsPage({
|
||||
user
|
||||
}: SuperSavingsPageProps) {
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [categoryId, setCategoryId] = useState(null)
|
||||
const [page, setPage] = useState(1)
|
||||
const [limit, setLimit] = useState(4)
|
||||
@@ -302,7 +304,8 @@ export function SuperSavingsPage({
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
>
|
||||
<Card className="bg-white border border-gray-200 rounded-xl overflow-hidden h-full hover:shadow-lg transition-shadow duration-300 relative">
|
||||
<Card className="bg-white border border-gray-200 rounded-xl overflow-hidden h-full hover:shadow-lg transition-shadow duration-300 relative cursor-pointer"
|
||||
onClick={()=> navigate(`/super-savings/${offer.id}`)}>
|
||||
{/* Image */}
|
||||
<div className="relative h-52 bg-gray-300">
|
||||
<ImageWithFallback
|
||||
|
||||
Reference in New Issue
Block a user