@questpackages/tours 0.1.0-test.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +122 -0
  3. package/dist/cli/index.d.ts +2 -0
  4. package/dist/cli/index.js +219 -0
  5. package/dist/cli/templates.d.ts +12 -0
  6. package/dist/cli/templates.js +100 -0
  7. package/dist/next/index.d.ts +4 -0
  8. package/dist/next/index.js +4 -0
  9. package/dist/next/metadata.d.ts +26 -0
  10. package/dist/next/metadata.js +58 -0
  11. package/dist/next/revalidate-handler.d.ts +18 -0
  12. package/dist/next/revalidate-handler.js +42 -0
  13. package/dist/next/signature.d.ts +12 -0
  14. package/dist/next/signature.js +26 -0
  15. package/dist/react/booking-steps.d.ts +22 -0
  16. package/dist/react/booking-steps.js +23 -0
  17. package/dist/react/booking-wizard.d.ts +12 -0
  18. package/dist/react/booking-wizard.js +68 -0
  19. package/dist/react/contact-dialog.d.ts +15 -0
  20. package/dist/react/contact-dialog.js +45 -0
  21. package/dist/react/contact-panel.d.ts +5 -0
  22. package/dist/react/contact-panel.js +15 -0
  23. package/dist/react/contact-shared.d.ts +55 -0
  24. package/dist/react/contact-shared.js +60 -0
  25. package/dist/react/countries.d.ts +4 -0
  26. package/dist/react/countries.js +6 -0
  27. package/dist/react/index.d.ts +12 -0
  28. package/dist/react/index.js +11 -0
  29. package/dist/react/inquiry-wizard.d.ts +5 -0
  30. package/dist/react/inquiry-wizard.js +43 -0
  31. package/dist/react/party-size-rules.d.ts +18 -0
  32. package/dist/react/party-size-rules.js +52 -0
  33. package/dist/react/party-size.d.ts +27 -0
  34. package/dist/react/party-size.js +81 -0
  35. package/dist/react/sections.d.ts +10 -0
  36. package/dist/react/sections.js +58 -0
  37. package/dist/react/shared.d.ts +50 -0
  38. package/dist/react/shared.js +39 -0
  39. package/dist/react/tour-card.d.ts +30 -0
  40. package/dist/react/tour-card.js +22 -0
  41. package/dist/react/tour-detail.d.ts +26 -0
  42. package/dist/react/tour-detail.js +55 -0
  43. package/dist/react/tour-gallery.d.ts +4 -0
  44. package/dist/react/tour-gallery.js +35 -0
  45. package/dist/react/tour-json-ld.d.ts +7 -0
  46. package/dist/react/tour-json-ld.js +13 -0
  47. package/dist/schema/index.d.ts +805 -0
  48. package/dist/schema/index.js +249 -0
  49. package/dist/server/client.d.ts +43 -0
  50. package/dist/server/client.js +85 -0
  51. package/dist/server/index.d.ts +2 -0
  52. package/dist/server/index.js +2 -0
  53. package/dist/styles.css +1395 -0
  54. package/package.json +67 -0
