Added Tour Navigator

This commit is contained in:
2025-10-21 23:13:32 +10:00
parent ef9318b5a3
commit 9b470e07d4
9 changed files with 471 additions and 149 deletions

View File

@@ -0,0 +1,43 @@
<template>
<article class="tour-actions">
<EdgyButton v-if="!tourDay" classes="btn-primary chamfer">
<ButtonLink :href="'/adventures/'+(tour.internal_name)+'/day/1'">
Explore Itinerary
</ButtonLink>
</EdgyButton>
<EdgyButton v-if="tourDay" classes="btn-secondary chamfer">
<ButtonLink href="#">
Previous Day
</ButtonLink>
</EdgyButton>
<EdgyButton v-if="tourDay" classes="btn-primary chamfer">
<ButtonLink href="#">
Next Day
</ButtonLink>
</EdgyButton>
</article>
</template>
<script setup lang="ts">
import EdgyButton from "@/components/dredgy/EdgyButton.vue";
import type {Tour, TourDay} from "@/types";
import ButtonLink from "@/components/dredgy/ButtonLink.vue";
interface Props {
tourDay?: TourDay;
tour: Tour;
}
defineProps<Props>();
</script>
<style scoped>
.tour-actions {
display: flex;
gap: 2em;
align-items: center;
justify-content: center;
width: 100%;
flex-basis: 10%;
}
</style>

View File

