Files
CityCards-Website/src/components/DealsPage.tsx
priyanshuvish 97969c079b new src added
2025-10-09 19:03:24 +05:30

1175 lines
47 KiB
TypeScript

import { useState, useEffect } from 'react';
import { motion } from 'motion/react';
import { Search, Filter, X, Calendar, MapPin, Users, Clock, Star, Tag, Percent, Grid, List, Heart, GitCompare, Share2, Copy, Facebook, Twitter, MessageCircle } from 'lucide-react';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Card, CardContent } from './ui/card';
import { Badge } from './ui/badge';
import { Checkbox } from './ui/checkbox';
import { Slider } from './ui/slider';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogDescription } from './ui/dialog';
import { toast } from 'sonner@2.0.3';
import Navbar from './Navbar';
import { EnhancedTestimonials } from './EnhancedTestimonials';
import { Footer } from './Footer';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { MobileAppSection } from './MobileAppSection';
import { HowItWorks } from './HowItWorks';
interface Deal {
id: string;
title: string;
description: string;
image: string;
originalPrice: number;
discountedPrice: number;
discountPercentage: number;
city: string;
category: string;
validUntil: string;
rating: number;
reviewsCount: number;
duration: string;
isPopular?: boolean;
isFeatured?: boolean;
attractions: number;
}
const deals: Deal[] = [
{
id: '1',
title: 'Melbourne Explorer Package',
description: '15% OFF on dining and dining on purchase over $200. Valid only today.',
image: 'https://images.unsplash.com/photo-1527264935190-1401c51b5bbc?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx0cmF2ZWwlMjBkZWFscyUyMGRpc2NvdW50JTIwcGFja2FnZXN8ZW58MXx8fHwxNzU3NjY0MzgyfDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
originalPrice: 120,
discountedPrice: 102,
discountPercentage: 15,
city: 'Melbourne',
category: 'dining',
validUntil: '2024-01-15',
rating: 4.8,
reviewsCount: 245,
duration: '3-5 days',
isPopular: true,
attractions: 25
},
{
id: '2',
title: 'Sydney Harbour Special',
description: '20% OFF on dining and dining on purchase over $150. Valid for 7 days.',
image: 'https://images.unsplash.com/photo-1649633564453-358048b0663d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxjaXR5JTIwdHJhdmVsJTIwb2ZmZXJzJTIwdmFjYXRpb258ZW58MXx8fHwxNzU3NjY0Mzg2fDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
originalPrice: 150,
discountedPrice: 120,
discountPercentage: 20,
city: 'Sydney',
category: 'sightseeing',
validUntil: '2024-01-22',
rating: 4.7,
reviewsCount: 189,
duration: '2-4 days',
isFeatured: true,
attractions: 18
},
{
id: '3',
title: 'Paris City Pass Offer',
description: '25% OFF on dining and dining on purchase over $300. Limited time offer.',
image: 'https://images.unsplash.com/photo-1708885818420-584b7fdfeb96?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzcGVjaWFsJTIwb2ZmZXJzJTIwdG91cmlzbSUyMHNhdmluZ3N8ZW58MXx8fHwxNzU3NjY0MzkwfDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
originalPrice: 200,
discountedPrice: 150,
discountPercentage: 25,
city: 'Paris',
category: 'culture',
validUntil: '2024-01-30',
rating: 4.9,
reviewsCount: 312,
duration: '4-6 days',
isPopular: true,
attractions: 35
},
{
id: '4',
title: 'London Heritage Bundle',
description: '18% OFF on dining and dining on purchase over $180. Weekend special.',
image: 'https://images.unsplash.com/photo-1486299267070-83823f5448dd?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxsb25kb24lMjBiaWclMjBiZW58ZW58MXx8fHwxNzU3NjQ3OTYxfDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
originalPrice: 180,
discountedPrice: 147,
discountPercentage: 18,
city: 'London',
category: 'heritage',
validUntil: '2024-01-20',
rating: 4.6,
reviewsCount: 156,
duration: '3-5 days',
attractions: 22
},
{
id: '5',
title: 'Tokyo Adventure Pack',
description: '30% OFF on dining and dining on purchase over $250. Flash sale ending soon.',
image: 'https://images.unsplash.com/photo-1588486691401-93624c48459b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx0b2t5byUyMGNpdHklMjBsYW5kc2NhcGV8ZW58MXx8fHwxNzU3NjYzNDk5fDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
originalPrice: 250,
discountedPrice: 175,
discountPercentage: 30,
city: 'Tokyo',
category: 'adventure',
validUntil: '2024-01-18',
rating: 4.8,
reviewsCount: 278,
duration: '5-7 days',
isFeatured: true,
isPopular: true,
attractions: 42
},
{
id: '6',
title: 'Rome Classic Tour',
description: '22% OFF on dining and dining on purchase over $160. Early bird special.',
image: 'https://images.unsplash.com/photo-1706884027668-4b2a1a9701ed?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxyb21lJTIwY29sb3NzZXVtJTIwYW5jaWVudHxlbnwxfHx8fDE3NTc2Mzk3NDN8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
originalPrice: 160,
discountedPrice: 125,
discountPercentage: 22,
city: 'Rome',
category: 'culture',
validUntil: '2024-02-01',
rating: 4.7,
reviewsCount: 203,
duration: '3-4 days',
attractions: 28
},
{
id: '7',
title: 'Barcelona Arts Package',
description: '16% OFF on dining and dining on purchase over $140. Art lovers special.',
image: 'https://images.unsplash.com/photo-1539037116277-4db20889f2d4?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxiYXJjZWxvbmElMjBjaXR5fGVufDF8fHx8MTc1NzY2MzUyOXww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
originalPrice: 140,
discountedPrice: 118,
discountPercentage: 16,
city: 'Barcelona',
category: 'culture',
validUntil: '2024-01-25',
rating: 4.5,
reviewsCount: 167,
duration: '2-4 days',
attractions: 19
},
{
id: '8',
title: 'New York City Lights',
description: '35% OFF on dining and dining on purchase over $400. Mega sale event.',
image: 'https://images.unsplash.com/photo-1496442226666-8d4d0e62e6e9?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxuZXclMjB5b3JrJTIwY2l0eXxlbnwxfHx8fDE3NTc2NjM1MzN8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
originalPrice: 400,
discountedPrice: 260,
discountPercentage: 35,
city: 'New York',
category: 'adventure',
validUntil: '2024-01-31',
rating: 4.9,
reviewsCount: 456,
duration: '5-8 days',
isPopular: true,
isFeatured: true,
attractions: 55
},
{
id: '9',
title: 'Amsterdam Canal Tour',
description: '12% OFF on dining and dining on purchase over $120. Seasonal discount.',
image: 'https://images.unsplash.com/photo-1534351590666-13e3e96b5017?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxhbXN0ZXJkYW0lMjBjYW5hbHN8ZW58MXx8fHwxNzU3NjY0NDI1fDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
originalPrice: 120,
discountedPrice: 106,
discountPercentage: 12,
city: 'Amsterdam',
category: 'sightseeing',
validUntil: '2024-02-05',
rating: 4.4,
reviewsCount: 134,
duration: '2-3 days',
attractions: 15
},
{
id: '10',
title: 'Berlin History Walk',
description: '28% OFF on dining and dining on purchase over $200. History buffs delight.',
image: 'https://images.unsplash.com/photo-1587330979470-3595ac045ab1?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxiZXJsaW4lMjBjaXR5fGVufDF8fHx8MTc1NzY2NDQyOXww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
originalPrice: 200,
discountedPrice: 144,
discountPercentage: 28,
city: 'Berlin',
category: 'heritage',
validUntil: '2024-01-28',
rating: 4.6,
reviewsCount: 198,
duration: '3-5 days',
attractions: 24
},
{
id: '11',
title: 'Prague Castle Experience',
description: '19% OFF on dining and dining on purchase over $110. Medieval charm package.',
image: 'https://images.unsplash.com/photo-1541849546-216549ae216d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxwcmFndWUlMjBjYXN0bGV8ZW58MXx8fHwxNzU3NjY0NDMzfDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
originalPrice: 110,
discountedPrice: 89,
discountPercentage: 19,
city: 'Prague',
category: 'heritage',
validUntil: '2024-02-10',
rating: 4.7,
reviewsCount: 176,
duration: '2-4 days',
attractions: 17
},
{
id: '12',
title: 'Vienna Music & Arts',
description: '24% OFF on dining and dining on purchase over $190. Classical elegance deal.',
image: 'https://images.unsplash.com/photo-1516550893923-42d28e5677af?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx2aWVubmElMjBjaXR5fGVufDF8fHx8MTc1NzY2NDQzN3ww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
originalPrice: 190,
discountedPrice: 144,
discountPercentage: 24,
city: 'Vienna',
category: 'culture',
validUntil: '2024-02-15',
rating: 4.8,
reviewsCount: 221,
duration: '3-5 days',
attractions: 26
}
];
interface DealsPageProps {
onBackClick: () => void;
onHomeClick: () => void;
onMelbourneClick: () => void;
onPassesClick: () => void;
onCitiesClick: () => void;
onDealsClick: () => void;
onCheckoutClick: () => void;
onSignInClick: () => void;
onAttractionsClick: () => void;
onBlogsClick: () => void;
onHowItWorksClick: () => void;
onFAQClick: () => void;
onPrivacyPolicyClick: () => void;
onAboutUsClick: () => void;
onProfileClick: () => void;
onDealClick?: (dealId: string) => void;
currentPage: string;
}
export function DealsPage({
onBackClick,
onHomeClick,
onMelbourneClick,
onPassesClick,
onCitiesClick,
onDealsClick,
onCheckoutClick,
onSignInClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onProfileClick,
onDealClick,
currentPage
}: DealsPageProps) {
const [timeRemaining, setTimeRemaining] = useState('24:59:32');
// Countdown timer effect
useEffect(() => {
const targetTime = new Date();
targetTime.setHours(23, 59, 59); // End of day
const updateTimer = () => {
const now = new Date();
const diff = targetTime.getTime() - now.getTime();
if (diff > 0) {
const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
setTimeRemaining(`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`);
} else {
setTimeRemaining('00:00:00');
}
};
const interval = setInterval(updateTimer, 1000);
updateTimer(); // Initial call
return () => clearInterval(interval);
}, []);
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const [selectedCities, setSelectedCities] = useState<string[]>([]);
const [priceRange, setPriceRange] = useState([0, 500]);
const [discountRange, setDiscountRange] = useState([0, 40]);
const [sortBy, setSortBy] = useState('featured');
const [showFavoritesOnly, setShowFavoritesOnly] = useState(false);
const [showMobileFilters, setShowMobileFilters] = useState(false);
const [pageNumber, setPageNumber] = useState(1);
const [itemsPerPage] = useState(9);
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const [favoriteDeals, setFavoriteDeals] = useState<string[]>([]);
const [compareMode, setCompareMode] = useState(false);
const [selectedDeals, setSelectedDeals] = useState<string[]>([]);
const [showShareModal, setShowShareModal] = useState(false);
const [selectedDealForShare, setSelectedDealForShare] = useState<string>('');
const categories = [
{ value: 'dining', label: 'Dining', count: 3 },
{ value: 'sightseeing', label: 'Sightseeing', count: 2 },
{ value: 'culture', label: 'Culture', count: 4 },
{ value: 'heritage', label: 'Heritage', count: 3 },
{ value: 'adventure', label: 'Adventure', count: 2 }
];
const cities = [
{ value: 'Melbourne', label: 'Melbourne', count: 1 },
{ value: 'Sydney', label: 'Sydney', count: 1 },
{ value: 'Paris', label: 'Paris', count: 1 },
{ value: 'London', label: 'London', count: 1 },
{ value: 'Tokyo', label: 'Tokyo', count: 1 },
{ value: 'Rome', label: 'Rome', count: 1 },
{ value: 'Barcelona', label: 'Barcelona', count: 1 },
{ value: 'New York', label: 'New York', count: 1 },
{ value: 'Amsterdam', label: 'Amsterdam', count: 1 },
{ value: 'Berlin', label: 'Berlin', count: 1 },
{ value: 'Prague', label: 'Prague', count: 1 },
{ value: 'Vienna', label: 'Vienna', count: 1 }
];
const filteredDeals = deals.filter(deal => {
const matchesSearch = deal.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
deal.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
deal.city.toLowerCase().includes(searchQuery.toLowerCase());
const matchesCategory = selectedCategories.length === 0 || selectedCategories.includes(deal.category);
const matchesCity = selectedCities.length === 0 || selectedCities.includes(deal.city);
const matchesPrice = deal.discountedPrice >= priceRange[0] && deal.discountedPrice <= priceRange[1];
const matchesDiscount = deal.discountPercentage >= discountRange[0] && deal.discountPercentage <= discountRange[1];
const matchesFavorites = !showFavoritesOnly || favoriteDeals.includes(deal.id);
return matchesSearch && matchesCategory && matchesCity && matchesPrice && matchesDiscount && matchesFavorites;
});
// Sort deals
const sortedDeals = [...filteredDeals].sort((a, b) => {
switch (sortBy) {
case 'discount':
return b.discountPercentage - a.discountPercentage;
case 'price-low':
return a.discountedPrice - b.discountedPrice;
case 'price-high':
return b.discountedPrice - a.discountedPrice;
case 'rating':
return b.rating - a.rating;
case 'expiry':
return getDaysRemaining(a.validUntil) - getDaysRemaining(b.validUntil);
case 'featured':
default:
return (b.isFeatured ? 1 : 0) - (a.isFeatured ? 1 : 0) || (b.isPopular ? 1 : 0) - (a.isPopular ? 1 : 0);
}
});
// Pagination logic
const totalPages = Math.ceil(sortedDeals.length / itemsPerPage);
const startIndex = (pageNumber - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const currentDeals = sortedDeals.slice(startIndex, endIndex);
const totalItems = sortedDeals.length;
const showingFrom = totalItems > 0 ? startIndex + 1 : 0;
const showingTo = Math.min(endIndex, totalItems);
// Reset to first page when filters change
useEffect(() => {
setPageNumber(1);
}, [searchQuery, selectedCategories, selectedCities, priceRange, discountRange, sortBy]);
const toggleCategory = (category: string) => {
setSelectedCategories(prev =>
prev.includes(category)
? prev.filter(c => c !== category)
: [...prev, category]
);
};
const toggleCity = (city: string) => {
setSelectedCities(prev =>
prev.includes(city)
? prev.filter(c => c !== city)
: [...prev, city]
);
};
const clearAllFilters = () => {
setSelectedCategories([]);
setSelectedCities([]);
setPriceRange([0, 500]);
setDiscountRange([0, 40]);
setSearchQuery('');
setSortBy('featured');
setShowFavoritesOnly(false);
};
const toggleFavorite = (dealId: string) => {
setFavoriteDeals(prev =>
prev.includes(dealId)
? prev.filter(id => id !== dealId)
: [...prev, dealId]
);
toast.success(favoriteDeals.includes(dealId) ? 'Removed from favorites' : 'Added to favorites');
};
const toggleCompareSelection = (dealId: string) => {
if (selectedDeals.includes(dealId)) {
setSelectedDeals(prev => prev.filter(id => id !== dealId));
} else if (selectedDeals.length < 3) {
setSelectedDeals(prev => [...prev, dealId]);
} else {
toast.error('You can only compare up to 3 deals');
}
};
const shareDeal = (dealId: string, platform: string) => {
const deal = deals.find(d => d.id === dealId);
if (!deal) return;
const url = `${window.location.origin}/deals/${dealId}`;
const text = `Check out this amazing deal: ${deal.title} - ${deal.discountPercentage}% OFF!`;
switch (platform) {
case 'copy':
navigator.clipboard.writeText(url);
toast.success('Link copied to clipboard!');
break;
case 'facebook':
window.open(`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`, '_blank');
break;
case 'twitter':
window.open(`https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(url)}`, '_blank');
break;
case 'whatsapp':
window.open(`https://wa.me/?text=${encodeURIComponent(text + ' ' + url)}`, '_blank');
break;
}
setShowShareModal(false);
};
const getDaysRemaining = (validUntil: string) => {
const today = new Date();
const expiry = new Date(validUntil);
const diffTime = expiry.getTime() - today.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return diffDays;
};
const FilterSidebar = ({ isMobile = false }) => (
<div className={`bg-white ${isMobile ? 'p-6' : 'p-6 sticky top-44'} rounded-lg border border-gray-100 h-fit`}>
<div className="flex items-center justify-between mb-6">
<h3 className="font-semibold text-gray-900">Search by</h3>
{isMobile && (
<Button
variant="ghost"
size="sm"
onClick={() => setShowMobileFilters(false)}
>
<X className="w-4 h-4" />
</Button>
)}
</div>
{/* Quick Filters */}
<div className="mb-6">
<h4 className="font-medium text-gray-900 mb-3">Quick Filters</h4>
<div className="space-y-3">
<div className="flex items-center space-x-3">
<Checkbox
id="favorites-only"
checked={showFavoritesOnly}
onCheckedChange={(checked) => setShowFavoritesOnly(checked === true)}
/>
<label
htmlFor="favorites-only"
className="text-sm text-gray-700 cursor-pointer flex-1 flex items-center gap-1"
>
<Heart className={`w-4 h-4 ${showFavoritesOnly ? 'fill-red-500 text-red-500' : ''}`} />
My Favorites ({favoriteDeals.length})
</label>
</div>
<div className="flex items-center space-x-3">
<Checkbox
id="expiring-soon"
checked={selectedCategories.includes('expiring-soon')}
onCheckedChange={() => {
// Filter deals expiring in 3 days or less
if (selectedCategories.includes('expiring-soon')) {
setSelectedCategories(prev => prev.filter(c => c !== 'expiring-soon'));
} else {
setSelectedCategories(prev => [...prev, 'expiring-soon']);
}
}}
/>
<label
htmlFor="expiring-soon"
className="text-sm text-gray-700 cursor-pointer flex-1 flex items-center gap-1"
>
<Clock className="w-4 h-4 text-orange-600" />
Ending Soon
</label>
</div>
</div>
</div>
{/* Category Filter */}
<div className="mb-6">
<h4 className="font-medium text-gray-900 mb-3">Rating (5)</h4>
<div className="space-y-3">
{categories.map(category => (
<div key={category.value} className="flex items-center space-x-3">
<Checkbox
id={category.value}
checked={selectedCategories.includes(category.value)}
onCheckedChange={() => toggleCategory(category.value)}
/>
<label
htmlFor={category.value}
className="text-sm text-gray-700 cursor-pointer flex-1 capitalize"
>
{category.label} ({category.count})
</label>
</div>
))}
</div>
</div>
{/* City Filter */}
<div className="mb-6">
<h4 className="font-medium text-gray-900 mb-3">Brand (5)</h4>
<div className="space-y-3 max-h-48 overflow-y-auto">
{cities.map(city => (
<div key={city.value} className="flex items-center space-x-3">
<Checkbox
id={city.value}
checked={selectedCities.includes(city.value)}
onCheckedChange={() => toggleCity(city.value)}
/>
<label
htmlFor={city.value}
className="text-sm text-gray-700 cursor-pointer flex-1"
>
{city.label} ({city.count})
</label>
</div>
))}
</div>
</div>
{/* Price Range */}
<div className="mb-6">
<h4 className="font-medium text-gray-900 mb-3">Price ($)</h4>
<div className="px-3">
<Slider
value={priceRange}
onValueChange={setPriceRange}
min={0}
max={500}
step={10}
className="mb-3"
/>
<div className="flex justify-between text-sm text-gray-600">
<span>${priceRange[0]}</span>
<span>${priceRange[1]}</span>
</div>
</div>
</div>
{/* Discount Range */}
<div className="mb-6">
<h4 className="font-medium text-gray-900 mb-3">Discount (%)</h4>
<div className="px-3">
<Slider
value={discountRange}
onValueChange={setDiscountRange}
min={0}
max={40}
step={1}
className="mb-3"
/>
<div className="flex justify-between text-sm text-gray-600">
<span>{discountRange[0]}%</span>
<span>{discountRange[1]}%</span>
</div>
</div>
</div>
{/* Clear filters */}
<Button
variant="outline"
onClick={clearAllFilters}
className="w-full"
>
Clear All Filters
</Button>
</div>
);
return (
<div className="min-h-screen bg-background">
<Navbar
activeCity=""
onCityChange={() => {}}
onSignInClick={onSignInClick}
onPassesClick={onPassesClick}
onCitiesClick={onCitiesClick}
onDealsClick={() => {}} // Already on deals page
onCheckoutClick={onCheckoutClick}
onHomeClick={onHomeClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
currentPage="deals"
isUserSignedIn={!!user}
user={user}
/>
<div className="container mx-auto px-4 py-8">
<div className="flex flex-col lg:flex-row gap-8 pt-20">
{/* Desktop Sidebar */}
<div className="hidden lg:block lg:w-1/4">
<FilterSidebar />
</div>
{/* Main Content */}
<div className="flex-1">
{/* Promotional Banner */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="mb-8 bg-gradient-to-r from-primary to-secondary pt-16 px-6 pb-6 rounded-lg text-white relative overflow-hidden"
>
<div className="absolute inset-0 bg-gradient-to-r from-primary/90 to-secondary/90"></div>
<div className="relative z-10">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold mb-2"> Flash Sale Alert!</h2>
<p className="text-white/90">Limited time offers ending soon - Save up to 35% on select city passes!</p>
</div>
<div className="text-right">
<div className="text-sm text-white/80 mb-1">Ends in</div>
<div className="text-xl font-bold font-mono">{timeRemaining}</div>
</div>
</div>
</div>
</motion.div>
{/* Header */}
<div className="mb-8">
<h1 className="mb-2">
<span className="font-light">Offers</span>{' '}
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">for you</span>
</h1>
<p className="text-gray-600">
Showing {totalItems} of {deals.length} result(s)
</p>
</div>
{/* Search and Sort Controls */}
<div className="flex flex-col lg:flex-row gap-4 mb-8">
{/* Search Bar */}
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
<Input
placeholder="Search offers..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 bg-white border-gray-200"
/>
</div>
<div className="flex items-center gap-4">
{/* View Mode Toggle */}
<div className="flex items-center bg-gray-100 rounded-lg p-1">
<Button
variant={viewMode === 'grid' ? 'default' : 'ghost'}
size="sm"
onClick={() => setViewMode('grid')}
className="px-3"
>
<Grid className="w-4 h-4" />
</Button>
<Button
variant={viewMode === 'list' ? 'default' : 'ghost'}
size="sm"
onClick={() => setViewMode('list')}
className="px-3"
>
<List className="w-4 h-4" />
</Button>
</div>
{/* Compare Mode Toggle */}
<Button
variant={compareMode ? 'default' : 'outline'}
size="sm"
onClick={() => {
setCompareMode(!compareMode);
if (!compareMode) {
setSelectedDeals([]);
}
}}
className="flex items-center gap-2"
>
<GitCompare className="w-4 h-4" />
Compare {selectedDeals.length > 0 && `(${selectedDeals.length})`}
</Button>
{/* Sort Dropdown */}
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-40">
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
<SelectItem value="featured">Featured</SelectItem>
<SelectItem value="discount">Highest Discount</SelectItem>
<SelectItem value="price-low">Price: Low to High</SelectItem>
<SelectItem value="price-high">Price: High to Low</SelectItem>
<SelectItem value="rating">Highest Rated</SelectItem>
<SelectItem value="expiry">Ending Soon</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Mobile Filter Button */}
<div className="lg:hidden mb-6">
<Button
variant="outline"
onClick={() => setShowMobileFilters(true)}
className="w-full"
>
<Filter className="w-4 h-4 mr-2" />
Filters ({selectedCategories.length + selectedCities.length})
</Button>
</div>
{/* Compare Bar */}
{compareMode && selectedDeals.length > 0 && (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-6 bg-primary/5 border border-primary/20 rounded-lg p-4"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<GitCompare className="w-5 h-5 text-primary" />
<span className="font-medium">
{selectedDeals.length} deal{selectedDeals.length !== 1 ? 's' : ''} selected for comparison
</span>
</div>
<div className="flex items-center gap-3">
{selectedDeals.length >= 2 && (
<Button size="sm" className="bg-primary hover:bg-primary/90">
Compare Selected
</Button>
)}
<Button
variant="outline"
size="sm"
onClick={() => {
setSelectedDeals([]);
setCompareMode(false);
}}
>
Clear
</Button>
</div>
</div>
</motion.div>
)}
{/* Results Count */}
<div className="mb-6">
<p className="text-gray-600">
Showing {showingFrom}-{showingTo} of {totalItems} offer(s)
</p>
</div>
{/* Deals Grid */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.2 }}
className={viewMode === 'grid'
? "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
: "space-y-4"
}
>
{currentDeals.map((deal, index) => {
const daysRemaining = getDaysRemaining(deal.validUntil);
const isExpiringSoon = daysRemaining <= 3;
const isFavorite = favoriteDeals.includes(deal.id);
const isSelected = selectedDeals.includes(deal.id);
return (
<motion.div
key={deal.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<Card
className={`group cursor-pointer interactive-card h-full overflow-hidden relative ${
isSelected ? 'ring-2 ring-primary ring-offset-2' : ''
} ${viewMode === 'list' ? 'md:flex md:h-48' : ''}`}
onClick={() => !compareMode && onDealClick?.(deal.id)}
>
<CardContent className="p-0">
<div className={`relative ${viewMode === 'list' ? 'md:w-80 md:flex-shrink-0' : ''}`}>
<ImageWithFallback
src={deal.image}
alt={deal.title}
className={`w-full object-cover ${viewMode === 'list' ? 'h-48 md:h-full' : 'h-48'}`}
/>
{/* Action Buttons */}
<div className="absolute top-3 right-3 flex flex-col gap-2">
{/* Favorite Button */}
<Button
size="sm"
variant="secondary"
className="w-8 h-8 p-0 bg-white/90 hover:bg-white"
onClick={(e) => {
e.stopPropagation();
toggleFavorite(deal.id);
}}
>
<Heart
className={`w-4 h-4 ${isFavorite ? 'fill-red-500 text-red-500' : 'text-gray-600'}`}
/>
</Button>
{/* Share Button */}
<Dialog open={showShareModal && selectedDealForShare === deal.id} onOpenChange={setShowShareModal}>
<DialogTrigger asChild>
<Button
size="sm"
variant="secondary"
className="w-8 h-8 p-0 bg-white/90 hover:bg-white"
onClick={(e) => {
e.stopPropagation();
setSelectedDealForShare(deal.id);
setShowShareModal(true);
}}
>
<Share2 className="w-4 h-4 text-gray-600" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Share this deal</DialogTitle>
<DialogDescription>
Choose how you'd like to share this deal with others.
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-3">
<Button
variant="outline"
onClick={() => shareDeal(deal.id, 'copy')}
className="flex items-center gap-2 justify-start"
>
<Copy className="w-4 h-4" />
Copy Link
</Button>
<Button
variant="outline"
onClick={() => shareDeal(deal.id, 'facebook')}
className="flex items-center gap-2 justify-start"
>
<Facebook className="w-4 h-4" />
Share on Facebook
</Button>
<Button
variant="outline"
onClick={() => shareDeal(deal.id, 'twitter')}
className="flex items-center gap-2 justify-start"
>
<Twitter className="w-4 h-4" />
Share on Twitter
</Button>
<Button
variant="outline"
onClick={() => shareDeal(deal.id, 'whatsapp')}
className="flex items-center gap-2 justify-start"
>
<MessageCircle className="w-4 h-4" />
Share on WhatsApp
</Button>
</div>
</DialogContent>
</Dialog>
{/* Compare Checkbox */}
{compareMode && (
<Button
size="sm"
variant={isSelected ? "default" : "secondary"}
className="w-8 h-8 p-0"
onClick={(e) => {
e.stopPropagation();
toggleCompareSelection(deal.id);
}}
>
<GitCompare className="w-4 h-4" />
</Button>
)}
</div>
{/* Discount Badge */}
<Badge className="absolute top-3 left-3 bg-red-600 text-white font-bold px-3 py-1">
-{deal.discountPercentage}% OFF
</Badge>
{/* Expiry Warning */}
{isExpiringSoon && (
<Badge className="absolute top-12 left-3 bg-orange-600 text-white text-xs px-2 py-1">
{daysRemaining === 0 ? 'Expires Today!' : `${daysRemaining} days left`}
</Badge>
)}
{/* Popular/Featured Badges */}
<div className="absolute bottom-3 left-3 flex items-center gap-2">
<div className="bg-white rounded-full px-2 py-1 shadow-sm">
<div className="flex items-center gap-1">
<Star className="w-3 h-3 fill-current text-yellow-500" />
<span className="text-xs font-medium">{deal.rating}</span>
</div>
</div>
{deal.isFeatured && (
<Badge className="bg-orange-600 text-white text-xs">
Featured
</Badge>
)}
{deal.isPopular && (
<Badge className="bg-primary text-white text-xs">
Popular
</Badge>
)}
</div>
</div>
<div className={`p-4 ${viewMode === 'list' ? 'md:flex-1 md:flex md:flex-col md:justify-between' : ''}`}>
<div className={viewMode === 'list' ? 'md:flex-1' : ''}>
<div className="mb-3">
<div className="flex items-center gap-2 mb-2">
<MapPin className="w-4 h-4 text-gray-500" />
<span className="text-sm text-gray-500">{deal.city}</span>
<Badge variant="secondary" className="text-xs capitalize">
{deal.category}
</Badge>
</div>
<h3 className="font-semibold text-gray-900 group-hover:text-primary transition-colors mb-1">
{deal.title}
</h3>
</div>
<p className="text-sm text-gray-600 mb-4 line-clamp-2">
{deal.description}
</p>
<div className="flex items-center gap-4 text-sm text-gray-500 mb-4">
<div className="flex items-center gap-1">
<Clock className="w-4 h-4" />
<span>{deal.duration}</span>
</div>
<div className="flex items-center gap-1">
<Users className="w-4 h-4" />
<span>{deal.attractions} attractions</span>
</div>
<div className="flex items-center gap-1">
<Calendar className="w-4 h-4" />
<span className={isExpiringSoon ? 'text-red-600 font-medium' : ''}>
{daysRemaining === 0 ? 'Today' : `${daysRemaining} days`}
</span>
</div>
</div>
</div>
<div>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<span className="text-lg font-bold text-gray-900">${deal.discountedPrice}</span>
<span className="text-sm text-gray-500 line-through">${deal.originalPrice}</span>
<Badge variant="outline" className="text-xs font-medium text-green-700 border-green-200">
Save ${deal.originalPrice - deal.discountedPrice}
</Badge>
</div>
<div className="text-xs text-gray-500 text-right">
<div>Valid until</div>
<div className={isExpiringSoon ? 'text-red-600 font-medium' : ''}>
{new Date(deal.validUntil).toLocaleDateString()}
</div>
</div>
</div>
<div className="flex gap-2">
<Button
className="flex-1 bg-gray-900 hover:bg-gray-800 text-white"
onClick={(e) => {
e.stopPropagation();
onDealClick?.(deal.id);
}}
>
VIEW DEAL
</Button>
{viewMode === 'list' && (
<Button
variant="outline"
size="sm"
onClick={(e) => {
e.stopPropagation();
toggleFavorite(deal.id);
}}
>
<Heart className={`w-4 h-4 ${isFavorite ? 'fill-red-500 text-red-500' : ''}`} />
</Button>
)}
</div>
</div>
</div>
</CardContent>
</Card>
</motion.div>
);
})}
</motion.div>
{totalItems === 0 && (
<div className="text-center py-12">
<p className="text-gray-500 mb-4">No deals found matching your criteria</p>
<Button onClick={clearAllFilters} variant="outline">
Clear All Filters
</Button>
</div>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="mt-12 flex flex-col sm:flex-row items-center justify-between gap-4">
<div className="text-sm text-gray-600">
Page {pageNumber} of {totalPages}
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
onClick={() => setPageNumber(Math.max(1, pageNumber - 1))}
disabled={pageNumber === 1}
className="px-3 py-2"
>
Previous
</Button>
{/* Page numbers */}
<div className="flex items-center gap-1">
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
let pageNum;
if (totalPages <= 5) {
pageNum = i + 1;
} else if (pageNumber <= 3) {
pageNum = i + 1;
} else if (pageNumber >= totalPages - 2) {
pageNum = totalPages - 4 + i;
} else {
pageNum = pageNumber - 2 + i;
}
return (
<Button
key={pageNum}
variant={pageNumber === pageNum ? "default" : "outline"}
onClick={() => setPageNumber(pageNum)}
className="px-3 py-2 min-w-[40px]"
>
{pageNum}
</Button>
);
})}
</div>
<Button
variant="outline"
onClick={() => setPageNumber(Math.min(totalPages, pageNumber + 1))}
disabled={pageNumber === totalPages}
className="px-3 py-2"
>
Next
</Button>
</div>
</div>
)}
</div>
</div>
</div>
{/* Mobile Filter Modal */}
{showMobileFilters && (
<div className="fixed inset-0 z-50 lg:hidden">
<div className="fixed inset-0 bg-black bg-opacity-50" onClick={() => setShowMobileFilters(false)} />
<div className="fixed inset-y-0 left-0 w-80 bg-white overflow-y-auto">
<FilterSidebar isMobile />
</div>
</div>
)}
{/* Access all your city cards section */}
<MobileAppSection />
{/* How It Works Section */}
<HowItWorks />
{/* Customer Reviews Section */}
<EnhancedTestimonials />
<Footer
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}
onSignInClick={onSignInClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
currentPage={currentPage}
/>
{/* Floating Favorites Button */}
{favoriteDeals.length > 0 && !showFavoritesOnly && (
<motion.div
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="fixed bottom-6 left-6 z-40"
>
<Button
onClick={() => setShowFavoritesOnly(true)}
className="bg-red-600 hover:bg-red-700 text-white rounded-full w-14 h-14 shadow-lg"
>
<div className="flex flex-col items-center">
<Heart className="w-5 h-5 fill-current" />
<span className="text-xs">{favoriteDeals.length}</span>
</div>
</Button>
</motion.div>
)}
</div>
);
}