1175 lines
47 KiB
TypeScript
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>
|
|
);
|
|
}
|