@@ -1,77 +1,159 @@
<template>
<aside class="tour-aside chamfer" v-show-on-intersect="{type:'fade-up'}">
<h3 class="aside-title">Quick Facts</h3>
<ul class="facts">
<li v-if="tour.tour_days?.length > 0">
<span class="label">Length</span>
<span class="value">{{ tour.tour_days?.length }} days</span>
</li>
<li v-if="tour.level">
<span class="label">Fitness</span>
<span class="value">{{ tour.level }}</span>
</li>
<li v-if="true">
<span class="label">Price</span>
<span class="value">{{ Intl.NumberFormat(undefined, { style: 'currency', currency: 'USD' }).format(tour.price) }}</span>
</li>
<li v-if="tour.min_people || tour.max_people">
<span class="label">Group Size</span>
<span class="value">
{{ tour.min_people ?? '?' }}{{ tour.max_people ?? '?' }} people
</span>
</li>
<li v-if="Array.isArray(tour.countries) && tour.countries.length">
<span class="label">Countries</span>
<span class="value">
{{ tour.countries?.map((c:any)=>c.name ?? c).join(', ') }}
</span>
</li>
</ul>
<h3 class="aside-title">Upcoming Departures</h3>
</aside>
<div class="tour-aside chamfer" v-show-on-intersect="{type:'fade-up'}">
<h3 class="aside-title">Quick Facts</h3>
<ul class="facts">
<li v-if="tour.tour_days?.length > 0">
<span class="label">Length</span>
<span class="value">{{ tour.tour_days?.length }} days</span>
</li>
<li v-if="tour.level">
<span class="label">Fitness</span>
<span class="value">{{ tour.level }}</span>
</li>
<li v-if="true">
<span class="label">Price From</span>
<span class="value">{{
Intl.NumberFormat(undefined, {
style: 'currency',
currency: 'USD'
}).format(tour.price)
}}</span>
</li>
<li v-if="tour.min_people || tour.max_people">
<span class="label">Group Size</span>
<span class="value">{{ tour.min_people ?? '?' }}{{ tour.max_people ?? '?' }} people</span>
</li>
<li v-if="Array.isArray(tour.countries) && tour.countries.length">
<span class="label">Countries</span>
<span class="value">{{ tour.countries?.map((c: any) => c.name ?? c).join(', ') }}</span>
</li>
<li class="departures-item">
<span class="label">Departures</span>
<span class="value">No Upcoming Departures</span>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { defineProps } from "vue";
import type { Tour } from "@/types";
import {defineProps} from "vue";
import type {Tour} from "@/types";
defineProps<{ tour: Tour }>();
</script>
<style scoped>
.tour-aside {
background: var(--card);
color: var(--foreground);
border: 1px solid color-mix(in oklab, var(--foreground), transparent 85%);
padding: 1.25rem;
height: 100%;
background: var(--card);
color: var(--foreground);
border: 1px solid color-mix(in oklab, var(--foreground), transparent 85%);
padding: 1.25rem;
flex-grow: 1;
height: 100%;
}
.aside-title {
margin: 1rem 0 1rem 0;
font-size: 1rem;
color: var(--foreground);
letter-spacing: 0.02em;
text-transform: uppercase;
margin: 1rem 0 1rem 0;
font-size: 1rem;
color: var(--foreground);
letter-spacing: 0.02em;
text-transform: uppercase;
}
.facts {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 0.5rem;
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 0.5rem;
}
.facts li {
display: grid;
grid-template-columns: 1fr auto;
gap: 1rem;
padding: 0.5rem 0.25rem;
border-bottom: 1px dashed color-mix(in oklab, var(--foreground), transparent 90%);
display: grid;
grid-template-columns: 1fr auto;
gap: 1rem;
padding: 0.5rem 0.25rem;
border-bottom: 1px dashed color-mix(in oklab, var(--foreground), transparent 90%);
}
.facts .label {
color: var(--muted-foreground);
color: var(--muted-foreground);
}
.facts .value {
color: var(--foreground);
font-weight: 600;
color: var(--foreground);
font-weight: 600;
text-align: right;
}
/* Mobile responsive */
@media (max-width: 1023px) {
.tour-aside {
padding: 2em 0.75em 0 2em;
}
.aside-title {
margin: 0 0 0.5rem 0;
font-size: 0.65rem;
font-weight: 700;
}
.aside-title:not(:first-of-type) {
margin-top: 0.5rem;
}
.facts {
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.facts li {
display: flex;
flex-direction: column;
gap: 0.125rem;
padding: 0;
border-bottom: none;
}
.departures-item {
grid-column: span 2;
}
.facts .label {
font-size: 0.65rem;
letter-spacing: 0.05em;
text-transform: uppercase;
font-weight: 600;
}
.facts .value {
font-size: 0.8rem;
text-align: left;
}
.aside-title:first-of-type {
display: none;
}
@media (min-width: 768px) {
.aside-title:first-of-type {
display: block;
}
.facts {
grid-template-columns: 1fr auto;
}
.facts li {
display: grid;
grid-template-columns: auto;
border-bottom: 1px dashed color-mix(in oklab, var(--foreground), transparent 90%);
padding: 0.5rem 0.25rem;
}
}
}
</style>

View File

@@ -7,7 +7,7 @@ defineProps({
</script>
<template>
<Link :href="href" style="display:block;width:100%;height:100%" >
<Link :href="href || '#'" style="display:block;width:100%;height:100%" >
<slot/>
</Link>
</template>

View File

@@ -145,7 +145,10 @@ main{
display: flex; /* added */
flex-direction: column; /* keep */
flex: 1;
gap: 2em;/* keep */
}
main:not(:has(.no-gap)) {
gap: 2em;
}
/* Push the Footer (last child in main) to the bottom if content is short */

View File

@@ -2,16 +2,16 @@
import { usePage } from "@inertiajs/vue3";
import AppLayout from "@/layouts/AppLayout.vue";
import TourOverviewSection from "./TourOverviewSection.vue";
import type { Tour } from "@/types";
import type {Tour, TourDay} from "@/types";
import TourQuickFacts from "@/components/TourNavigator/TourQuickFacts.vue";
import SectionTitle from "@/components/dredgy/SectionTitle.vue";
interface Properties {
tour: Tour;
nextHref?: string | null;
tourDay: TourDay
}
const { tour, nextHref } = usePage().props as unknown as Properties;
const { tour, tourDay} = usePage().props as unknown as Properties;
defineOptions({
layout: AppLayout,
@@ -19,18 +19,8 @@ defineOptions({
</script>
<template>
<section class="tour-overview-section">
<SectionTitle
title=""
:gradient="tour.name"
:subtitle="tour.short_description ?? ''"
/>
</section>
<TourOverviewSection :tour="tour" />
<TourOverviewSection :tourDay="tourDay" :tour="tour" />
</template>
<style scoped>
.tour-overview-section{
margin-top: 10dvh;
}
</style>

View File

@@ -1,97 +1,300 @@
<script setup lang="ts">
import SectionContainer from "@/components/dredgy/SectionContainer.vue";
import SectionTitle from "@/components/dredgy/SectionTitle.vue";
import GradientText from "@/components/dredgy/GradientText.vue";
import EdgyButton from "@/components/dredgy/EdgyButton.vue";
import ButtonLink from "@/components/dredgy/ButtonLink.vue";
import TourQuickFacts from "@/components/TourNavigator/TourQuickFacts.vue";
import type { Tour } from "@/types";
import TourOverviewActions from "@/components/TourNavigator/TourOverviewActions.vue";
import type {Tour, TourDay} from "@/types";
import {ref} from "vue";
interface Props {
tour: Tour;
tour: Tour;
tourDay?: TourDay;
}
const currentStep = ref(1)
const props = defineProps<Props>();
</script>
<template>
<section class="tour-overview">
<SectionContainer>
<div
class="relative chamfer bg-neutral-900/70 border border-neutral-700/60 shadow-xl shadow-black/30 backdrop-blur-sm"
>
<!-- Banner with overlapping title -->
<div class="relative">
<div class="aspect-[16/7] md:aspect-[16/6] lg:aspect-[16/5] overflow-hidden">
<img
:src="`/img/tours/${props.tour.internal_name}.jpg`"
:alt="tour.name + 'Banner'"
class="w-full h-full object-cover"
loading="lazy"
/>
</div>
<!-- Overlapping H1 with glass background (no rounded, chamfered) -->
<div class="absolute left-6 md:left-8 -bottom-8">
<div
class="chamfer border border-white/10 glass shadow-lg shadow-black/30 px-4 md:px-5 py-2.5"
>
<h1 style="padding:0.5em" class="text-2xl md:text-3xl lg:text-4xl font-semibold tracking-tight">
<GradientText>About this Adventure</GradientText>
</h1>
</div>
</div>
<section v-show-on-intersect class="no-gap tour-overview chamfer-tl">
<div
:style="{ backgroundImage: `url('/img/tours/${tour.internal_name}.jpg')` }"
style="background-attachment: scroll; background-size: cover; background-position: center"
class="tour-overview-header"
>
<h1 class="glass chamfer">
<GradientText>{{ tour.name }}</GradientText>
</h1>
</div>
<div class="tour-overview-body">
<section class="tour-content">
<article class="tour-description">
<div v-if="tour.tour_days?.length" class="custom-stepper">
<!-- Horizontal Stepper Header -->
<div class="stepper-header">
<button
v-for="(day, index) in tour.tour_days"
:key="day.id"
class="stepper-step"
:class="{
active: currentStep === index + 1,
complete: currentStep > index + 1
}"
@click="currentStep = index + 1"
>
<span class="step-number">Day {{ index + 1 }}</span>
</button>
</div>
<!-- Content area: flex columns, fixed height, right column flush with edge -->
<div>
<div class="flex gap-6 h-[560px] md:h-[600px] lg:h-[640px]">
<!-- Main description (wider column) -->
<div class="flex-1 pl-6 md:pl-8">
<div class="text-neutral-200 leading-relaxed h-full overflow-auto pr-1 pt-14 ">
<div
v-html="props.tour.long_description || 'No long description available'"
></div>
</div>
</div>
<!-- Stepper Content -->
<div class="stepper-content">
<div
v-for="(day, index) in tour.tour_days"
:key="day.id"
v-show="currentStep === index + 1"
class="step-item"
>
<h2 class="text-h3 font-weight-bold mb-2">
Day {{ index + 1 }}
</h2>
<p class="text-h6 text-grey mb-6">
{{ day.description }}
</p>
<!-- Quick facts (narrower right column, flush right, chamfer top-left only) -->
<aside class="w-80 md:w-88 lg:w-96 h-full">
<div
class="h-full chamfer-tl bg-neutral-900/40 backdrop-blur-sm ring-1 ring-neutral-700/50"
>
<TourQuickFacts :tour="props.tour" />
</div>
<div v-if="day.image" class="mb-6">
<v-img
:src="day.image"
:alt="`Day ${index + 1}`"
class="rounded"
height="300"
cover
/>
</div>
<div class="text-body1 line-height-relaxed mb-6">
{{ day.content }}
</div>
<div class="d-flex gap-3">
<v-btn
v-if="index > 0"
variant="outlined"
@click="currentStep = index"
>
Previous
</v-btn>
<v-btn
v-if="index < tour.tour_days.length - 1"
color="primary"
@click="currentStep = index + 2"
>
Next
</v-btn>
<v-btn
v-else
color="success"
disabled
>
Tour Complete
</v-btn>
</div>
</div>
</div>
</div>
</article>
</section>
<aside class="chamfer chamfer-tl">
<TourQuickFacts :tour="tour" />
</aside>
</div>
</div>
</div>
</SectionContainer>
</section>
</section>
</template>
<style scoped>
.chamfer {
--cut: 20px;
clip-path: polygon(
0 0,
calc(100% - var(--cut)) 0,
100% var(--cut),
100% 100%,
var(--cut) 100%,
0 calc(100% - var(--cut))
);
.tour-overview {
margin: 5%;
background: var(--sidebar-background);
align-self: center;
justify-self: center;
min-height: 70vh;
width: 100%;
max-width: 1200px;
display: flex;
flex-direction: column;
}
.tour-overview-header {
width: 100%;
height: auto;
flex-shrink: 0;
position: relative;
min-height: 300px;
}
.tour-overview-header h1 {
position: absolute;
bottom: 0;
transform: translateY(50%) translateX(-50%);
left: 37.5%;
padding: 0.2em 0.75em;
text-align: center;
font-size: 2.5em;
margin: 0;
z-index: 999;
}
.tour-overview-body {
flex: 1;
width: 100%;
display: flex;
gap: 2rem;
}
.tour-overview-body .tour-content {
flex: 1;
padding: 2em;
display: flex;
flex-direction: column;
align-items: stretch;
min-height: 0;
}
.tour-description {
width: 100%;
display: flex;
flex-direction: column;
}
.tour-overview-body aside {
display: flex;
flex-direction: column;
flex-basis: 25%;
flex-shrink: 0;
}
@media (max-width: 768px) {
.tour-overview {
height: auto;
}
.tour-overview-header h1 {
font-size: 1.5em;
left: 50%;
}
.tour-overview-body {
flex-direction: column;
gap: 1rem;
}
.tour-overview-body aside {
order: 1;
height: auto;
min-height: 0;
}
.tour-overview-body .tour-content {
padding-top: 0.5em;
order: 2;
}
}
.chamfer {
--cut: 20px;
clip-path: polygon(
0 0,
calc(100% - var(--cut)) 0,
100% var(--cut),
100% 100%,
var(--cut) 100%,
0 calc(100% - var(--cut))
);
}
/* Only the top-left corner chamfered (for the right column box) */
.chamfer-tl {
--cut: 20px;
clip-path: polygon(
var(--cut) 0,
100% 0,
100% 100%,
0 100%,
0 0
);
--cut: 20px;
clip-path: polygon(
var(--cut) 0,
100% 0,
100% 100%,
0 100%,
0 0
);
}
.line-height-relaxed {
line-height: 1.6;
}
.custom-stepper {
width: 100%;
display: flex;
flex-direction: column;
gap: 2rem;
}
.stepper-header {
display: flex;
gap: 1rem;
overflow-x: auto;
padding: 1rem 0;
border-bottom: 2px solid rgba(255, 255, 255, 0.1);
}
.stepper-step {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
background: transparent;
border: 2px solid rgba(255, 255, 255, 0.3);
color: rgba(255, 255, 255, 0.6);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
flex-shrink: 0;
}
.stepper-step:hover {
border-color: rgba(255, 255, 255, 0.6);
color: rgba(255, 255, 255, 0.9);
}
.stepper-step.active {
border-color: #1976d2;
background-color: rgba(25, 118, 210, 0.1);
color: #1976d2;
}
.stepper-step.complete {
border-color: #4caf50;
color: #4caf50;
}
.step-number {
font-weight: bold;
font-size: 1.2em;
}
.step-label {
font-size: 0.875em;
}
.stepper-content {
flex: 1;
overflow-y: auto;
}
.step-item {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>