@@ -0,0 +1,81 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { createContext, useContext, useMemo, useState } from "react";
4
+ import { adultCountOptions, childrenCountOptions, findPartyPriceOption, } from "./party-size-rules.js";
5
+ import { Icon } from "./shared.js";
6
+ const TourPartySizeContext = createContext(null);
7
+ function initialAdults(tour) {
8
+ const bounds = tour.enquiry;
9
+ const options = adultCountOptions(bounds, tour.enquiry.childrenAllowed);
10
+ const baseline = tour.pricing.partySize?.baselineAdults ?? bounds.minTravellers;
11
+ return options.includes(baseline) ? baseline : (options[0] ?? bounds.minTravellers);
12
+ }
13
+ /** Shares the chosen group size between the price panel and the booking and inquiry forms. */
14
+ export function TourPartySizeProvider({ tour, children: content }) {
15
+ const [adults, setAdultsState] = useState(() => initialAdults(tour));
16
+ const [children, setChildren] = useState(0);
17
+ const value = useMemo(() => {
18
+ const setAdults = (next) => {
19
+ setAdultsState(next);
20
+ const allowed = childrenCountOptions(next, tour.enquiry);
21
+ setChildren((current) => (allowed.includes(current) ? current : (allowed[0] ?? 0)));
22
+ };
23
+ return {
24
+ adults,
25
+ children,
26
+ setAdults,
27
+ setChildren,
28
+ selected: findPartyPriceOption(tour, adults, children),
29
+ };
30
+ }, [adults, children, tour]);
31
+ return _jsx(TourPartySizeContext.Provider, { value: value, children: content });
32
+ }
33
+ export function useOptionalTourPartySize() {
34
+ return useContext(TourPartySizeContext);
35
+ }
36
+ export function useTourPartySize() {
37
+ const context = useOptionalTourPartySize();
38
+ if (!context) {
39
+ throw new Error("useTourPartySize must be used inside <TourDetail> or <TourPartySizeProvider>.");
40
+ }
41
+ return context;
42
+ }
43
+ /** Adults and (when allowed) children selects bound to the shared group size. */
44
+ export function TourPartySizeFields({ tour, idPrefix }) {
45
+ const { adults, children, setAdults, setChildren } = useTourPartySize();
46
+ const adultOptions = adultCountOptions(tour.enquiry, tour.enquiry.childrenAllowed);
47
+ return (_jsxs("div", { className: "qt-field-row", children: [_jsxs("label", { className: "qt-field", htmlFor: `${idPrefix}-adults`, children: [_jsxs("span", { className: "qt-field-label", children: [_jsx(Icon.Users, {}), " Adults"] }), _jsx("select", { id: `${idPrefix}-adults`, className: "qt-input", value: adults, onChange: (event) => setAdults(Number(event.target.value)), children: adultOptions.map((count) => (_jsx("option", { value: count, children: count }, count))) })] }), tour.enquiry.childrenAllowed ? (_jsxs("label", { className: "qt-field", htmlFor: `${idPrefix}-children`, children: [_jsx("span", { className: "qt-field-label", children: "Children" }), _jsx("select", { id: `${idPrefix}-children`, className: "qt-input", value: children, onChange: (event) => setChildren(Number(event.target.value)), children: childrenCountOptions(adults, tour.enquiry).map((count) => (_jsx("option", { value: count, children: count }, count))) })] })) : null] }));
48
+ }
49
+ const availabilityDate = new Intl.DateTimeFormat("en", { dateStyle: "medium", timeZone: "UTC" });
50
+ function formatAvailabilityDate(value) {
51
+ if (!value)
52
+ return null;
53
+ const date = new Date(`${value.slice(0, 10)}T00:00:00Z`);
54
+ return Number.isNaN(date.getTime()) ? value : availabilityDate.format(date);
55
+ }
56
+ function availabilityNote(tour) {
57
+ const startDate = formatAvailabilityDate(tour.availability.startDate);
58
+ const endDate = formatAvailabilityDate(tour.availability.endDate);
59
+ if (tour.availability.unlimited)
60
+ return "Available all year";
61
+ if (startDate && endDate)
62
+ return `Available ${startDate} to ${endDate}`;
63
+ if (startDate)
64
+ return `Available from ${startDate}`;
65
+ if (endDate)
66
+ return `Available until ${endDate}`;
67
+ return null;
68
+ }
69
+ export function TourPricing({ tour }) {
70
+ const { selected } = useTourPartySize();
71
+ const hasPartyPricing = tour.pricing.partySize !== null;
72
+ const headline = selected
73
+ ? { amount: selected.perPerson, label: "per person" }
74
+ : tour.pricing.fromPerPerson
75
+ ? { amount: tour.pricing.fromPerPerson.formatted, label: "per person, from" }
76
+ : tour.pricing.total
77
+ ? { amount: tour.pricing.total.formatted, label: "total" }
78
+ : null;
79
+ const availability = availabilityNote(tour);
80
+ return (_jsxs("div", { className: "qt-panel qt-pricing", id: "quest-pricing", children: [_jsx("p", { className: "qt-panel-eyebrow", children: "Price" }), _jsx("p", { className: "qt-pricing-headline", children: headline ? (_jsxs(_Fragment, { children: [_jsx("strong", { children: headline.amount }), " ", _jsx("span", { children: headline.label })] })) : (_jsx("span", { children: "Price on request" })) }), hasPartyPricing ? _jsx(TourPartySizeFields, { tour: tour, idPrefix: "quest-pricing" }) : null, selected ? (_jsxs("dl", { className: "qt-pricing-rows", children: [_jsxs("div", { children: [_jsx("dt", { children: "Total for your group" }), _jsx("dd", { children: selected.total })] }), selected.vehicleCapacityLabel ? (_jsxs("div", { children: [_jsx("dt", { children: "Vehicle" }), _jsx("dd", { children: selected.vehicleCapacityLabel })] })) : null] })) : null, tour.pricing.lineItems.length > 0 ? (_jsxs("details", { className: "qt-pricing-breakdown", children: [_jsxs("summary", { children: ["Price breakdown ", _jsx(Icon.Chevron, {})] }), _jsx("table", { children: _jsx("tbody", { children: tour.pricing.lineItems.map((item, index) => (_jsxs("tr", { children: [_jsxs("th", { scope: "row", children: [item.name, item.qty ? _jsxs("span", { className: "qt-muted", children: [" x ", item.qty] }) : null] }), _jsx("td", { children: item.total })] }, `${item.name}-${index}`))) }) })] })) : null, availability ? _jsx("p", { className: "qt-muted qt-small", children: availability }) : null, hasPartyPricing ? (_jsx("p", { className: "qt-muted qt-small", children: "Indicative price for the selected group size. Final quote confirmed by the operator." })) : null] }));
81
+ }
@@ -0,0 +1,10 @@
1
+ import { type TourSectionProps } from "./shared.js";
2
+ export declare function TourHero({ tour }: TourSectionProps): import("react").JSX.Element;
3
+ export declare function TourOverview({ tour }: TourSectionProps): import("react").JSX.Element | null;
4
+ export declare function TourHighlights({ tour }: TourSectionProps): import("react").JSX.Element | null;
5
+ export declare function TourItinerary({ tour }: TourSectionProps): import("react").JSX.Element | null;
6
+ export declare function TourAccommodation({ tour }: TourSectionProps): import("react").JSX.Element | null;
7
+ export declare function TourVehicle({ tour }: TourSectionProps): import("react").JSX.Element | null;
8
+ export declare function TourInclusions({ tour }: TourSectionProps): import("react").JSX.Element | null;
9
+ export declare function TourPaymentTerms({ tour }: TourSectionProps): import("react").JSX.Element | null;
10
+ export declare function TourRelated({ tour, hrefFor, LinkComponent }: TourSectionProps): import("react").JSX.Element | null;
@@ -0,0 +1,58 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { TourCard } from "./tour-card.js";
3
+ import { Icon, routeLabel, TourImg, TourSection, } from "./shared.js";
4
+ export function TourHero({ tour }) {
5
+ const route = routeLabel(tour);
6
+ const price = tour.price.fromPerPerson ?? tour.price.total;
7
+ return (_jsxs("header", { className: "qt-hero", children: [_jsxs("div", { className: "qt-hero-media", children: [tour.heroImage ? (_jsx(TourImg, { image: tour.heroImage, sizes: "100vw", priority: true, className: "qt-hero-image" })) : null, _jsx("div", { className: "qt-hero-overlay" })] }), _jsxs("div", { className: "qt-hero-content", children: [_jsx("p", { className: "qt-hero-eyebrow", children: tour.operator.name }), _jsx("h1", { className: "qt-hero-title", children: tour.title }), tour.summary ? _jsx("p", { className: "qt-hero-lede", children: tour.summary }) : null, _jsxs("ul", { className: "qt-hero-meta", children: [_jsxs("li", { children: [_jsx(Icon.Clock, {}), tour.durationLabel] }), route ? (_jsxs("li", { children: [_jsx(Icon.Route, {}), route] })) : null, tour.tags.slice(0, 3).map((tag) => (_jsx("li", { children: tag.label }, tag.slug)))] }), _jsxs("div", { className: "qt-hero-actions", children: [price ? (_jsxs("p", { className: "qt-hero-price", children: [tour.price.fromPerPerson ? "From " : "", _jsx("strong", { children: price.formatted }), tour.price.fromPerPerson ? " per person" : " total"] })) : null, _jsx("a", { className: "qt-button", href: "#quest-enquiry", children: "Enquire now" })] })] })] }));
8
+ }
9
+ export function TourOverview({ tour }) {
10
+ if (tour.overview.length === 0)
11
+ return null;
12
+ return (_jsx(TourSection, { id: "quest-overview", title: "Trip overview", children: _jsx("div", { className: "qt-prose", children: tour.overview.map((paragraph, index) => (_jsx("p", { children: paragraph }, index))) }) }));
13
+ }
14
+ export function TourHighlights({ tour }) {
15
+ if (tour.highlights.length === 0)
16
+ return null;
17
+ return (_jsx(TourSection, { id: "quest-highlights", title: "Highlights", children: _jsx("ul", { className: "qt-highlights", children: tour.highlights.map((highlight) => (_jsxs("li", { className: "qt-highlight", children: [_jsx("span", { className: "qt-highlight-icon", children: _jsx(Icon.Check, {}) }), highlight] }, highlight))) }) }));
18
+ }
19
+ function ItineraryDay({ day, index }) {
20
+ const groupedActivities = day.activityGroups.filter((group) => group.activities.length > 0);
21
+ const hasActivities = groupedActivities.length > 0 || day.activities.length > 0;
22
+ const images = [day.image, ...day.gallery].filter((image) => image !== null).slice(0, 2);
23
+ return (_jsx("li", { className: "qt-day", children: _jsxs("details", { open: true, children: [_jsxs("summary", { className: "qt-day-summary", children: [_jsx("span", { className: "qt-day-badge", children: index + 1 }), _jsxs("span", { className: "qt-day-heading", children: [_jsxs("span", { className: "qt-day-label", children: [day.dayLabel, day.dateLabel ? ` · ${day.dateLabel}` : ""] }), _jsx("h3", { className: "qt-day-title", children: day.title })] }), _jsx(Icon.Chevron, {})] }), _jsxs("div", { className: "qt-day-body", children: [images.length > 0 ? (_jsx("div", { className: `qt-day-images${images.length === 2 ? " is-two" : ""}`, children: images.map((image) => (_jsx(TourImg, { image: image, sizes: "(min-width: 768px) 40vw, 100vw" }, image.url))) })) : null, day.description ? _jsx("p", { className: "qt-day-description", children: day.description }) : null, _jsxs("dl", { className: "qt-day-facts", children: [day.destination ? (_jsxs("div", { children: [_jsxs("dt", { children: [_jsx(Icon.Pin, {}), " Destination"] }), _jsx("dd", { children: day.destination })] })) : null, day.accommodation ? (_jsxs("div", { children: [_jsxs("dt", { children: [_jsx(Icon.Bed, {}), " ", day.accommodation.label || "Accommodation"] }), _jsx("dd", { children: day.accommodation.name })] })) : null, day.mealPlan ? (_jsxs("div", { children: [_jsxs("dt", { children: [_jsx(Icon.Meal, {}), " Meals"] }), _jsx("dd", { children: day.mealPlan })] })) : null] }), hasActivities ? (_jsxs("div", { className: "qt-day-activities", children: [groupedActivities.map((group) => (_jsxs("div", { className: "qt-activity-group", children: [group.title ? _jsx("p", { className: "qt-activity-group-title", children: group.title }) : null, _jsx(ActivityList, { activities: group.activities })] }, group.title))), day.activities.length > 0 ? _jsx(ActivityList, { activities: day.activities }) : null] })) : null] })] }) }));
24
+ }
25
+ function ActivityList({ activities }) {
26
+ return (_jsx("ul", { className: "qt-activities", children: activities.map((activity, index) => (_jsxs("li", { children: [_jsx(Icon.Check, {}), _jsxs("span", { children: [activity.time ? _jsxs("span", { className: "qt-activity-time", children: [activity.time, " "] }) : null, activity.name, activity.location ? _jsxs("span", { className: "qt-muted", children: [" \u00B7 ", activity.location] }) : null, activity.notes ? _jsx("span", { className: "qt-activity-notes", children: activity.notes }) : null] })] }, `${activity.name}-${index}`))) }));
27
+ }
28
+ export function TourItinerary({ tour }) {
29
+ if (tour.days.length === 0)
30
+ return null;
31
+ return (_jsx(TourSection, { id: "quest-itinerary", title: "Day-by-day itinerary", description: `${tour.days.length} ${tour.days.length === 1 ? "day" : "days"} planned for you`, children: _jsx("ol", { className: "qt-itinerary", children: tour.days.map((day, index) => (_jsx(ItineraryDay, { day: day, index: index }, `${day.dayLabel}-${index}`))) }) }));
32
+ }
33
+ export function TourAccommodation({ tour }) {
34
+ if (tour.lodging.length === 0)
35
+ return null;
36
+ return (_jsx(TourSection, { id: "quest-accommodation", title: "Where you will stay", children: _jsx("ul", { className: "qt-lodging", children: tour.lodging.map((lodge) => (_jsxs("li", { className: "qt-lodge", children: [lodge.image ? (_jsx(TourImg, { image: lodge.image, sizes: "(min-width: 768px) 33vw, 100vw", className: "qt-lodge-image" })) : null, _jsxs("div", { className: "qt-lodge-body", children: [lodge.label ? _jsx("p", { className: "qt-lodge-label", children: lodge.label }) : null, _jsx("h3", { className: "qt-lodge-name", children: lodge.name }), lodge.detail ? _jsx("p", { className: "qt-muted qt-small", children: lodge.detail }) : null, lodge.nights.length > 0 ? (_jsxs("p", { className: "qt-small", children: [_jsx(Icon.Bed, {}), " ", lodge.nights.length, " ", lodge.nights.length === 1 ? "night" : "nights"] })) : null, lodge.mealPlan ? (_jsxs("p", { className: "qt-small", children: [_jsx(Icon.Meal, {}), " ", lodge.mealPlan] })) : null] })] }, lodge.name))) }) }));
37
+ }
38
+ export function TourVehicle({ tour }) {
39
+ const vehicle = tour.vehicle;
40
+ if (!vehicle)
41
+ return null;
42
+ return (_jsx(TourSection, { id: "quest-vehicle", title: "Your safari vehicle", children: _jsxs("div", { className: "qt-vehicle", children: [vehicle.image ? (_jsx(TourImg, { image: vehicle.image, sizes: "(min-width: 768px) 40vw, 100vw", className: "qt-vehicle-image" })) : null, _jsxs("div", { children: [_jsxs("h3", { className: "qt-vehicle-name", children: [_jsx(Icon.Car, {}), " ", vehicle.name] }), vehicle.description ? _jsx("p", { className: "qt-muted", children: vehicle.description }) : null, vehicle.maxPassengers ? (_jsxs("p", { className: "qt-small", children: [_jsx(Icon.Users, {}), " Up to ", vehicle.maxPassengers, " passengers"] })) : null, vehicle.features.length > 0 ? (_jsx("ul", { className: "qt-checklist", children: vehicle.features.map((feature) => (_jsxs("li", { children: [_jsx(Icon.Check, {}), feature] }, feature))) })) : null] })] }) }));
43
+ }
44
+ export function TourInclusions({ tour }) {
45
+ if (tour.inclusions.length === 0 && tour.exclusions.length === 0)
46
+ return null;
47
+ return (_jsx(TourSection, { id: "quest-inclusions", title: "What is included", children: _jsxs("div", { className: "qt-inclusions", children: [tour.inclusions.length > 0 ? (_jsxs("div", { className: "qt-inclusions-column", children: [_jsx("h3", { className: "qt-inclusions-title", children: "Included" }), _jsx("ul", { className: "qt-checklist", children: tour.inclusions.map((item) => (_jsxs("li", { children: [_jsx(Icon.Check, {}), item] }, item))) })] })) : null, tour.exclusions.length > 0 ? (_jsxs("div", { className: "qt-inclusions-column", children: [_jsx("h3", { className: "qt-inclusions-title", children: "Not included" }), _jsx("ul", { className: "qt-checklist is-excluded", children: tour.exclusions.map((item) => (_jsxs("li", { children: [_jsx(Icon.X, {}), item] }, item))) })] })) : null] }) }));
48
+ }
49
+ export function TourPaymentTerms({ tour }) {
50
+ if (tour.paymentTerms.length === 0 && !tour.paymentNote)
51
+ return null;
52
+ return (_jsxs(TourSection, { id: "quest-payment-terms", title: "Payment terms", children: [tour.paymentTerms.length > 0 ? (_jsx("ul", { className: "qt-terms", children: tour.paymentTerms.map((term) => (_jsx("li", { children: term }, term))) })) : null, tour.paymentNote ? _jsx("p", { className: "qt-muted", children: tour.paymentNote }) : null] }));
53
+ }
54
+ export function TourRelated({ tour, hrefFor, LinkComponent }) {
55
+ if (tour.related.length === 0)
56
+ return null;
57
+ return (_jsx(TourSection, { id: "quest-related", title: "You may also like", children: _jsx("ul", { className: "qt-grid", children: tour.related.slice(0, 3).map((related) => (_jsx("li", { className: "qt-grid-item", children: _jsx(TourCard, { tour: related, href: hrefFor(related.slug), LinkComponent: LinkComponent }) }, related.slug))) }) }));
58
+ }
@@ -0,0 +1,50 @@
1
+ import type { ComponentType, ReactNode } from "react";
2
+ import type { Tour, TourImage } from "../schema/index.js";
3
+ export type TourLinkProps = {
4
+ href: string;
5
+ className?: string;
6
+ children?: ReactNode;
7
+ "aria-label"?: string;
8
+ };
9
+ /** Pass `next/link` (or your router's link) to get client-side navigation. Defaults to `<a>`. */
10
+ export type TourLinkComponent = ComponentType<TourLinkProps> | "a";
11
+ /** Props every detail section receives. Use this type for your own section overrides. */
12
+ export type TourSectionProps = {
13
+ tour: Tour;
14
+ hrefFor: (slug: string) => string;
15
+ LinkComponent: TourLinkComponent;
16
+ };
17
+ export declare function TourLink({ LinkComponent, ...props }: TourLinkProps & {
18
+ LinkComponent: TourLinkComponent;
19
+ }): import("react").JSX.Element;
20
+ export declare function defaultHrefFor(hrefBase: string): (slug: string) => string;
21
+ export declare function TourImg({ image, sizes, className, priority, }: {
22
+ image: TourImage;
23
+ sizes: string;
24
+ className?: string;
25
+ priority?: boolean;
26
+ }): import("react").JSX.Element;
27
+ export declare function TourSection({ id, title, description, children, }: {
28
+ id: string;
29
+ title: string;
30
+ description?: string | null;
31
+ children: ReactNode;
32
+ }): import("react").JSX.Element;
33
+ export declare const Icon: {
34
+ Clock: () => import("react").JSX.Element;
35
+ Pin: () => import("react").JSX.Element;
36
+ Bed: () => import("react").JSX.Element;
37
+ Meal: () => import("react").JSX.Element;
38
+ Check: () => import("react").JSX.Element;
39
+ X: () => import("react").JSX.Element;
40
+ Users: () => import("react").JSX.Element;
41
+ Car: () => import("react").JSX.Element;
42
+ Chevron: () => import("react").JSX.Element;
43
+ ArrowLeft: () => import("react").JSX.Element;
44
+ ArrowRight: () => import("react").JSX.Element;
45
+ Route: () => import("react").JSX.Element;
46
+ };
47
+ export declare function routeLabel(tour: {
48
+ startDestination: string | null;
49
+ endDestination: string | null;
50
+ }): string | null;
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export function TourLink({ LinkComponent, ...props }) {
3
+ if (LinkComponent === "a")
4
+ return _jsx("a", { ...props });
5
+ return _jsx(LinkComponent, { ...props });
6
+ }
7
+ export function defaultHrefFor(hrefBase) {
8
+ const base = hrefBase.replace(/\/+$/, "");
9
+ return (slug) => `${base}/${slug}`;
10
+ }
11
+ export function TourImg({ image, sizes, className, priority = false, }) {
12
+ return (_jsx("img", { src: image.url, srcSet: image.srcSet ?? undefined, sizes: image.srcSet ? sizes : undefined, alt: image.alt, width: image.width ?? undefined, height: image.height ?? undefined, loading: priority ? "eager" : "lazy", fetchPriority: priority ? "high" : undefined, decoding: "async", className: className }));
13
+ }
14
+ export function TourSection({ id, title, description, children, }) {
15
+ return (_jsxs("section", { id: id, className: "qt-section", "aria-labelledby": `${id}-title`, children: [_jsxs("div", { className: "qt-section-head", children: [_jsx("h2", { id: `${id}-title`, className: "qt-section-title", children: title }), description ? _jsx("p", { className: "qt-section-desc", children: description }) : null] }), children] }));
16
+ }
17
+ function Svg({ children, className }) {
18
+ return (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", className: `qt-icon ${className ?? ""}`.trim(), children: children }));
19
+ }
20
+ export const Icon = {
21
+ Clock: () => (_jsxs(Svg, { children: [_jsx("circle", { cx: "12", cy: "12", r: "10" }), _jsx("path", { d: "M12 6v6l4 2" })] })),
22
+ Pin: () => (_jsxs(Svg, { children: [_jsx("path", { d: "M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z" }), _jsx("circle", { cx: "12", cy: "10", r: "3" })] })),
23
+ Bed: () => (_jsx(Svg, { children: _jsx("path", { d: "M2 4v16M2 8h18a2 2 0 0 1 2 2v10M2 17h20M6 8v9" }) })),
24
+ Meal: () => (_jsx(Svg, { children: _jsx("path", { d: "M3 2v7c0 1.1.9 2 2 2h4a2 2 0 0 0 2-2V2M7 2v20M21 15V2a5 5 0 0 0-5 5v6c0 1.1.9 2 2 2h3Zm0 0v7" }) })),
25
+ Check: () => (_jsx(Svg, { children: _jsx("path", { d: "M20 6 9 17l-5-5" }) })),
26
+ X: () => (_jsx(Svg, { children: _jsx("path", { d: "M18 6 6 18M6 6l12 12" }) })),
27
+ Users: () => (_jsxs(Svg, { children: [_jsx("path", { d: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" }), _jsx("circle", { cx: "9", cy: "7", r: "4" }), _jsx("path", { d: "M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" })] })),
28
+ Car: () => (_jsxs(Svg, { children: [_jsx("path", { d: "M19 17h2c.6 0 1-.4 1-1v-3c0-.9-.7-1.7-1.5-1.9L18 10l-2.7-3.6A2 2 0 0 0 13.7 6H6.3a2 2 0 0 0-1.6.8L2 10.5V16c0 .6.4 1 1 1h2" }), _jsx("circle", { cx: "7", cy: "17", r: "2" }), _jsx("circle", { cx: "17", cy: "17", r: "2" }), _jsx("path", { d: "M9 17h6" })] })),
29
+ Chevron: () => (_jsx(Svg, { className: "qt-chevron", children: _jsx("path", { d: "m6 9 6 6 6-6" }) })),
30
+ ArrowLeft: () => (_jsx(Svg, { children: _jsx("path", { d: "m15 18-6-6 6-6" }) })),
31
+ ArrowRight: () => (_jsx(Svg, { children: _jsx("path", { d: "m9 18 6-6-6-6" }) })),
32
+ Route: () => (_jsxs(Svg, { children: [_jsx("circle", { cx: "6", cy: "19", r: "3" }), _jsx("path", { d: "M9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15" }), _jsx("circle", { cx: "18", cy: "5", r: "3" })] })),
33
+ };
34
+ export function routeLabel(tour) {
35
+ const { startDestination: start, endDestination: end } = tour;
36
+ if (start && end && start.toLowerCase() !== end.toLowerCase())
37
+ return `${start} to ${end}`;
38
+ return start ?? end ?? null;
39
+ }
@@ -0,0 +1,30 @@
1
+ import type { ComponentType } from "react";
2
+ import type { TourCard as TourCardData } from "../schema/index.js";
3
+ import { type TourLinkComponent } from "./shared.js";
4
+ export type TourCardProps = {
5
+ tour: TourCardData;
6
+ href: string;
7
+ LinkComponent?: TourLinkComponent;
8
+ /** Load the image eagerly (first row above the fold). */
9
+ priority?: boolean;
10
+ headingLevel?: "h2" | "h3";
11
+ };
12
+ export declare function TourCardPrice({ tour }: {
13
+ tour: TourCardData;
14
+ }): import("react").JSX.Element | null;
15
+ export declare function TourCard({ tour, href, LinkComponent, priority, headingLevel: Heading, }: TourCardProps): import("react").JSX.Element;
16
+ export type TourGridProps = {
17
+ tours: TourCardData[];
18
+ /** Base path of your tour detail route. Default `/tours`, giving `/tours/<slug>`. */
19
+ hrefBase?: string;
20
+ /** Full control over card links. Overrides `hrefBase`. */
21
+ hrefFor?: (slug: string) => string;
22
+ LinkComponent?: TourLinkComponent;
23
+ /** Replace the card design while keeping the grid. */
24
+ CardComponent?: ComponentType<TourCardProps>;
25
+ /** Shown when there are no tours. */
26
+ emptyMessage?: string;
27
+ headingLevel?: "h2" | "h3";
28
+ className?: string;
29
+ };
30
+ export declare function TourGrid({ tours, hrefBase, hrefFor, LinkComponent, CardComponent, emptyMessage, headingLevel, className, }: TourGridProps): import("react").JSX.Element;
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { defaultHrefFor, Icon, routeLabel, TourImg, TourLink, } from "./shared.js";
3
+ export function TourCardPrice({ tour }) {
4
+ if (tour.price.fromPerPerson) {
5
+ return (_jsxs("p", { className: "qt-card-price", children: [_jsx("span", { className: "qt-card-price-label", children: "From" }), " ", _jsx("strong", { children: tour.price.fromPerPerson.formatted }), " ", _jsx("span", { className: "qt-card-price-label", children: "per person" })] }));
6
+ }
7
+ if (tour.price.total) {
8
+ return (_jsxs("p", { className: "qt-card-price", children: [_jsx("strong", { children: tour.price.total.formatted }), " ", _jsx("span", { className: "qt-card-price-label", children: "total" })] }));
9
+ }
10
+ return null;
11
+ }
12
+ export function TourCard({ tour, href, LinkComponent = "a", priority = false, headingLevel: Heading = "h3", }) {
13
+ const route = routeLabel(tour);
14
+ return (_jsxs("article", { className: "qt-card", children: [_jsxs("div", { className: "qt-card-media", children: [tour.heroImage ? (_jsx(TourImg, { image: tour.heroImage, sizes: "(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw", priority: priority, className: "qt-card-image" })) : (_jsx("div", { className: "qt-card-image qt-card-image-empty", "aria-hidden": "true" })), _jsxs("span", { className: "qt-card-duration", children: [_jsx(Icon.Clock, {}), tour.durationLabel] })] }), _jsxs("div", { className: "qt-card-body", children: [tour.tags.length > 0 ? (_jsx("ul", { className: "qt-card-tags", "aria-label": "Tour types", children: tour.tags.slice(0, 2).map((tag) => (_jsx("li", { className: "qt-tag", children: tag.label }, tag.slug))) })) : null, _jsx(Heading, { className: "qt-card-title", children: _jsx(TourLink, { LinkComponent: LinkComponent, href: href, className: "qt-card-link", children: tour.title }) }), route ? (_jsxs("p", { className: "qt-card-route", children: [_jsx(Icon.Pin, {}), route] })) : null, tour.summary ? _jsx("p", { className: "qt-card-summary", children: tour.summary }) : null, tour.highlights.length > 0 ? (_jsx("ul", { className: "qt-card-highlights", children: tour.highlights.slice(0, 2).map((highlight) => (_jsxs("li", { children: [_jsx(Icon.Check, {}), highlight] }, highlight))) })) : null, _jsxs("div", { className: "qt-card-footer", children: [_jsx(TourCardPrice, { tour: tour }), _jsxs("span", { className: "qt-card-cta", "aria-hidden": "true", children: ["View tour", _jsx(Icon.ArrowRight, {})] })] })] })] }));
15
+ }
16
+ export function TourGrid({ tours, hrefBase = "/tours", hrefFor, LinkComponent = "a", CardComponent = TourCard, emptyMessage = "No tours available right now. Please check back soon.", headingLevel, className, }) {
17
+ const resolveHref = hrefFor ?? defaultHrefFor(hrefBase);
18
+ if (tours.length === 0) {
19
+ return (_jsx("div", { className: `quest-tours qt-empty ${className ?? ""}`.trim(), children: _jsx("p", { children: emptyMessage }) }));
20
+ }
21
+ return (_jsx("div", { className: `quest-tours ${className ?? ""}`.trim(), children: _jsx("ul", { className: "qt-grid", children: tours.map((tour, index) => (_jsx("li", { className: "qt-grid-item", children: _jsx(CardComponent, { tour: tour, href: resolveHref(tour.slug), LinkComponent: LinkComponent, priority: index < 3, headingLevel: headingLevel }) }, tour.slug))) }) }));
22
+ }
@@ -0,0 +1,26 @@
1
+ import type { ComponentType } from "react";
2
+ import type { Tour } from "../schema/index.js";
3
+ import { type TourLinkComponent, type TourSectionProps } from "./shared.js";
4
+ /**
5
+ * Every section Quest models, in page order. There is no section for data Quest does not
6
+ * have, and each section renders nothing when the package has no data for it.
7
+ */
8
+ export type TourSectionKey = "Hero" | "Overview" | "Highlights" | "Itinerary" | "Accommodation" | "Vehicle" | "Gallery" | "Inclusions" | "PaymentTerms" | "Pricing" | "Enquiry" | "Related";
9
+ export type TourSectionComponents = Record<TourSectionKey, ComponentType<TourSectionProps>>;
10
+ export declare const defaultTourSections: TourSectionComponents;
11
+ export type TourDetailProps = {
12
+ tour: Tour;
13
+ /**
14
+ * Replace individual sections: `components={{ Itinerary: MyItinerary }}`. Overrides are
15
+ * rendered on the server; for a client component, wrap it and pass only `tour`.
16
+ */
17
+ components?: Partial<TourSectionComponents>;
18
+ /** Hide sections on this site, e.g. `hide={["PaymentTerms"]}`. */
19
+ hide?: TourSectionKey[];
20
+ /** Base path of your tour detail route, used for related tours. Default `/tours`. */
21
+ hrefBase?: string;
22
+ hrefFor?: (slug: string) => string;
23
+ LinkComponent?: TourLinkComponent;
24
+ className?: string;
25
+ };
26
+ export declare function TourDetail({ tour, components, hide, hrefBase, hrefFor, LinkComponent, className, }: TourDetailProps): import("react").JSX.Element;
@@ -0,0 +1,55 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { TourPartySizeProvider, TourPricing } from "./party-size.js";
3
+ import { TourAccommodation, TourHero, TourHighlights, TourInclusions, TourItinerary, TourOverview, TourPaymentTerms, TourRelated, TourVehicle, } from "./sections.js";
4
+ import { defaultHrefFor } from "./shared.js";
5
+ import { TourContactPanel } from "./contact-panel.js";
6
+ import { TourGallery } from "./tour-gallery.js";
7
+ // Client sections get only `tour`: functions like `hrefFor` cannot cross the server/client boundary.
8
+ function GallerySection({ tour }) {
9
+ return _jsx(TourGallery, { tour: tour });
10
+ }
11
+ function PricingSection({ tour }) {
12
+ return _jsx(TourPricing, { tour: tour });
13
+ }
14
+ function EnquirySection({ tour }) {
15
+ return _jsx(TourContactPanel, { tour: tour });
16
+ }
17
+ export const defaultTourSections = {
18
+ Hero: TourHero,
19
+ Overview: TourOverview,
20
+ Highlights: TourHighlights,
21
+ Itinerary: TourItinerary,
22
+ Accommodation: TourAccommodation,
23
+ Vehicle: TourVehicle,
24
+ Gallery: GallerySection,
25
+ Inclusions: TourInclusions,
26
+ PaymentTerms: TourPaymentTerms,
27
+ Pricing: PricingSection,
28
+ Enquiry: EnquirySection,
29
+ Related: TourRelated,
30
+ };
31
+ const MAIN_SECTIONS = [
32
+ "Overview",
33
+ "Highlights",
34
+ "Itinerary",
35
+ "Accommodation",
36
+ "Vehicle",
37
+ "Gallery",
38
+ "Inclusions",
39
+ "PaymentTerms",
40
+ ];
41
+ export function TourDetail({ tour, components, hide = [], hrefBase = "/tours", hrefFor, LinkComponent = "a", className, }) {
42
+ const sections = { ...defaultTourSections, ...components };
43
+ const sectionProps = {
44
+ tour,
45
+ hrefFor: hrefFor ?? defaultHrefFor(hrefBase),
46
+ LinkComponent,
47
+ };
48
+ const render = (key) => {
49
+ if (hide.includes(key))
50
+ return null;
51
+ const Section = sections[key];
52
+ return _jsx(Section, { ...sectionProps }, key);
53
+ };
54
+ return (_jsx("article", { className: `quest-tours qt-detail ${className ?? ""}`.trim(), children: _jsxs(TourPartySizeProvider, { tour: tour, children: [render("Hero"), _jsxs("div", { className: "qt-layout", children: [_jsx("div", { className: "qt-main", children: MAIN_SECTIONS.map(render) }), _jsx("aside", { className: "qt-aside", "aria-label": "Price and enquiry", children: _jsxs("div", { className: "qt-aside-sticky", children: [render("Pricing"), render("Enquiry")] }) })] }), _jsx("div", { className: "qt-footer-sections", children: render("Related") })] }) }));
55
+ }
@@ -0,0 +1,4 @@
1
+ import type { Tour } from "../schema/index.js";
2
+ export declare function TourGallery({ tour }: {
3
+ tour: Tour;
4
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,35 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { Icon, TourImg, TourSection } from "./shared.js";
5
+ const GRID_LIMIT = 9;
6
+ export function TourGallery({ tour }) {
7
+ const dialogRef = useRef(null);
8
+ const [index, setIndex] = useState(null);
9
+ const images = tour.gallery;
10
+ const count = images.length;
11
+ const open = (next) => {
12
+ setIndex(next);
13
+ dialogRef.current?.showModal();
14
+ };
15
+ const step = useCallback((delta) => setIndex((current) => (current === null ? current : (current + delta + count) % count)), [count]);
16
+ useEffect(() => {
17
+ if (index === null)
18
+ return;
19
+ const onKey = (event) => {
20
+ if (event.key === "ArrowRight")
21
+ step(1);
22
+ if (event.key === "ArrowLeft")
23
+ step(-1);
24
+ };
25
+ window.addEventListener("keydown", onKey);
26
+ return () => window.removeEventListener("keydown", onKey);
27
+ }, [index, step]);
28
+ if (count === 0)
29
+ return null;
30
+ const active = index === null ? null : images[index];
31
+ return (_jsxs(TourSection, { id: "quest-gallery", title: "Gallery", children: [_jsx("ul", { className: "qt-gallery", children: images.slice(0, GRID_LIMIT).map((image, imageIndex) => (_jsx("li", { children: _jsxs("button", { type: "button", className: "qt-gallery-button", onClick: () => open(imageIndex), "aria-label": `View photo${image.alt ? `: ${image.alt}` : ""}`, children: [_jsx(TourImg, { image: image, sizes: "(min-width: 768px) 33vw, 50vw" }), imageIndex === GRID_LIMIT - 1 && count > GRID_LIMIT ? (_jsxs("span", { className: "qt-gallery-more", children: ["+", count - GRID_LIMIT] })) : null] }) }, image.url))) }), _jsxs("dialog", { ref: dialogRef, className: "qt-lightbox", onClose: () => setIndex(null), onClick: (event) => {
32
+ if (event.target === dialogRef.current)
33
+ dialogRef.current?.close();
34
+ }, children: [active ? (_jsxs("figure", { className: "qt-lightbox-figure", children: [_jsx("img", { src: active.url, srcSet: active.srcSet ?? undefined, sizes: "90vw", alt: active.alt }), _jsxs("figcaption", { children: [active.alt ? _jsx("span", { children: active.alt }) : null, _jsxs("span", { className: "qt-muted", children: [(index ?? 0) + 1, " / ", count] })] })] })) : null, _jsx("button", { type: "button", className: "qt-lightbox-close", onClick: () => dialogRef.current?.close(), "aria-label": "Close gallery", children: _jsx(Icon.X, {}) }), count > 1 ? (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "qt-lightbox-nav is-prev", onClick: () => step(-1), "aria-label": "Previous photo", children: _jsx(Icon.ArrowLeft, {}) }), _jsx("button", { type: "button", className: "qt-lightbox-nav is-next", onClick: () => step(1), "aria-label": "Next photo", children: _jsx(Icon.ArrowRight, {}) })] })) : null] })] }));
35
+ }
@@ -0,0 +1,7 @@
1
+ import type { TourSeo } from "../schema/index.js";
2
+ /** Escape `<` so JSON content can never close the script tag. */
3
+ export declare function serializeJsonLd(value: unknown): string;
4
+ /** Structured data Quest built for this page (TouristTrip, Offer, TravelAgency, breadcrumbs, images). */
5
+ export declare function TourJsonLd({ seo }: {
6
+ seo: TourSeo | null;
7
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /** Escape `<` so JSON content can never close the script tag. */
3
+ export function serializeJsonLd(value) {
4
+ return JSON.stringify(value).replace(/</g, "\\u003c");
5
+ }
6
+ /** Structured data Quest built for this page (TouristTrip, Offer, TravelAgency, breadcrumbs, images). */
7
+ export function TourJsonLd({ seo }) {
8
+ if (!seo || seo.jsonLd.length === 0)
9
+ return null;
10
+ return (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: {
11
+ __html: serializeJsonLd(seo.jsonLd.length === 1 ? seo.jsonLd[0] : seo.jsonLd),
12
+ } }));
13
+ }