import React, { useEffect, useMemo, useRef, useState } from "react";
import { ALL_SEED_PACKAGES } from "../data/seedPackages";
import assetUrl from "../utils/assetUrl";
import travelArrow from "/assets/rethink-ways-travel-arrow.svg";
function WhatsappMark() {
return (
<svg aria-hidden="true" viewBox="0 0 32 32" className="h-[16px] w-[16px] shrink-0 fill-white" xmlns="http://www.w3.org/2000/svg">
<path d="M26.576 5.363c-2.69-2.69-6.406-4.354-10.511-4.354-8.209 0-14.865 6.655-14.865 14.865 0 2.732 0.737 5.291 2.022 7.491l-0.038-0.07-2.109 7.702 7.879-2.067c2.051 1.139 4.498 1.809 7.102 1.809h0.006c8.209-0.003 14.862-6.659 14.862-14.868 0-4.103-1.662-7.817-4.349-10.507l0 0zM16.062 28.228h-0.005c-0 0-0.001 0-0.001 0-2.319 0-4.489-0.64-6.342-1.753l0.056 0.031-0.451-0.267-4.675 1.227 1.247-4.559-0.294-0.467c-1.185-1.862-1.889-4.131-1.889-6.565 0-6.822 5.531-12.353 12.353-12.353s12.353 5.531 12.353 12.353c0 6.822-5.53 12.353-12.353 12.353h-0zM22.838 18.977c-0.371-0.186-2.197-1.083-2.537-1.208-0.341-0.124-0.589-0.185-0.837 0.187-0.246 0.371-0.958 1.207-1.175 1.455-0.216 0.249-0.434 0.279-0.805 0.094-1.15-0.466-2.138-1.087-2.997-1.852l0.01 0.009c-0.799-0.74-1.484-1.587-2.037-2.521l-0.028-0.052c-0.216-0.371-0.023-0.572 0.162-0.757 0.167-0.166 0.372-0.434 0.557-0.65 0.146-0.179 0.271-0.384 0.366-0.604l0.006-0.017c0.043-0.087 0.068-0.188 0.068-0.296 0-0.131-0.037-0.253-0.101-0.357l0.002 0.003c-0.094-0.186-0.836-2.014-1.145-2.758-0.302-0.724-0.609-0.625-0.836-0.637-0.216-0.010-0.464-0.012-0.712-0.012-0.395 0.010-0.746 0.188-0.988 0.463l-0.001 0.002c-0.802 0.761-1.3 1.834-1.3 3.023 0 0.026 0 0.053 0.001 0.079l-0-0.004c0.131 1.467 0.681 2.784 1.527 3.857l-0.012-0.015c1.604 2.379 3.742 4.282 6.251 5.564l0.094 0.043c0.548 0.248 1.25 0.513 1.968 0.74l0.149 0.041c0.442 0.14 0.951 0.221 1.479 0.221 0.303 0 0.601-0.027 0.889-0.078l-0.031 0.004c1.069-0.223 1.956-0.868 2.497-1.749l0.009-0.017c0.165-0.366 0.261-0.793 0.261-1.242 0-0.185-0.016-0.366-0.047-0.542l0.003 0.019c-0.092-0.155-0.34-0.247-0.712-0.434z"/>
</svg>
);
}
const REGIONS = [
{ id: "international", label: "International Packages" },
{ id: "india", label: "India Packages" },
{ id: "europe", label: "Europe Packages" },
];
const DURATIONS = [
{ id: "1-3", label: "1 to 3 Days" },
{ id: "4-6", label: "4 to 6 Days" },
{ id: "7-9", label: "7 to 9 Days" },
{ id: "10-12", label: "10 to 12 Days" },
];
const DEFAULT_DURATION = "7-9";
const LIVE_TOUR_PACKAGES_URL = "https://travel.rethinkways.com/tour-packages";
const WHATSAPP_PACKAGE_NUMBER = "918438506813";
const CHEVRON_HEIGHT = 56;
const CHEVRON_TIP = 14;
const CHEVRON_VIEWBOX_WIDTH = 600;
const getLiveTourPackageLink = (destination, sectionCode, destinationSlug) => {
if (sectionCode === "international") {
const slug = destinationSlug || String(destination || "").trim().toLowerCase().replace(/&/g, "and").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
return `https://travel.rethinkways.com/${slug}-tour-packages`;
}
return `${LIVE_TOUR_PACKAGES_URL}?section=${encodeURIComponent(sectionCode)}`;
};
const getWhatsappPackageLink = (destinationName) => {
const message = `Hi! I'm interested in the ${destinationName} tour package on Travel Rethink Ways. Could you share more details?`;
return `https://wa.me/${WHATSAPP_PACKAGE_NUMBER}?text=${encodeURIComponent(message)}`;
};
const getMockDurationCategory = (destinationName) => {
const length = String(destinationName || "").length;
if (length < 5) return "1-3";
if (length < 8) return "4-6";
if (length < 12) return "7-9";
if (length < 15) return "10-12";
return "13+";
};
function useDarkMode() {
const [darkMode, setDarkMode] = useState(() => {
if (typeof document === "undefined") return false;
return document.documentElement.classList.contains("dark");
});
useEffect(() => {
if (typeof document === "undefined") return undefined;
const observer = new MutationObserver(() => {
setDarkMode(document.documentElement.classList.contains("dark"));
});
observer.observe(document.documentElement, { attributeFilter: ["class"] });
return () => observer.disconnect();
}, []);
return darkMode;
}
function chevronPoints(x, width, isFirst, isLast) {
const start = x;
const end = x + width;
const middleY = CHEVRON_HEIGHT / 2;
if (isFirst && isLast) {
return `${start},0 ${end},0 ${end},${CHEVRON_HEIGHT} ${start},${CHEVRON_HEIGHT}`;
}
if (isFirst) {
return `${start},0 ${end - CHEVRON_TIP},0 ${end},${middleY} ${end - CHEVRON_TIP},${CHEVRON_HEIGHT} ${start},${CHEVRON_HEIGHT}`;
}
if (isLast) {
return `${start},0 ${end},0 ${end},${CHEVRON_HEIGHT} ${start},${CHEVRON_HEIGHT} ${start + CHEVRON_TIP},${middleY}`;
}
return `${start},0 ${end - CHEVRON_TIP},0 ${end},${middleY} ${end - CHEVRON_TIP},${CHEVRON_HEIGHT} ${start},${CHEVRON_HEIGHT} ${start + CHEVRON_TIP},${middleY}`;
}
function ReadMoreExpandable() {
return (
<div className="group/view-package inline-flex h-[40px] shrink-0 items-center justify-center gap-2 rounded-full border-2 border-[#D02525] bg-[#D02525] px-4 text-white no-underline transition-all duration-300 hover:scale-[1.03] hover:border-[#D02525] hover:bg-black">
<span className="font-poppins text-[11px] font-semibold leading-none uppercase tracking-[0.2px] text-white sm:hidden">
View Packages
</span>
<span className="hidden font-poppins text-[11px] font-semibold leading-none uppercase tracking-[0.2px] text-white sm:inline">
View
</span>
<img
src={travelArrow}
alt=""
aria-hidden="true"
className="h-[14px] w-[14px] shrink-0 rotate-[-45deg] transition-all duration-300 group-hover/view-package:translate-x-[3px] group-hover/view-package:rotate-0"
/>
</div>
);
}
function DurationStepper({ activeDuration, setActiveDuration }) {
const durationMeta = [
{
icon: (
<svg viewBox="0 0 24 24" aria-hidden="true" className="h-[40px] w-[40px] fill-current text-[#D02525]">
<path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2Zm0 18.25A8.25 8.25 0 1 1 20.25 12 8.26 8.26 0 0 1 12 20.25Zm2.83-11.08-1.1 4.43-4.43 1.1 1.1-4.43ZM12 10.75A1.25 1.25 0 1 0 13.25 12 1.25 1.25 0 0 0 12 10.75Z" />
</svg>
),
title: "1 to 3 Days",
subtitle: "Quick Escape",
},
{
icon: (
<svg
viewBox="0 0 512 512"
aria-hidden="true"
className="h-[40px] w-[40px]"
fill="none"
stroke="#D02525"
strokeWidth="32"
strokeLinecap="square"
strokeLinejoin="miter"
>
<polyline points="352 144 464 144 464 256" />
<polyline points="48 368 192 224 288 320 448 160" />
</svg>
),
title: "4 to 6 Days",
subtitle: "Top Choice",
},
{
icon: (
<svg viewBox="0 0 203.583 203.583" aria-hidden="true" className="h-[40px] w-[40px] fill-current text-[#D02525]">
<path d="M135.521,203.583c-1.381,0-2.5-1.119-2.5-2.5v-2.833h-62.5v2.833c0,1.381-1.119,2.5-2.5,2.5s-2.5-1.119-2.5-2.5v-3.059c-8.061-1.327-14.229-8.344-14.229-16.774V78.75c0-12.958,10.542-23.5,23.5-23.5h2.375V8.5c0-4.687,3.813-8.5,8.5-8.5h32.25c4.687,0,8.5,3.813,8.5,8.5v46.75h2.375c12.958,0,23.5,10.542,23.5,23.5v102.5c0,8.445-6.189,15.472-14.271,16.781v3.052C138.021,202.464,136.902,203.583,135.521,203.583z M70.542,193.25h62.5v-78c0-9.374-7.626-17-17-17h-28.5c-9.374,0-17,7.626-17,17V193.25z M87.542,93.25h28.5c12.131,0,22,9.869,22,22v77.682c5.296-1.247,9.25-6.011,9.25-11.682V78.75c0-10.201-8.299-18.5-18.5-18.5h-54c-10.201,0-18.5,8.299-18.5,18.5v102.5c0,5.671,3.954,10.436,9.25,11.682V115.25C65.542,103.119,75.411,93.25,87.542,93.25z M82.167,55.25h39.25v-42.5h-39.25V55.25z M82.248,7.75h39.088C120.991,6.179,119.589,5,117.917,5h-32.25C83.994,5,82.592,6.179,82.248,7.75z M122.375,155.583H81.208c-1.381,0-2.5-1.119-2.5-2.5s1.119-2.5,2.5-2.5h41.167c1.381,0,2.5,1.119,2.5,2.5S123.756,155.583,122.375,155.583z" />
</svg>
),
title: "7 to 9 Days",
subtitle: "Ideal Duration",
},
{
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 500 500"
aria-hidden="true"
className="h-[40px] w-[40px] fill-current text-[#D02525]"
>
<path d="M291.574,409.834H166.863c-2.39,0-4.446-1.691-4.906-4.037c-0.461-2.345,0.804-4.688,3.016-5.592l50.993-20.822v-63.229 c0-6.841-0.893-13.661-2.653-20.271l-49.156-184.639c-2.563,0.362-5.181,0.549-7.843,0.549c-30.821,0-55.896-25.075-55.896-55.896 S125.492,0,156.313,0c28.396,0,51.916,21.286,55.44,48.739h92.663c1.553,0,3.018,0.722,3.964,1.953 c0.947,1.231,1.268,2.832,0.868,4.333l-64.124,240.856c-1.76,6.611-2.652,13.432-2.652,20.271v63.229l50.992,20.822 c2.212,0.903,3.477,3.247,3.016,5.592C296.02,408.143,293.964,409.834,291.574,409.834z M192.335,399.834h73.769l-30.521-12.463 c-1.88-0.768-3.109-2.598-3.109-4.629v-66.589c0-7.708,1.006-15.394,2.988-22.844l53.661-201.557h-89.96 c-6.595,7.868-15.314,13.901-25.266,17.208l49.08,184.349c1.983,7.448,2.989,15.135,2.989,22.844v66.589 c0,2.031-1.229,3.861-3.109,4.629L192.335,399.834z M156.313,10c-25.308,0-45.896,20.589-45.896,45.896s20.589,45.896,45.896,45.896 c1.773,0,3.525-0.102,5.247-0.298l-12.372-46.47c-0.399-1.501-0.079-3.102,0.868-4.333c0.946-1.231,2.411-1.953,3.964-1.953h47.632 C198.205,26.819,179.186,10,156.313,10z M169.315,91.753l2.002,7.521c4.982-1.728,9.576-4.291,13.615-7.521H169.315z M205.862,81.753h85.922l6.128-23.014h-85.773C211.723,67.007,209.501,74.808,205.862,81.753z M166.652,81.753h27.563 c4.54-6.634,7.386-14.515,7.907-23.014h-41.598L166.652,81.753z M215.414,211.835c-2.209,0-4.229-1.474-4.827-3.709l-23-86 c-0.714-2.668,0.87-5.409,3.538-6.122c2.665-0.718,5.408,0.869,6.122,3.538l23,86c0.714,2.668-0.87,5.409-3.538,6.122 C216.276,211.78,215.842,211.835,215.414,211.835z"/>
</svg>
),
title: "10 to 12 Days",
subtitle: "13+ Days",
}
];
return (
<div className="w-full pb-2">
<div className="mx-auto grid w-full max-w-[420px] grid-cols-2 gap-[10px] overflow-visible px-1 sm:flex sm:max-w-none sm:flex-nowrap sm:justify-center sm:gap-2 sm:px-0">
{durationMeta.map((item, index) => {
const isActive = activeDuration === DURATIONS[index].id;
const mobileWidthClass = "w-full";
const iconCircleClass = "h-[42px] w-[42px] sm:h-[46px] sm:w-[46px]";
const iconWrapClass = "h-[28px] w-[28px] sm:h-[30px] sm:w-[30px]";
return (
<button
key={DURATIONS[index].id}
type="button"
onClick={() => setActiveDuration(DURATIONS[index].id)}
style={{ WebkitTapHighlightColor: "transparent" }}
className={`group inline-flex min-w-0 flex-none items-stretch text-left ${mobileWidthClass} sm:w-fit sm:basis-auto`}
>
{/* Icon circle — always white bg, always red icon, never changes */}
<span className={`relative z-20 flex shrink-0 items-center justify-center rounded-full border border-[#e5e7eb] bg-white ${iconCircleClass}`}>
<span className={`flex items-center justify-center text-[#D02525] ${iconWrapClass}`}>
{item.icon}
</span>
</span>
{/* Pill body — only this changes on hover/active */}
<span
className={`relative z-10 -ml-[16px] flex min-h-[42px] min-w-0 flex-1 flex-col justify-center rounded-r-[12px] border border-l-0 pl-[24px] pr-2.5 transition-colors duration-300 ${
isActive
? "border-[#D02525] bg-[#D02525]"
: "border-[#e5e7eb] bg-white group-hover:border-[#D02525] group-hover:bg-[#D02525]"
}`}
>
<span
className={`block whitespace-normal font-poppins text-[10px] font-semibold leading-tight transition-colors duration-300 ${
isActive
? "text-white"
: "text-[#111111] group-hover:text-white"
}`}
>
{item.title}
</span>
<span
className={`block whitespace-normal font-poppins text-[9px] leading-tight transition-colors duration-300 ${
isActive
? "text-white/75"
: "text-[#6b7280] group-hover:text-white/75"
}`}
>
{item.subtitle}
</span>
</span>
</button>
);
})}
</div>
</div>
);
}
export default function BestSellingPackages() {
const [activeRegion, setActiveRegion] = useState("international");
const [activeDuration, setActiveDuration] = useState(DEFAULT_DURATION);
const [currentPage, setCurrentPage] = useState(0);
const [itemsPerView, setItemsPerView] = useState(3);
const [mobileVisibleCount, setMobileVisibleCount] = useState(4);
const touchStartX = useRef(0);
const touchEndX = useRef(0);
const isPointerDown = useRef(false);
const didDrag = useRef(false);
useEffect(() => {
const handleResize = () => {
setItemsPerView(window.innerWidth < 640 ? 4 : 3);
};
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
useEffect(() => {
setCurrentPage(0);
setMobileVisibleCount(4);
}, [activeRegion, activeDuration, itemsPerView]);
const filteredPackages = useMemo(() => {
return ALL_SEED_PACKAGES.filter((pkg) => {
const regionMatch = activeRegion === "all" || pkg.sectionCode === activeRegion;
const mockCategory = getMockDurationCategory(pkg.destination);
const durationMatch = activeDuration === "all" || mockCategory === activeDuration;
return regionMatch && durationMatch;
});
}, [activeRegion, activeDuration]);
const cardsPerView = itemsPerView;
const startIndex = currentPage;
const isMobile = itemsPerView === 4;
const visiblePackages = isMobile
? filteredPackages.slice(0, mobileVisibleCount)
: filteredPackages.slice(startIndex, startIndex + cardsPerView);
const pageCount = Math.max(1, filteredPackages.length - cardsPerView + 1);
const formatINR = (value) => new Intl.NumberFormat("en-IN").format(value);
const goToNext = () => {
setCurrentPage((page) => Math.min(page + 1, Math.max(pageCount - 1, 0)));
};
const goToPrevious = () => {
setCurrentPage((page) => Math.max(page - 1, 0));
};
const handleSwipeStart = (event) => {
touchStartX.current = event.touches[0].clientX;
touchEndX.current = event.touches[0].clientX;
};
const handleSwipeMove = (event) => {
touchEndX.current = event.touches[0].clientX;
};
const handleSwipeEnd = () => {
const deltaX = touchStartX.current - touchEndX.current;
const swipeThreshold = 40;
if (deltaX > swipeThreshold) {
goToNext();
didDrag.current = true;
} else if (deltaX < -swipeThreshold) {
goToPrevious();
didDrag.current = true;
}
};
const handlePointerDown = (event) => {
if (event.pointerType === "mouse" && event.button !== 0) return;
isPointerDown.current = true;
didDrag.current = false;
touchStartX.current = event.clientX;
touchEndX.current = event.clientX;
if (event.currentTarget.setPointerCapture) {
event.currentTarget.setPointerCapture(event.pointerId);
}
};
const handlePointerMove = (event) => {
if (!isPointerDown.current) return;
touchEndX.current = event.clientX;
if (Math.abs(touchStartX.current - event.clientX) > 8) {
didDrag.current = true;
}
};
const handlePointerUp = () => {
if (!isPointerDown.current) return;
isPointerDown.current = false;
handleSwipeEnd();
};
const handleClickCapture = (event) => {
if (!didDrag.current) return;
event.preventDefault();
event.stopPropagation();
didDrag.current = false;
};
return (
<section className="font-poppins w-full overflow-hidden bg-white transition-colors duration-300 dark:bg-black">
<div className="mx-auto max-w-[720px] rounded-2xl bg-transparent px-4 py-8 transition-colors duration-300 dark:bg-[#111111] sm:bg-[#F8F9FA] sm:px-6 sm:py-7">
<div className="mb-8 flex flex-col gap-5 sm:mb-5 sm:gap-6">
<div className="flex flex-col items-center gap-3 sm:items-center sm:justify-center">
<h1 className="text-center text-2xl font-bold text-[#D02525] dark:text-[#D02525] sm:text-3xl">
Featured Getaways
</h1>
</div>
<div className="flex w-full flex-nowrap items-center justify-center gap-2 overflow-hidden sm:flex-wrap sm:justify-center sm:gap-4">
{REGIONS.map((region) => (
<button
key={region.id}
onClick={() => setActiveRegion(region.id)}
style={{ WebkitTapHighlightColor: "transparent" }}
className={`group inline-flex min-w-0 items-center gap-1 text-[12px] font-medium transition-colors duration-300 sm:text-base ${
activeRegion === region.id
? "text-[#D02525] dark:text-[#D02525]"
: "text-black hover:text-[#D02525] dark:text-white dark:hover:text-[#D02525]"
}`}
>
<span className="relative whitespace-nowrap">
{region.label}
<span
className={`absolute -bottom-0.5 left-0 h-[1.5px] bg-[#D02525] transition-all duration-300 ${
activeRegion === region.id ? "w-full" : "w-0 group-hover:w-full"
}`}
/>
</span>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="#D02525"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className="h-4 w-4 shrink-0 transition-transform duration-300"
>
<path d="M7 17L17 7M7 7h10v10" />
</svg>
</button>
))}
</div>
<DurationStepper activeDuration={activeDuration} setActiveDuration={setActiveDuration} />
</div>
{filteredPackages.length > 0 ? (
<>
<div className={`relative ${isMobile ? "pb-0" : "pr-10"}`}>
<div
className={`relative select-none gap-2 overflow-hidden sm:gap-3 ${isMobile ? "grid grid-cols-2" : "flex w-full"}`}
style={{ touchAction: "pan-y", cursor: "grab" }}
onTouchStart={handleSwipeStart}
onTouchMove={handleSwipeMove}
onTouchEnd={handleSwipeEnd}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPointerCancel={handlePointerUp}
onPointerLeave={handlePointerUp}
onClickCapture={handleClickCapture}
>
{visiblePackages.map((destination) => (
<div
key={destination.id}
className={`relative animate-in fade-in zoom-in-95 duration-500 ${isMobile ? "h-[320px] w-full" : "h-[230px] w-full flex-shrink-0 sm:w-[43%]"}`}
>
<a
href={getLiveTourPackageLink(destination.destination, destination.sectionCode, destination.destinationSlug)}
target="_blank"
rel="noopener noreferrer"
style={{ WebkitTapHighlightColor: "transparent" }}
className="group relative block h-full w-full overflow-hidden rounded-2xl border border-gray-100 bg-white no-underline transition-colors duration-300 focus:outline-none focus:ring-0 dark:border-[#2a2a2a] dark:bg-[#1a1a1a]"
>
<div className="absolute inset-x-0 top-0 h-[66%] overflow-hidden">
<img
src={assetUrl(destination.imageUrl)}
alt={destination.destination}
loading="lazy"
className="h-full w-full object-cover"
/>
<svg
className="absolute bottom-0 right-0"
style={{ width: "100%", height: "230px", maxWidth: "100%", transform: "translateY(0)" }}
viewBox="0 0 200 200"
preserveAspectRatio="xMaxYMax meet"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<radialGradient id={`blobFade-${destination.id}`} cx="100%" cy="100%" r="100%">
<stop offset="0%" stopColor="rgba(0,0,0,1)" />
<stop offset="35%" stopColor="rgba(0,0,0,0.85)" />
<stop offset="50%" stopColor="rgba(0,0,0,0.5)" />
<stop offset="100%" stopColor="rgba(0,0,0,0)" />
</radialGradient>
</defs>
<path d="M200,200 L200,0 A200,200 0 0 0 0,200 Z" fill={`url(#blobFade-${destination.id})`} />
</svg>
<div className="absolute bottom-[8px] right-0 z-10 px-3 pb-2 pt-3 text-right">
<p className="mb-0.5 text-[16px] font-medium leading-none text-white">
Starts From
</p>
<div className="flex items-end justify-end gap-1.5 whitespace-nowrap leading-[1.15]">
<p className="text-[30px] font-[700] leading-none text-[#fff]">
{"\u20B9"}{formatINR(destination.startsFrom)}
</p>
</div>
<span className="mt-1 block whitespace-nowrap text-right text-[11px] font-medium leading-[1.15] text-white">
Per Adult
</span>
</div>
</div>
<div className="absolute inset-x-0 bottom-0 z-10 flex items-center bg-white p-2 transition-colors duration-300 dark:bg-[#1a1a1a] lg:p-3">
<div className={`flex w-full gap-3 ${isMobile ? "flex-col items-center text-center" : "flex-row items-center justify-between lg:gap-4"}`}>
<div className={`min-w-0 ${isMobile ? "w-full" : "flex-1"}`}>
<h3 className={`mb-1 font-poppins text-[18px] font-semibold leading-tight text-[#111827] dark:text-white ${isMobile ? "text-center" : "truncate"}`}>
{destination.destination}
</h3>
<p className={`font-poppins text-[12px] font-medium text-gray-500 dark:text-gray-400 ${isMobile ? "text-center" : "truncate"}`}>
{destination.tagline}
</p>
</div>
{isMobile ? (
<div className="flex w-full justify-center pt-1">
<ReadMoreExpandable />
</div>
) : (
<div className="flex w-fit justify-start">
<ReadMoreExpandable />
</div>
)}
</div>
</div>
</a>
<div className="absolute left-2.5 top-2.5 flex items-center">
<span className="relative z-10 flex h-[32px] w-[32px] shrink-0 items-center justify-center rounded-full bg-[#D02525] text-[13px] font-bold text-white shadow-sm">
{destination.packageCount}+
</span>
<span className="dark:bg-[#2a2a2a] dark:text-white -ml-[10px] flex h-[32px] items-center rounded-r-full border border-gray-200 bg-white pl-[14px] pr-3 text-[12px] font-medium text-[#111827] shadow-sm dark:border-[#3a3a3a]">
Packages
</span>
</div>
<a
href={getWhatsappPackageLink(destination.destination)}
target="_blank"
rel="noopener noreferrer"
aria-label={`Message us on WhatsApp about ${destination.destination}`}
style={{ WebkitTapHighlightColor: "transparent" }}
className="absolute right-3 top-3 z-20 inline-flex h-[36px] w-[36px] items-center justify-center rounded-full bg-[#25D366] shadow-[0_8px_18px_rgba(0,0,0,0.24)] transition-all duration-300 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-black"
>
<WhatsappMark />
</a>
</div>
))}
</div>
{!isMobile && pageCount > 1 ? (
<div className="absolute right-0 top-1/2 flex -translate-y-1/2 flex-col items-center gap-2">
{Array.from({ length: pageCount }).map((_, index) => {
const isActive = index === currentPage;
return (
<button
key={`pager-${index}`}
type="button"
aria-label={`Show package group ${index + 1}`}
onClick={() => setCurrentPage(index)}
className={`h-2 w-2 rounded-full transition-colors duration-300 ${
isActive ? "bg-[#d02525]" : "bg-gray-300/80 dark:bg-gray-600/80"
}`}
/>
);
})}
</div>
) : null}
</div>
<div className="mt-4 flex flex-row items-stretch justify-center gap-3 sm:items-center">
{isMobile && mobileVisibleCount < filteredPackages.length ? (
<button
type="button"
onClick={() => setMobileVisibleCount((count) => Math.min(count + 4, filteredPackages.length))}
className="inline-flex h-[40px] basis-1/2 items-center justify-center rounded-full border-2 border-[#D02525] bg-[#D02525] px-5 text-[13px] font-semibold text-white transition-all duration-300 hover:bg-black hover:text-white sm:basis-auto sm:w-auto"
>
Load More
</button>
) : (
<div />
)}
{isMobile ? (
<a
href={getWhatsappPackageLink(filteredPackages[0]?.destination || "our packages")}
target="_blank"
rel="noopener noreferrer"
aria-label="Plan your holiday on WhatsApp"
style={{ WebkitTapHighlightColor: "transparent" }}
className="inline-flex h-[40px] basis-1/2 items-center justify-center gap-2 rounded-full border-2 border-[#25D366] bg-[#25D366] px-4 text-white no-underline transition-all duration-300 hover:bg-black hover:text-white sm:basis-auto sm:w-auto"
>
<WhatsappMark />
<span className="font-poppins text-[11px] font-semibold uppercase leading-none text-white">
Plan Your Holiday
</span>
</a>
) : pageCount > 1 ? (
<a
href={getWhatsappPackageLink(filteredPackages[0]?.destination || "our packages")}
target="_blank"
rel="noopener noreferrer"
aria-label="Plan your holiday on WhatsApp"
style={{ WebkitTapHighlightColor: "transparent" }}
className="inline-flex h-[40px] w-fit items-center justify-center gap-2 rounded-full border-2 border-[#25D366] bg-[#25D366] px-4 text-white no-underline transition-all duration-300 hover:bg-black hover:text-white"
>
<WhatsappMark />
<span className="font-poppins text-[11px] font-semibold uppercase leading-none text-white sm:hidden">
Plan Your Holiday
</span>
<span className="hidden font-poppins text-[11px] font-semibold uppercase leading-none text-white sm:inline">
Plan Your Holiday
</span>
</a>
) : (
<div />
)}
</div>
</>
) : (
<div className="flex w-full flex-col items-center justify-center rounded-2xl border border-gray-200 bg-white py-16 transition-colors duration-300 dark:border-[#2a2a2a] dark:bg-[#1a1a1a] sm:py-20">
<p className="mb-4 px-4 text-center text-base font-semibold text-gray-500 dark:text-gray-400 sm:text-lg">
No destinations match your filters.
</p>
<button
onClick={() => {
setActiveRegion("all");
setActiveDuration(DEFAULT_DURATION);
}}
style={{ WebkitTapHighlightColor: "transparent" }}
className="rounded-full bg-black px-6 py-2.5 text-sm font-semibold text-white transition-colors duration-300 hover:bg-gray-800 focus:outline-none focus:ring-0 dark:bg-white dark:text-black dark:hover:bg-gray-200 sm:text-base"
>
Clear Filters
</button>
</div>
)}
</div>
</section>
);
}
const IMAGE_BASE = "/assets/images/tour-packages/tour-packages/";
const slugify = (value = "") =>
String(value)
.trim()
.toLowerCase()
.replace(/&/g, "and")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
const imgUrl = (name) =>
name
? `${IMAGE_BASE}Travel-Rethinkways-${name.replace(/\s+/g, "-")}.avif`
: "";
const INDIA_IMG = {
"Andaman": `${IMAGE_BASE}Travel-Rethinkways-Andaman.avif`,
"Jammu and Kashmir": `${IMAGE_BASE}Travel-Rethinkways-Kashmir.avif`,
"Kashmir": `${IMAGE_BASE}Travel-Rethinkways-Kashmir.avif`,
"Goa": `${IMAGE_BASE}Travel-Rethinkways-Goa.avif`,
"Himachal Pradesh": `${IMAGE_BASE}Travel-Rethinkways-Himachal-Pradesh.avif`,
"Kerala": `${IMAGE_BASE}Travel-Rethinkways-Kerala.avif`,
"Rajasthan": `${IMAGE_BASE}Travel-Rethinkways-Rajasthan.avif`,
"Uttarakhand": `${IMAGE_BASE}Travel-Rethinkways-Uttarakhand.avif`,
"Ladakh": `${IMAGE_BASE}Travel-Rethinkways-Ladakh.avif`,
"Tamil Nadu": `${IMAGE_BASE}Travel-Rethinkways-Tamil-Nadu.avif`,
"Karnataka": `${IMAGE_BASE}Travel-Rethinkways-Karnataka.avif`,
"Maharashtra": `${IMAGE_BASE}Travel-Rethinkways-Maharashtra.avif`,
"Uttar Pradesh": `${IMAGE_BASE}Travel-Rethinkways-Uttar-Pradesh.avif`,
"Delhi": `${IMAGE_BASE}Travel-Rethinkways-Delhi.avif`,
"West Bengal": `${IMAGE_BASE}Travel-Rethinkways-West-Bengal.avif`,
"Sikkim": `${IMAGE_BASE}Travel-Rethinkways-Sikkim.avif`,
"Meghalaya": `${IMAGE_BASE}Travel-Rethinkways-Meghalaya.avif`,
"Gujarat": `${IMAGE_BASE}Travel-Rethinkways-Gujarat.avif`,
"Punjab": `${IMAGE_BASE}Travel-Rethinkways-Punjab.avif`,
"Telangana": `${IMAGE_BASE}Travel-Rethinkways-Telangana.avif`,
"Arunachal Pradesh": `${IMAGE_BASE}Travel-Rethinkways-Arunachal-Pradesh.avif`,
"Assam": `${IMAGE_BASE}Travel-Rethinkways-Assam.avif`,
"Nagaland": `${IMAGE_BASE}Travel-Rethinkways-Nagaland.avif`,
"Manipur": `${IMAGE_BASE}Travel-Rethinkways-Manipur.avif`,
"Mizoram": `${IMAGE_BASE}Travel-Rethinkways-Mizoram.avif`,
"Tripura": `${IMAGE_BASE}Travel-Rethinkways-Tripura.avif`,
"Chhattisgarh": `${IMAGE_BASE}Travel-Rethinkways-Chhattisgarh.avif`,
"Jharkhand": `${IMAGE_BASE}Travel-Rethinkways-Jharkhand.avif`,
"Odisha": `${IMAGE_BASE}Travel-Rethinkways-Odisha.avif`,
"Madhya Pradesh": `${IMAGE_BASE}Travel-Rethinkways-Madhya-Pradesh.avif`,
"Puducherry": `${IMAGE_BASE}Travel-Rethinkways-Puducherry.avif`,
"Chandigarh": `${IMAGE_BASE}Travel-Rethinkways-Chandigarh.avif`,
"Lakshadweep": `${IMAGE_BASE}Travel-Rethinkways-Lakshadweep.avif`,
"Dadra and Nagar Haveli and Daman and Diu": `${IMAGE_BASE}Travel-Rethinkways-Dadra-and-Nagar-Haveli-and-Daman-and-Diu.avif`,
"Andhra Pradesh": `${IMAGE_BASE}Travel-Rethinkways-Andhra-Pradesh.avif`,
"Bihar": `${IMAGE_BASE}Travel-Rethinkways-Bihar.avif`,
"Haryana": `${IMAGE_BASE}Travel-Rethinkways-Haryana.avif`,
};
const INDIA_ROWS = [
["Goa", "Sun, Sand & Vibes", 1, 9999],
["Himachal Pradesh", "Land of Snow & Hills", 1, 12999],
["Kashmir", "Paradise on Earth", 1, 14999],
["Kerala", "God's Green Canvas", 1, 11999],
["Rajasthan", "Desert & Royal Trails", 1, 20999],
["Uttarakhand", "Land of Gods & Hills", 1, 12499],
["Andaman", "Island Life Starts Here", 1, 14999],
["Ladakh", "Land of High Passes", 1, 18999],
["Tamil Nadu", "Culture Crafted Forever", 1, 9499],
["Karnataka", "Culture, Coffee & Coasts", 1, 10999],
["Maharashtra", "From Mumbai to Mountains", 1, 10499],
["Uttar Pradesh", "Land of Timeless Heritage", 1, 12999],
["Delhi", "Heart of Incredible India", 1, 14999],
["West Bengal", "Soul of Eastern India", 1, 11999],
["Sikkim", "Hidden Himalayan Gem", 1, 13999],
["Meghalaya", "Abode of Clouds", 1, 14499],
["Gujarat", "Land of Legends & Lions", 1, 11499],
["Punjab", "Spirit of True India", 1, 15999],
["Telangana", "Land of Nawabs & Culture", 1, 9499],
["Arunachal Pradesh", "Land of Rising Sun", 1, 16999],
["Assam", "Tea Gardens & Wildlife", 1, 12999],
["Nagaland", "Land of Tribal Heritage", 1, 14999],
["Manipur", "Land of Floating Lakes", 1, 14499],
["Mizoram", "Land of Blue Mountains", 1, 15499],
["Tripura", "Palaces & Scenic Hills", 1, 13999],
["Chhattisgarh", "India's Hidden Wilderness", 1, 10999],
["Jharkhand", "Land of Forests & Falls", 1, 10499],
["Odisha", "Heritage by the Coast", 1, 11999],
["Madhya Pradesh", "Wildlife & Heritage Hub", 1, 11499],
["Puducherry", "Little France of India", 1, 8999],
["Chandigarh", "Modern India's Pride", 1, 17999],
["Lakshadweep", "India's Hidden Islands", 1, 29999],
["Dadra and Nagar Haveli and Daman and Diu", "Peace by the Arabian Sea", 1, 10999],
["Andhra Pradesh", "Where Faith Meets Nature", 1, 9499],
["Bihar", "Land of Ancient Wisdom", 1, 14999],
["Haryana", "Heritage Near the Capital", 1, 17999],
];
const INTL_ROWS = [
["Singapore", "Where Fun Never Ends", 6, 69999],
["Malaysia", "Where Asia Comes Alive", 10, 39999],
["Bali", "Island of Gods Awaits", 9, 39999],
["Maldives", "Luxury Island Escape", 5, 49999],
["Thailand", "Fun, Beaches & Nightlife", 11, 24999],
["Dubai (UAE)", "Desert to Skyscrapers", 21, 45999],
["Vietnam", "Scenic Asia Unfolds", 14, 44999],
["Sri Lanka", "Beaches, Hills & Heritage", 11, 34999],
["Philippines", "7000 Islands of Beauty", 22, 49999],
["Oman", "Desert Beauty of Arabia", 23, 49999],
["Qatar", "Arabian Elegance Unfolds", 24, 49999],
["Mauritius", "Island Luxury Redefined", 23, 69999],
["Seychelles", "Untouched Island Paradise", 25, 89999],
["Cambodia", "Culture Beyond Time", 19, 44999],
["Laos", "Hidden Gem of Asia", 18, 49999],
["Kenya", "Into the African Wild", 21, 89999],
["Tanzania", "Wild Africa Unleashed", 21, 99999],
["Japan", "Culture & Cherry Blossom", 24, 99999],
["South Korea", "K Culture Comes Alive", 21, 89999],
["Turkey", "Timeless Turkish Trails", 23, 79999],
["Nepal", "Gateway to the Himalayas", 24, 39999],
["Bhutan", "Land of Happiness Awaits", 24, 49999],
["Egypt", "Land of Ancient Wonders", 23, 89999],
["China", "Explore Chinese Heritage", 24, 109999],
["Hong Kong", "City of Lights & Skyline", 24, 89999],
["Jordan", "Wonders of the Middle East", 26, 79999],
["Uzbekistan", "Silk Road Wonders", 17, 69999],
["Kazakhstan", "Hidden Eurasian Gem", 19, 59999],
["Fiji", "Luxury in Blue Waters", 21, 99999],
["Australia", "Beaches, Cities & Nature", 24, 149999],
["New Zealand", "Land of Epic Landscapes", 23, 169999],
["USA", "From Cities to Wonders", 22, 179999],
["Canada", "Nature & Cities Combined", 24, 159999],
["South Africa", "Adventure in Every Mile", 23, 129999],
];
const EUROPE_ROWS = [
["Europe Multi-Country", "Expertly Designed Routes", 165000],
["France", "Romance & Culture", 145000],
["Switzerland", "Alpine Beauty Escape", 155000],
["Italy", "Art, Food & Romance", 140000],
["United Kingdom", "Royal & Urban Escape", 160000],
["Greece", "Islands & Blue Bliss", 150000],
["Netherlands", "Canals & City Vibes", 135000],
["Spain", "Culture & Coastal Life", 145000],
["Austria", "Music & Mountain Charm", 140000],
["Germany", "History & Modern Mix", 135000],
["Turkey", "East Meets West Magic", 120000],
["Norway", "Fjords & Scenic Beauty", 200000],
["Iceland", "Fire, Ice & Wonders", 210000],
["Finland", "Snow & Northern Lights", 210000],
["Ireland", "Green Lands & Castles", 155000],
["Portugal", "Coastal Charm & Heritage", 130000],
["Czech Republic", "Fairytale City Charm", 120000],
["Hungary", "Thermal Baths & Views", 115000],
["Croatia", "Adriatic Coastal Gems", 130000],
["Sweden", "Nordic Design & Nature", 190000],
["Denmark", "Hygge & Harbor Charm", 185000],
["Poland", "Heritage & Old Towns", 110000],
["Slovakia", "Castles & Nature Trails", 110000],
["Slovenia", "Lakes & Alpine Beauty", 120000],
["Romania", "Medieval Charm & Legends", 115000],
["Bulgaria", "History & Coastal Beauty", 110000],
["Estonia", "Medieval Meets Digital", 120000],
["Latvia", "Baltic Charm & Old Towns", 115000],
["Lithuania", "Heritage & Baltic Beauty", 115000],
["Malta", "Island History Escape", 130000],
["Montenegro", "Coastal Hidden Gem", 120000],
["Belgium", "Medieval Meets Modern", 135000],
["Luxembourg", "Castles & Scenic Views", 140000],
["Liechtenstein", "Alpine Luxury Escape", 145000],
["San Marino", "Hilltop Views & Heritage", 125000],
["Vatican City", "Faith & Timeless Art", 135000],
["Georgia", "Culture, Peaks & Wine", 110000],
["Armenia", "Timeless Culture & Peaks", 105000],
["Azerbaijan", "Modern Meets Old Charm", 110000],
["Serbia", "Nightlife & Culture Mix", 110000],
["Albania", "Raw Riviera Beauty", 110000],
["North Macedonia", "Lakes & Quiet Beauty", 105000],
["Belarus", "Soviet Charm & Nature", 110000],
["Russia", "Grand Cities & Heritage", 160000],
];
const buildIndia = () => INDIA_ROWS.map(([destination, tagline, packageCount, startsFrom]) => ({
id: `seed-india-${slugify(destination)}`,
sectionCode: "india",
destination,
destinationSlug: slugify(destination),
tagline,
packageCount,
startsFrom,
priceUnit: "Per Adult",
imageUrl: INDIA_IMG[destination] || imgUrl(destination),
showShare: true,
}));
const buildInternational = () => INTL_ROWS.map(([destination, tagline, packageCount, startsFrom]) => ({
id: `seed-international-${slugify(destination)}`,
sectionCode: "international",
destination,
destinationSlug: slugify(destination),
tagline,
packageCount,
startsFrom,
priceUnit: "Per Adult",
imageUrl: imgUrl(destination),
showShare: true,
}));
const buildEurope = () => EUROPE_ROWS.map(([destination, tagline, startsFrom]) => ({
id: `seed-europe-${slugify(destination)}`,
sectionCode: "europe",
destination,
destinationSlug: slugify(destination),
tagline,
packageCount: 1,
startsFrom,
priceUnit: "Per Adult",
imageUrl: imgUrl(destination),
showShare: true,
}));
export const ALL_SEED_PACKAGES = [
...buildIndia(),
...buildInternational(),
...buildEurope(),
];
import BestSellingPackages from "../components/BestSellingPackages.jsx";
export default function App() {
return (
<main className="min-h-screen bg-white py-6 text-black dark:bg-black dark:text-white">
<BestSellingPackages />
</main>
);
}
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";
const host =
document.querySelector('[id^="tour-widget-root-"]') ||
document.getElementById("root");
if (!host) {
throw new Error("Tour widget mount point not found");
}
ReactDOM.createRoot(host).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
@tailwind base;
@tailwind components;
@tailwind utilities;
html,
body,
#root {
min-height: 100%;
}
body {
margin: 0;
}
{
"name": "widgets",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"predev": "node generate-source-html.mjs",
"dev": "vite",
"prebuild": "node generate-source-html.mjs",
"build": "node build.mjs",
"preview": "vite preview --outDir build"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"autoprefixer": "^10.4.20",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.13",
"vite": "^5.4.8",
"@vitejs/plugin-react": "^4.3.1"
}
}
/** @type {import('tailwindcss').Config} */
export default {
darkMode: "class",
content: [
"./index.html",
"./src/**/*.{js,jsx,ts,tsx}",
"./components/**/*.{js,jsx,ts,tsx}",
"./data/**/*.{js,jsx,ts,tsx}",
"./utils/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {
fontFamily: {
sans: ["Poppins", "system-ui", "sans-serif"],
},
},
},
plugins: [],
};
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
});
# Widget static asset rules for Best Selling widget
Options -MultiViews
DirectorySlash Off
DirectoryIndex test.html index.html
<IfModule mod_headers.c>
<FilesMatch "\.(js|css|png|jpg|jpeg|gif|svg|webp|avif|ico|woff2?|ttf|eot|map)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
<FilesMatch "\.(html|json)$">
Header set Cache-Control "public, max-age=300, must-revalidate"
</FilesMatch>
</IfModule>
<IfModule mod_mime.c>
AddType text/css .css
AddType application/javascript .js
AddType application/json .map
</IfModule>
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /widgets/best-selling/
RewriteRule ^$ test.html [L]
RewriteRule ^(?:assets/.*|best-selling-widget\.js|best-selling-widget\.css|best-selling-widget\.js\.map|favicon\.svg|test\.html)$ - [L]
</IfModule>