Components / Data Display
Journey
Horizontal step-by-step flow for processes, onboarding, and architecture diagrams.
Timeline
Operator Side
Timeline
Guest Side
Cards
Operator Journey
Bidirectional
Steps connected both ways — showing how the first and last steps feed back into each other.
Iterative Flow
Source JourneyDemo.tsx
The exact code behind the live demo above. Fetch it raw at /components/source/journey.txt.
import { useState } from 'react';
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div className="mb-8">
<h3 className="text-sm font-bold text-[var(--flux-heading)] mb-3">{title}</h3>
{children}
</div>
);
}
interface JourneyStep {
icon: React.ReactNode;
iconBg: string;
title: string;
subtitle: string;
details: {
heading: string;
body: string;
bullets?: string[];
};
}
const ArrowIcon = () => (
<svg width="32" height="16" viewBox="0 0 32 16" fill="none" className="flex-shrink-0 text-[var(--flux-grey-200)]">
<line x1="0" y1="8" x2="22" y2="8" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />
<path d="M20 3l7 5-7 5" stroke="currentColor" strokeWidth="2.5" fill="none" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
function StepCard({
step,
onClick,
}: {
step: JourneyStep;
onClick: () => void;
}) {
return (
<button
onClick={onClick}
className="group flex flex-col items-center text-center transition-all duration-200 cursor-pointer"
style={{ minWidth: 140 }}
>
<div
className="w-14 h-14 rounded flex items-center justify-center mb-3 transition-all duration-200 group-hover:scale-110 group-hover:shadow-lg"
style={{ backgroundColor: step.iconBg }}
>
{step.icon}
</div>
<p className="text-sm font-bold text-[var(--flux-heading)] mb-0.5 group-hover:text-[var(--flux-primary-400)] transition-colors">
{step.title}
</p>
<p className="text-xs text-[var(--flux-black)] font-light leading-snug">
{step.subtitle}
</p>
</button>
);
}
function DetailModal({
step,
onClose,
}: {
step: JourneyStep;
onClose: () => void;
}) {
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center"
onClick={onClose}
>
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" />
<div
className="relative bg-[var(--flux-surface)] rounded shadow-xl max-w-md w-full mx-4 overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center gap-3 p-5 border-b border-[var(--flux-grey-100)]">
<div
className="w-10 h-10 rounded flex items-center justify-center flex-shrink-0"
style={{ backgroundColor: step.iconBg }}
>
{step.icon}
</div>
<div className="flex-1 min-w-0">
<h3 className="text-base font-bold text-[var(--flux-heading)]">{step.title}</h3>
<p className="text-xs text-[var(--flux-black)] font-light">{step.subtitle}</p>
</div>
<button
onClick={onClose}
className="w-8 h-8 flex items-center justify-center rounded text-[var(--flux-black)] hover:bg-[var(--flux-grey-50)] transition-colors"
>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
<path d="M1 1l12 12M13 1L1 13" />
</svg>
</button>
</div>
<div className="p-5">
<h4 className="text-sm font-bold text-[var(--flux-heading)] mb-2">{step.details.heading}</h4>
<p className="text-sm text-[var(--flux-black)] leading-relaxed mb-3">{step.details.body}</p>
{step.details.bullets && (
<ul className="space-y-1.5 list-disc pl-5 text-sm text-[var(--flux-black)]">
{step.details.bullets.map((b, i) => (
<li key={i}>{b}</li>
))}
</ul>
)}
</div>
</div>
</div>
);
}
const HomeIcon = () => (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 12l9-8 9 8" />
<path d="M5 10v9a1 1 0 001 1h3v-5h6v5h3a1 1 0 001-1v-9" />
</svg>
);
const CaptureIcon = () => (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="4" y="4" width="16" height="16" rx="2" />
<path d="M8 8h8M8 12h8M8 16h4" />
</svg>
);
const ConvertIcon = () => (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
</svg>
);
const ConfigureIcon = () => (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="3" />
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
</svg>
);
const operatorSteps: JourneyStep[] = [
{
icon: <HomeIcon />,
iconBg: '#056F82',
title: 'Quest Home',
subtitle: 'Lightning App',
details: {
heading: 'Entry point',
body: 'The Quest Home page is the operator\'s starting point inside Salesforce. It provides a dashboard overview of active leads, recent searches, and quick-access shortcuts.',
bullets: [
'Salesforce Lightning App',
'Dashboard with active pipeline',
'Quick search and recent activity',
],
},
},
{
icon: <CaptureIcon />,
iconBg: '#4285FF',
title: 'Lead Capture',
subtitle: 'Salesforce Lead',
details: {
heading: 'Capture enquiry',
body: 'Incoming travel enquiries are captured as Salesforce Leads. This step gathers traveller details, preferences, and initial requirements.',
bullets: [
'Standard Salesforce Lead object',
'Traveller preferences and requirements',
'Source tracking and attribution',
],
},
},
{
icon: <ConvertIcon />,
iconBg: '#FFBC42',
title: 'Convert',
subtitle: 'Quest Canvas + API',
details: {
heading: 'Build the itinerary',
body: 'The lead is converted into a bookable trip using Quest Canvas. Operators search availability, configure packages, and build a complete itinerary with real-time pricing.',
bullets: [
'Quest Canvas visual builder',
'Real-time availability and pricing via API',
'Multi-day itinerary configuration',
'Dynamic package assembly',
],
},
},
{
icon: <ConfigureIcon />,
iconBg: '#DE37A4',
title: 'Configure',
subtitle: 'Quest App',
details: {
heading: 'Finalise and book',
body: 'Final configuration, pricing adjustments, and booking confirmation happen in the Quest App. The operator reviews the complete package before submitting.',
bullets: [
'Pricing review and adjustments',
'Add-ons and optional services',
'Booking confirmation and payment',
'Automated confirmation emails',
],
},
},
];
const GuestIcon = () => (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="8" r="4" />
<path d="M20 21a8 8 0 00-16 0" />
</svg>
);
const SearchIcon = () => (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="11" cy="11" r="7" />
<path d="M21 21l-4.35-4.35" />
</svg>
);
const CartIcon = () => (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z" />
<line x1="3" y1="6" x2="21" y2="6" />
<path d="M16 10a4 4 0 01-8 0" />
</svg>
);
const ConfirmIcon = () => (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M22 11.08V12a10 10 0 11-5.93-9.14" />
<polyline points="22 4 12 14.01 9 11.01" />
</svg>
);
const guestSteps: JourneyStep[] = [
{
icon: <GuestIcon />,
iconBg: '#034955',
title: 'Browse',
subtitle: 'Web / App',
details: {
heading: 'Discovery',
body: 'The guest browses available travel packages through the operator\'s website or app, powered by Quest search.',
bullets: ['Curated package listings', 'Filter by destination, date, and budget', 'Responsive web and mobile experience'],
},
},
{
icon: <SearchIcon />,
iconBg: '#056F82',
title: 'Search',
subtitle: 'Quest API',
details: {
heading: 'Find availability',
body: 'Real-time search powered by Quest API returns available packages with pricing, availability, and options.',
bullets: ['Real-time availability', 'Dynamic pricing', 'Package comparison'],
},
},
{
icon: <CartIcon />,
iconBg: '#FFBC42',
title: 'Customise',
subtitle: 'Package Builder',
details: {
heading: 'Build your trip',
body: 'The guest selects options, adds extras, and customises their trip to their preferences before checkout.',
bullets: ['Room and cabin selection', 'Optional add-ons and upgrades', 'Traveller details and preferences'],
},
},
{
icon: <ConfirmIcon />,
iconBg: '#2BA711',
title: 'Confirm',
subtitle: 'Checkout',
details: {
heading: 'Book and pay',
body: 'Secure checkout with payment processing, booking confirmation, and automated communications.',
bullets: ['Secure payment processing', 'Instant booking confirmation', 'Email and SMS notifications'],
},
},
];
function JourneyFlow({
steps,
label,
labelColor,
}: {
steps: JourneyStep[];
label: string;
labelColor: string;
}) {
const [activeStep, setActiveStep] = useState<number | null>(null);
return (
<div className="p-6 rounded border border-[var(--flux-grey-100)] bg-[var(--flux-surface)]">
<p
className="text-xs font-bold tracking-widest uppercase mb-6"
style={{ color: labelColor }}
>
{label}
</p>
<div className="flex items-start justify-center gap-3 sm:gap-4 overflow-x-auto pt-2 pb-2">
{steps.map((step, i) => (
<div key={step.title} className="flex items-center gap-3 sm:gap-4">
<StepCard step={step} onClick={() => setActiveStep(i)} />
{i < steps.length - 1 && <ArrowIcon />}
</div>
))}
</div>
{activeStep !== null && (
<DetailModal
step={steps[activeStep]}
onClose={() => setActiveStep(null)}
/>
)}
</div>
);
}
interface CardStep {
icon: React.ReactNode;
iconBg: string;
title: string;
subtitle: string;
image: string;
description: string;
features: string[];
tag?: string;
}
const cardSteps: CardStep[] = [
{
icon: <HomeIcon />,
iconBg: '#056F82',
title: 'Quest Home',
subtitle: 'Lightning App',
image: '/images/hero-quest.png',
tag: 'Entry Point',
description: 'The operator\'s command centre inside Salesforce. Quest Home surfaces active leads, recent searches, and pipeline health in a single dashboard — removing the need to navigate between tabs.',
features: [
'Dashboard with pipeline overview and KPIs',
'Quick-access shortcuts to active itineraries',
'Recent search history and saved filters',
'Role-based views for agents and managers',
],
},
{
icon: <CaptureIcon />,
iconBg: '#4285FF',
title: 'Lead Capture',
subtitle: 'Salesforce Lead',
image: '/images/hero-circle.png',
tag: 'Capture',
description: 'Every travel enquiry begins as a Salesforce Lead. This step captures traveller details, preferences, and source attribution — ensuring nothing falls through the cracks.',
features: [
'Standard Salesforce Lead object integration',
'Traveller preferences and special requirements',
'Source tracking, UTM attribution, and referral data',
'Automated lead assignment and routing rules',
],
},
{
icon: <ConvertIcon />,
iconBg: '#FFBC42',
title: 'Convert',
subtitle: 'Quest Canvas + API',
image: '/images/hero-travel.png',
tag: 'Build',
description: 'Quest Canvas transforms a lead into a bookable itinerary. Operators search live availability, drag-and-drop components, and see real-time pricing — all within a visual builder.',
features: [
'Visual drag-and-drop itinerary builder',
'Real-time availability and pricing via Quest API',
'Multi-day, multi-destination configuration',
'Dynamic package assembly with optional add-ons',
],
},
{
icon: <ConfigureIcon />,
iconBg: '#034955',
title: 'Configure',
subtitle: 'Quest App',
image: '/images/card-example.png',
tag: 'Finalise',
description: 'The final step before booking. Operators review the complete package, apply pricing adjustments, add optional services, and confirm the reservation with a single action.',
features: [
'Full pricing review with margin controls',
'Add-on services and experience upgrades',
'Payment processing and deposit management',
'Automated confirmation emails and documentation',
],
},
{
icon: <ConfirmIcon />,
iconBg: '#2BA711',
title: 'Confirm',
subtitle: 'Booking Engine',
image: '/images/hero-travel.png',
tag: 'Book',
description: 'The booking is confirmed and payment is processed. The system generates all required documents and triggers downstream fulfilment workflows.',
features: [
'Secure payment capture and receipts',
'Booking reference generation',
'Supplier notifications and allocations',
'Traveller confirmation and documents',
],
},
{
icon: <SearchIcon />,
iconBg: '#056F82',
title: 'Monitor',
subtitle: 'Operations Dashboard',
image: '/images/hero-quest.png',
tag: 'Track',
description: 'Active bookings are monitored through the operations dashboard. Operators track supplier confirmations, amendment requests, and upcoming departures.',
features: [
'Real-time booking status tracking',
'Supplier confirmation management',
'Amendment and cancellation workflows',
'Departure countdown and readiness checks',
],
},
];
function JourneyCard({ step, index, onClick }: { step: CardStep; index: number; onClick: () => void }) {
return (
<button
onClick={onClick}
className="group rounded border border-[var(--flux-grey-100)] bg-[var(--flux-surface)] overflow-hidden transition-shadow duration-300 hover:shadow-lg text-left cursor-pointer w-full"
>
<div className="relative h-28 overflow-hidden">
<img
src={step.image}
alt={step.title}
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
/>
<div className="absolute inset-0" style={{ background: 'linear-gradient(to top, rgba(3,46,54,0.7) 0%, transparent 60%)' }} />
<div className="absolute top-2 left-2">
<span
className="text-[9px] font-bold tracking-widest uppercase px-2 py-0.5 rounded text-white"
style={{ backgroundColor: step.iconBg }}
>
{step.tag || `Step ${index + 1}`}
</span>
</div>
<div className="absolute bottom-2 left-2 flex items-center gap-2">
<div
className="w-7 h-7 rounded flex items-center justify-center flex-shrink-0"
style={{ backgroundColor: step.iconBg }}
>
<span className="scale-75">{step.icon}</span>
</div>
<div>
<p className="text-xs font-bold text-white leading-tight">{step.title}</p>
<p className="text-[10px] text-white/70 font-light">{step.subtitle}</p>
</div>
</div>
</div>
<div className="p-3">
<p className="text-xs text-[var(--flux-black)] leading-relaxed" style={{ display: '-webkit-box', WebkitLineClamp: 3, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>
{step.description}
</p>
</div>
</button>
);
}
function CardDetailModal({ step, onClose }: { step: CardStep; onClose: () => void }) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center" onClick={onClose}>
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" />
<div
className="relative bg-[var(--flux-surface)] rounded shadow-xl max-w-lg w-full mx-4 overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
<div className="relative h-44 overflow-hidden">
<img src={step.image} alt={step.title} className="w-full h-full object-cover" />
<div className="absolute inset-0" style={{ background: 'linear-gradient(to top, rgba(3,46,54,0.85) 0%, rgba(3,46,54,0.2) 60%)' }} />
<button
onClick={onClose}
className="absolute top-3 right-3 w-8 h-8 flex items-center justify-center rounded bg-black/30 text-white hover:bg-black/50 transition-colors"
>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
<path d="M1 1l12 12M13 1L1 13" />
</svg>
</button>
<div className="absolute bottom-3 left-4 flex items-center gap-3">
<div
className="w-10 h-10 rounded flex items-center justify-center flex-shrink-0"
style={{ backgroundColor: step.iconBg }}
>
{step.icon}
</div>
<div>
<p className="text-base font-bold text-white">{step.title}</p>
<p className="text-xs text-white/70 font-light">{step.subtitle}</p>
</div>
</div>
</div>
<div className="p-5">
<p className="text-sm text-[var(--flux-black)] leading-relaxed mb-4">{step.description}</p>
<ul className="space-y-2 list-disc pl-5 text-sm text-[var(--flux-black)]">
{step.features.map((f, i) => (
<li key={i}>{f}</li>
))}
</ul>
</div>
</div>
</div>
);
}
const LeftArrowIcon = () => (
<svg width="32" height="16" viewBox="0 0 32 16" fill="none" className="flex-shrink-0 text-[var(--flux-grey-200)]">
<line x1="32" y1="8" x2="10" y2="8" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />
<path d="M12 3l-7 5 7 5" stroke="currentColor" strokeWidth="2.5" fill="none" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
function ReturnArrow({ side }: { side: 'right' | 'left' }) {
return (
<div className={`flex ${side === 'right' ? 'justify-end pr-[92px]' : 'justify-start pl-[92px]'} py-1`}>
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" className="text-[var(--flux-grey-200)]">
<path d="M16 0 C16 14 16 16 6 16 C16 16 16 18 16 32" stroke="currentColor" strokeWidth="2.5" fill="none" strokeLinecap="round" strokeLinejoin="round" />
<path d="M12 28l4 6 4-6" stroke="currentColor" strokeWidth="2.5" fill="none" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
);
}
function CardFlow({ steps, label }: { steps: CardStep[]; label: string }) {
const [activeStep, setActiveStep] = useState<number | null>(null);
const perRow = 4;
const rows: CardStep[][] = [];
for (let i = 0; i < steps.length; i += perRow) {
rows.push(steps.slice(i, i + perRow));
}
return (
<div>
<p className="text-xs font-bold tracking-widest uppercase mb-4" style={{ color: '#056F82' }}>
{label}
</p>
{rows.map((row, rowIdx) => {
const reversed = rowIdx % 2 === 1;
const displayRow = reversed ? [...row].reverse() : row;
const startIdx = rowIdx * perRow;
const hasNextRow = rowIdx < rows.length - 1;
return (
<div key={rowIdx}>
<div className={`flex items-start gap-3 pt-2 pb-2 ${reversed ? 'justify-end' : ''}`}>
{displayRow.map((step, colIdx) => {
const actualIdx = reversed
? startIdx + (row.length - 1 - colIdx)
: startIdx + colIdx;
const isLastInRow = colIdx === displayRow.length - 1;
return (
<div key={step.title} className="flex items-center gap-3 flex-shrink-0" style={{ width: 200 }}>
<div className="w-full">
<JourneyCard step={step} index={actualIdx} onClick={() => setActiveStep(actualIdx)} />
</div>
{!isLastInRow && (
<div className="flex-shrink-0">
{reversed ? <LeftArrowIcon /> : <ArrowIcon />}
</div>
)}
</div>
);
})}
</div>
{hasNextRow && (
<ReturnArrow side={reversed ? 'left' : 'right'} />
)}
</div>
);
})}
{activeStep !== null && (
<CardDetailModal step={steps[activeStep]} onClose={() => setActiveStep(null)} />
)}
</div>
);
}
const BiArrowIcon = () => (
<svg width="32" height="20" viewBox="0 0 32 20" fill="none" className="flex-shrink-0 text-[var(--flux-grey-200)]">
<line x1="6" y1="7" x2="26" y2="7" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />
<path d="M24 3l5 4-5 4" stroke="currentColor" strokeWidth="2.5" fill="none" strokeLinecap="round" strokeLinejoin="round" />
<line x1="26" y1="14" x2="6" y2="14" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />
<path d="M8 10l-5 4 5 4" stroke="currentColor" strokeWidth="2.5" fill="none" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
function BidirectionalFlow({ steps, label }: { steps: CardStep[]; label: string }) {
const [activeStep, setActiveStep] = useState<number | null>(null);
const display = steps.slice(0, 4);
return (
<div>
<p className="text-xs font-bold tracking-widest uppercase mb-4" style={{ color: '#056F82' }}>
{label}
</p>
<div className="relative pb-8">
<div className="flex items-start gap-3 pt-2 pb-2">
{display.map((step, i) => (
<div key={step.title} className="flex items-center gap-3 flex-shrink-0" style={{ width: 200 }}>
<div className="w-full">
<JourneyCard step={step} index={i} onClick={() => setActiveStep(i)} />
</div>
{i < display.length - 1 && (
<div className="flex-shrink-0">
{i === 0 || i === display.length - 2
? <BiArrowIcon />
: <ArrowIcon />
}
</div>
)}
</div>
))}
</div>
<svg
className="absolute bottom-0 left-0 w-full text-[var(--flux-grey-200)]"
height="32"
viewBox="0 0 900 32"
fill="none"
preserveAspectRatio="none"
style={{ overflow: 'visible' }}
>
<path
d="M 100 0 C 100 24, 200 30, 450 30 C 700 30, 800 24, 800 0"
stroke="currentColor"
strokeWidth="2.5"
fill="none"
strokeLinecap="round"
/>
<path d="M 96 6 L 100 0 L 106 5" stroke="currentColor" strokeWidth="2.5" fill="none" strokeLinecap="round" strokeLinejoin="round" />
<path d="M 794 5 L 800 0 L 804 6" stroke="currentColor" strokeWidth="2.5" fill="none" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
{activeStep !== null && (
<CardDetailModal step={display[activeStep]} onClose={() => setActiveStep(null)} />
)}
</div>
);
}
export default function JourneyDemo() {
return (
<div>
<Section title="Timeline">
<JourneyFlow
steps={operatorSteps}
label="Operator Side"
labelColor="#056F82"
/>
</Section>
<Section title="Timeline">
<JourneyFlow
steps={guestSteps}
label="Guest Side"
labelColor="#056F82"
/>
</Section>
<Section title="Cards">
<CardFlow steps={cardSteps} label="Operator Journey" />
</Section>
<Section title="Bidirectional">
<p className="text-xs text-[var(--flux-black)] mb-3">
Steps connected both ways — showing how the first and last steps feed back into each other.
</p>
<BidirectionalFlow steps={cardSteps} label="Iterative Flow" />
</Section>
</div>
);
}