Added About Page
This commit is contained in:
39
resources/js/components/Footer.vue
Normal file
39
resources/js/components/Footer.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="footer">
|
||||
<span>© Dr Edgy Adventures</span>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.footer {
|
||||
will-change: transform, opacity;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 5dvh;
|
||||
|
||||
/* glassy background */
|
||||
background: hsla(30, 100%, 46%, 0.8);
|
||||
|
||||
/* frosted borders */
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.25);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.15);
|
||||
|
||||
/* chamfered corners */
|
||||
clip-path: polygon(
|
||||
20px 0, /* top-left chamfer */
|
||||
calc(100% - 20px) 0, /* top-right chamfer */
|
||||
100% 20px,
|
||||
100% 100%,
|
||||
0 100%,
|
||||
0 20px /* bottom-left chamfer */
|
||||
);
|
||||
|
||||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15);
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
@@ -3,29 +3,25 @@
|
||||
<header class="header">
|
||||
<section class="navContainer">
|
||||
<nav class="nav">
|
||||
<div class="nav-brand">Dr Edgy Adventures</div>
|
||||
<div class="nav-brand">
|
||||
<Link href="/">
|
||||
<img src="/img/logos/logo_main.png" alt="Dr Edgy Logo" style="height:5vh; width:auto;" />
|
||||
</Link>
|
||||
</div>
|
||||
<button class="mobile-menu-btn">
|
||||
<span></span><span></span><span></span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="nav-menu">
|
||||
<a href="/about" class="nav-link">About</a>
|
||||
<a href="#contact" class="nav-link">Contact</a>
|
||||
<div
|
||||
class="dropdown"
|
||||
:class="{ open: dropdownOpen }"
|
||||
>
|
||||
<Link href="/about" @click="dropdownOpen = false" class="nav-link">About</Link>
|
||||
<div class="dropdown" ref="dropdownRef" :class="{ open: dropdownOpen }">
|
||||
<button
|
||||
class="dropdown-toggle nav-link"
|
||||
@click="dropdownOpen = !dropdownOpen"
|
||||
@click.stop="dropdownOpen = !dropdownOpen"
|
||||
>
|
||||
Adventures
|
||||
<svg
|
||||
class="dropdown-icon"
|
||||
width="16" height="16" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="2"
|
||||
>
|
||||
<svg class="dropdown-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="6,9 12,15 18,9"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
@@ -47,36 +43,52 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, computed, ref } from 'vue';
|
||||
import { usePage} from "@inertiajs/vue3";
|
||||
import {GlobalProperties} from "@/types";
|
||||
import { usePage } from "@inertiajs/vue3";
|
||||
import { GlobalProperties } from "@/types";
|
||||
import { Link } from '@inertiajs/vue3';
|
||||
|
||||
const { continents_with_tours } = usePage().props as unknown as GlobalProperties
|
||||
const { continents_with_tours } = usePage().props as unknown as GlobalProperties;
|
||||
|
||||
const dropdownOpen = ref(false)
|
||||
const dropdownOpen = ref(false);
|
||||
const windowWidth = ref(window.innerWidth);
|
||||
const dropdownRef = ref<HTMLElement | null>(null);
|
||||
|
||||
|
||||
onMounted(() =>{
|
||||
initDropdown();
|
||||
initHeaderScroll()
|
||||
initMobileMenu()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
|
||||
const isDropdownVisible = computed(() => {
|
||||
return dropdownOpen.value || windowWidth.value < 768;
|
||||
});
|
||||
const isDropdownVisible = computed(() => dropdownOpen.value || windowWidth.value < 768);
|
||||
|
||||
const handleResize = () => {
|
||||
windowWidth.value = window.innerWidth;
|
||||
};
|
||||
|
||||
// Close when clicking outside (desktop only)
|
||||
const handleClickOutside = (e: Event) => {
|
||||
if (!dropdownRef.value) return;
|
||||
if (windowWidth.value < 768) return; // on mobile we show inline; skip closing here
|
||||
|
||||
// Support composedPath for shadow DOM correctness
|
||||
const path = (e as MouseEvent).composedPath?.();
|
||||
const clickedInside = path ? path.includes(dropdownRef.value) : dropdownRef.value.contains(e.target as Node);
|
||||
|
||||
if (!clickedInside) {
|
||||
dropdownOpen.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleEsc = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') dropdownOpen.value = false;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initMobileMenu(); // keep your mobile menu init
|
||||
window.addEventListener('resize', handleResize);
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
document.addEventListener('keydown', handleEsc);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
document.removeEventListener('keydown', handleEsc);
|
||||
});
|
||||
|
||||
// Mobile menu functionality
|
||||
const initMobileMenu = (): void => {
|
||||
@@ -85,50 +97,21 @@ const initMobileMenu = (): void => {
|
||||
|
||||
if (!mobileMenuBtn || !navMenu) return;
|
||||
|
||||
// Toggle menu open/close
|
||||
mobileMenuBtn.addEventListener('click', () => {
|
||||
navMenu.classList.toggle('open');
|
||||
mobileMenuBtn.classList.toggle('open');
|
||||
});
|
||||
};
|
||||
|
||||
// Header background on scroll
|
||||
const initHeaderScroll = (): void => {
|
||||
const header = document.querySelector('.header') as HTMLElement | null;
|
||||
if (!header) return;
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
if (window.scrollY > 100) {
|
||||
header.classList.add('scrolled');
|
||||
} else {
|
||||
header.classList.remove('scrolled');
|
||||
}
|
||||
// Close menu when a link is clicked
|
||||
navMenu.querySelectorAll('.nav-link').forEach(link => {
|
||||
link.addEventListener('click', () => {
|
||||
navMenu.classList.remove('open');
|
||||
mobileMenuBtn.classList.remove('open');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const initDropdown = (): void => {
|
||||
const dropdown = document.querySelector('.dropdown') as HTMLElement | null;
|
||||
const dropdownToggle = dropdown?.querySelector('.dropdown-toggle') as HTMLElement | null;
|
||||
|
||||
if (!dropdown || !dropdownToggle) return;
|
||||
|
||||
dropdownToggle.addEventListener('click', (e: Event) => {
|
||||
e.preventDefault();
|
||||
dropdown.classList.toggle('open');
|
||||
});
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
document.addEventListener('click', (e: Event) => {
|
||||
if (!dropdown.contains(e.target as Node)) {
|
||||
dropdown.classList.remove('open');
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
dropdown.classList.remove('open');
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -208,20 +191,9 @@ const initDropdown = (): void => {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 1em;
|
||||
/* padding / width handled in the desktop guard above */
|
||||
}
|
||||
|
||||
/* brand */
|
||||
.nav-brand {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
background: var(--gradient-adventure);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
/* links container (desktop default) */
|
||||
.nav-menu {
|
||||
display: flex;
|
||||
@@ -359,6 +331,8 @@ const initDropdown = (): void => {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.nav-menu.open {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
@@ -370,6 +344,20 @@ const initDropdown = (): void => {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.nav-brand {
|
||||
position:relative;
|
||||
padding: 1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 90dvw;
|
||||
}
|
||||
|
||||
.nav-brand img {
|
||||
display: block;
|
||||
margin-left: calc(10dvw / 2);
|
||||
}
|
||||
|
||||
/* Dropdown in mobile: behave as inline sublist */
|
||||
.dropdown {
|
||||
width: 100%;
|
||||
@@ -408,6 +396,7 @@ const initDropdown = (): void => {
|
||||
|
||||
.mobile-menu-btn {
|
||||
display: flex;
|
||||
width: 10dvw;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
139
resources/js/components/ProfileCard.vue
Normal file
139
resources/js/components/ProfileCard.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
name: String,
|
||||
photo: String,
|
||||
blurb: String,
|
||||
slide: String,
|
||||
position: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
photoSide: {
|
||||
type: String as () => 'left' | 'right',
|
||||
default: 'left'
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article v-hover="{ transform: 'translateY(-8px) rotate(0deg) scale(1.02)'}" class="profile-card" v-show-on-intersect="{type: 'slide-'+slide}">
|
||||
<div :class="['profile-flex', photoSide === 'right' ? 'reverse' : '']">
|
||||
<div class="profile-image">
|
||||
<img class="profile-photo" :src="photo" :alt="'Photo of ' + name" />
|
||||
</div>
|
||||
<div class="profile-body">
|
||||
<h3 class="profile-name">{{ name }}</h3>
|
||||
<p v-if="position" class="profile-position">{{ position }}</p>
|
||||
<p class="profile-blurb">{{ blurb }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.profile-card {
|
||||
cursor: pointer;
|
||||
background: var(--card);
|
||||
border-radius: 0;
|
||||
clip-path: polygon(0 0, calc(100% - 20px) 0, 100% 20px, 100% 100%, 20px 100%, 0 calc(100% - 20px));
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.profile-card img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 1.5s ease;
|
||||
}
|
||||
|
||||
.profile-card:hover img {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.profile-flex {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.profile-flex.reverse {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.profile-flex {
|
||||
flex-direction: row;
|
||||
align-items: flex-start; /* align top for image and text */
|
||||
}
|
||||
|
||||
.profile-flex.reverse {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.profile-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Always photo above text on mobile */
|
||||
.profile-flex,
|
||||
.profile-flex.reverse {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Make the text take remaining space */
|
||||
.profile-body {
|
||||
padding: 0.5rem;
|
||||
flex: 1 1 0; /* allow body to grow */
|
||||
}
|
||||
|
||||
.profile-image {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 320px; /* similar to your old grid column */
|
||||
aspect-ratio: 4 / 3;
|
||||
background: var(--muted);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
clip-path: polygon(
|
||||
50% 0%, 50% 0%,
|
||||
100% 25%, 100% 75%,
|
||||
50% 100%, 50% 100%,
|
||||
0% 75%, 0% 25%
|
||||
);
|
||||
}
|
||||
|
||||
.profile-photo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.profile-name {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
background: var(--gradient-adventure);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.profile-position {
|
||||
color: var(--muted-foreground);
|
||||
font-size: 0.95rem;
|
||||
letter-spacing: 0.2px;
|
||||
margin-bottom: 0.75rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.profile-blurb {
|
||||
color: var(--muted-foreground);
|
||||
font-size: 1.0625rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
</style>
|
||||
79
resources/js/components/dredgy/ScrollIndicator.vue
Normal file
79
resources/js/components/dredgy/ScrollIndicator.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<!-- wrapper controls opacity so animation on the inner element is unaffected -->
|
||||
<div class="scroll-indicator-fade" :style="{ opacity: indicatorOpacity }">
|
||||
<div class="scroll-indicator">
|
||||
<div class="scroll-mouse">
|
||||
<div class="scroll-dot"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const indicatorOpacity = ref(1)
|
||||
|
||||
function handleScroll() {
|
||||
const maxScroll = 200
|
||||
const scrolled = window.scrollY
|
||||
indicatorOpacity.value = Math.max(0, 1 - scrolled / maxScroll)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* wrapper: centering + fade transition */
|
||||
.scroll-indicator-fade {
|
||||
position: absolute;
|
||||
bottom: 2rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%); /* keep horizontal centering here */
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none; /* prevents accidental clicks while visible */
|
||||
}
|
||||
|
||||
/* inner element has the bounce animation — animate translateY only */
|
||||
.scroll-indicator {
|
||||
animation: bounce 2s infinite;
|
||||
/* no translateX here; wrapper handles the horizontal offset */
|
||||
}
|
||||
|
||||
.scroll-mouse {
|
||||
width: 24px;
|
||||
height: 40px;
|
||||
border: 2px solid var(--foreground);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.scroll-dot {
|
||||
width: 4px;
|
||||
height: 12px;
|
||||
background: var(--foreground);
|
||||
border-radius: 2px;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
/* bounce now only moves Y — no translateX so it won't conflict */
|
||||
@keyframes bounce {
|
||||
0%, 20%, 53%, 80%, 100% { transform: translateY(0); }
|
||||
40%, 43% { transform: translateY(-15px); }
|
||||
70% { transform: translateY(-7px); }
|
||||
90% { transform: translateY(-2px); }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
</style>
|
||||
@@ -38,7 +38,6 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import GradientText from "@/components/dredgy/GradientText.vue";
|
||||
import { defineProps } from 'vue'
|
||||
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -9,11 +9,13 @@ const formatPrice = (price: number) => {
|
||||
|
||||
const props = defineProps<{
|
||||
tour: Tour
|
||||
dataIndex: number
|
||||
}>()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tour-card" data-index="0">
|
||||
<div v-hover="{ transform: 'translateY(-8px) rotate(0deg) scale(1.02)'}" v-show-on-intersect="dataIndex % 2 == 0 ? 'rotate-left' : 'rotate-right'" class="tour-card" :data-index="dataIndex">
|
||||
<div class="tour-image">
|
||||
<img :src="`/img/tours/${tour.internal_name}.jpg`" alt="">
|
||||
<div class="tour-overlay"></div>
|
||||
@@ -67,18 +69,8 @@ const props = defineProps<{
|
||||
transform-origin: center bottom;
|
||||
}
|
||||
|
||||
.tour-card:nth-child(2n) {
|
||||
transform: translateY(100px) rotate(6deg) scale(0.9);
|
||||
}
|
||||
|
||||
.tour-card.visible {
|
||||
transform: translateY(0) rotate(0deg) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tour-card:hover {
|
||||
box-shadow: var(--shadow-intense);
|
||||
transform: translateY(-8px) rotate(0deg) scale(1.02);
|
||||
}
|
||||
|
||||
.tour-image {
|
||||
@@ -187,47 +179,4 @@ const props = defineProps<{
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes bounce {
|
||||
0%, 20%, 53%, 80%, 100% {
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
40%, 43% {
|
||||
transform: translateX(-50%) translateY(-15px);
|
||||
}
|
||||
70% {
|
||||
transform: translateX(-50%) translateY(-7px);
|
||||
}
|
||||
90% {
|
||||
transform: translateX(-50%) translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade {
|
||||
animation: fadeInUp 1s ease-out;
|
||||
}
|
||||
|
||||
.animate-slide {
|
||||
animation: fadeInUp 1s ease-out 0.2s both;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,32 +7,39 @@
|
||||
</div>
|
||||
|
||||
<SectionContainer>
|
||||
<div class="about-content">
|
||||
<SectionTitle title="Beyond" gradient="Borders" />
|
||||
|
||||
<div class="about-grid">
|
||||
<div class="about-text">
|
||||
<p>
|
||||
We've been travelling the world our whole lives. From North Korea to Afghanistan, Comoros to Venezuela, Suriname to Uruguay. We know the world, and we want to show it to you.
|
||||
</p>
|
||||
<p>
|
||||
Dr Edgy offers immersive, authentic small group and private adventures, leveraging an extensive global network forged through decades of adventure travel.
|
||||
</p>
|
||||
<p>
|
||||
Our main focus for now is China, where we offer deep-dive adventures of individual provinces and subjects-of-interest and going to places that most other tours completely bypass.
|
||||
We can arrange private adventures in many other countries on request.
|
||||
</p>
|
||||
<div
|
||||
v-show-on-intersect="'fade-up'"
|
||||
class="about-content"
|
||||
>
|
||||
<div>
|
||||
<SectionTitle title="Beyond" gradient="Borders" />
|
||||
|
||||
<div class="about-grid">
|
||||
<div class="about-text" v-show-on-intersect="{type: 'slide-right'}">
|
||||
<p>
|
||||
We've been travelling the world our whole lives. From North Korea to Afghanistan, Comoros to Venezuela, Suriname to Uruguay. We know the world, and we want to show it to you.
|
||||
</p>
|
||||
<p>
|
||||
Dr Edgy offers immersive, authentic small group and private adventures, leveraging an extensive global network forged through decades of adventure travel.
|
||||
</p>
|
||||
<p>
|
||||
Our main focus for now is China, where we offer deep-dive adventures of individual provinces and subjects-of-interest and going to places that most other tours completely bypass.
|
||||
We can arrange private adventures in many other countries on request.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="about-cta" v-show-on-intersect="{type:'slide-left'}">
|
||||
<h3>Who Are We?</h3>
|
||||
<p>Dr Edgy was founded by two close friends who met travelling in North Korea. Both of us love adventure travel with a bit of luxury. One of us is even an actual doctor.</p>
|
||||
<p>You are in safe hands.</p>
|
||||
<EdgyButton classes="btn-primary">
|
||||
<Link href="/about">
|
||||
GET TO KNOW US
|
||||
</Link>
|
||||
</EdgyButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="about-cta">
|
||||
<h3>Who Are We?</h3>
|
||||
<p>Dr Edgy was founded by two close friends who met travelling in North Korea. Both of us love adventure travel with a bit of luxury. One of us is even an actual doctor.</p>
|
||||
<p> You are in safe hands.</p>
|
||||
<EdgyButton classes="btn-primary">GET TO KNOW US</EdgyButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SectionContainer>
|
||||
</section>
|
||||
@@ -83,17 +90,6 @@
|
||||
background: var(--gradient-adventure);
|
||||
}
|
||||
|
||||
.about-content {
|
||||
opacity: 0;
|
||||
transform: translateY(50px);
|
||||
transition: all 1s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.about-content.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.about-grid {
|
||||
display: grid;
|
||||
gap: 3rem;
|
||||
@@ -113,33 +109,6 @@
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 2rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.stat {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
background: var(--gradient-adventure);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--muted-foreground);
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.about-cta {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
@@ -176,34 +145,15 @@
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import { ref } from 'vue'
|
||||
import EdgyButton from "@/components/dredgy/EdgyButton.vue";
|
||||
import SectionContainer from "@/components/dredgy/SectionContainer.vue";
|
||||
import SectionTitle from "@/components/dredgy/SectionTitle.vue";
|
||||
import {createIntersectionObserver} from "@/composables/useIntersectionObserver";
|
||||
import {onMounted} from "vue";
|
||||
import {Link} from "@inertiajs/vue3";
|
||||
|
||||
onMounted(() => {
|
||||
initAboutAnimation()
|
||||
})
|
||||
|
||||
// About section animation
|
||||
const initAboutAnimation = (): void => {
|
||||
const aboutContent = document.querySelector('.about-content') as HTMLElement | null;
|
||||
if (!aboutContent) return;
|
||||
|
||||
const observer = createIntersectionObserver((entries: IntersectionObserverEntry[]) => {
|
||||
entries.forEach((entry: IntersectionObserverEntry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('visible');
|
||||
} else {
|
||||
entry.target.classList.remove('visible');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(aboutContent);
|
||||
};
|
||||
const isVisible = ref(false)
|
||||
|
||||
const onIntersect = (isIntersecting: boolean, entries: IntersectionObserverEntry[]) => {
|
||||
isVisible.value = isIntersecting
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -6,28 +6,33 @@
|
||||
</div>
|
||||
|
||||
<SectionContainer>
|
||||
<div class="tours-header">
|
||||
<div
|
||||
v-show-on-intersect="{ type: 'fade-up', duration: '1s' }"
|
||||
class="tours-header"
|
||||
>
|
||||
<SectionTitle title="Featured" gradient="Adventures" subtitle="Discover somewhere new, in a new way" />
|
||||
</div>
|
||||
|
||||
<div class="tours-grid">
|
||||
<TourCard
|
||||
v-for="(tour, index) in featuredTours"
|
||||
:key="tour.id ?? index"
|
||||
:key="index"
|
||||
class="tour-card"
|
||||
:data-index="index"
|
||||
:tour="tour"
|
||||
/>
|
||||
</div>
|
||||
<br/>
|
||||
<EdgyButton classes="btn-primary btn-full">Explore All Adventures</EdgyButton>
|
||||
|
||||
<EdgyButton
|
||||
v-show-on-intersect="{ type: 'fade-up', duration: '0.6s' }"
|
||||
classes="btn-primary btn-full"
|
||||
>
|
||||
Explore All Adventures
|
||||
</EdgyButton>
|
||||
</SectionContainer>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<style scoped>
|
||||
/* Featured Tours Section */
|
||||
.featured-tours {
|
||||
@@ -69,19 +74,12 @@
|
||||
.tours-header {
|
||||
text-align: center;
|
||||
margin-bottom: 4rem;
|
||||
opacity: 0;
|
||||
transform: translateY(50px);
|
||||
transition: all 1s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.tours-header.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.tours-grid {
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@@ -95,85 +93,27 @@
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* Hover effects for tour cards */
|
||||
.tour-card {
|
||||
transition: transform 0.3s ease;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tour-card:hover {
|
||||
transform: translateY(-8px) scale(1.02);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SectionContainer from "@/components/dredgy/SectionContainer.vue";
|
||||
import SectionTitle from "@/components/dredgy/SectionTitle.vue";
|
||||
import TourCard from "@/components/dredgy/TourCard.vue";
|
||||
import { Tour } from "@/types";
|
||||
import EdgyButton from "@/components/dredgy/EdgyButton.vue";
|
||||
import { Tour } from "@/types";
|
||||
|
||||
|
||||
import { defineProps, onMounted } from 'vue'
|
||||
import {createIntersectionObserver} from "@/composables/useIntersectionObserver";
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
featuredTours: Tour[]
|
||||
}>()
|
||||
|
||||
onMounted(() => {
|
||||
initToursAnimation()
|
||||
initTourCardInteractions()
|
||||
})
|
||||
|
||||
const initTourCardInteractions = (): void => {
|
||||
const tourCards = document.querySelectorAll('.tour-card') as NodeListOf<HTMLElement>;
|
||||
|
||||
tourCards.forEach((card: HTMLElement) => {
|
||||
// Add hover effects
|
||||
card.addEventListener('mouseenter', () => {
|
||||
card.style.transform = 'translateY(-8px) scale(1.02)';
|
||||
});
|
||||
|
||||
card.addEventListener('mouseleave', () => {
|
||||
if (card.classList.contains('visible')) {
|
||||
card.style.transform = 'translateY(0) rotate(0deg) scale(1)';
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const initToursAnimation = (): void => {
|
||||
const toursHeader = document.querySelector('.tours-header') as HTMLElement | null;
|
||||
const tourCards = document.querySelectorAll('.tour-card') as NodeListOf<HTMLElement>;
|
||||
|
||||
if (!toursHeader || !tourCards.length) return;
|
||||
|
||||
// Header animation
|
||||
const headerObserver = createIntersectionObserver((entries: IntersectionObserverEntry[]) => {
|
||||
entries.forEach((entry: IntersectionObserverEntry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('visible');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
headerObserver.observe(toursHeader);
|
||||
|
||||
// Tour cards animation with staggered effect
|
||||
const cardsObserver = createIntersectionObserver((entries: IntersectionObserverEntry[]) => {
|
||||
entries.forEach((entry: IntersectionObserverEntry) => {
|
||||
const tourCard = entry.target as HTMLElement;
|
||||
const index = parseInt(tourCard.dataset.index || '0');
|
||||
|
||||
if (entry.isIntersecting) {
|
||||
// Staggered animation when scrolling down
|
||||
setTimeout(() => {
|
||||
tourCard.classList.add('visible');
|
||||
}, index * 150);
|
||||
} else {
|
||||
// Reverse staggered animation when scrolling up
|
||||
const reverseIndex = (tourCards.length - 1) - index;
|
||||
setTimeout(() => {
|
||||
tourCard.classList.remove('visible');
|
||||
}, reverseIndex * 100);
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.2 });
|
||||
|
||||
tourCards.forEach((card: HTMLElement) => {
|
||||
cardsObserver.observe(card);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,183 +1,126 @@
|
||||
<template>
|
||||
<section class="hero">
|
||||
<div class="hero-bg"></div>
|
||||
<div class="hero-overlay"></div>
|
||||
<section class="hero">
|
||||
<div class="hero-bg"></div>
|
||||
<div class="hero-overlay"></div>
|
||||
|
||||
<div class="hero-content">
|
||||
<h1 class="hero-title animate-fade">
|
||||
<GradientText>Travel.</GradientText><br>
|
||||
<span>On the Edge</span>
|
||||
</h1>
|
||||
<div class="hero-content">
|
||||
<h1 class="hero-title animate-fade">
|
||||
<GradientText>Travel.</GradientText><br>
|
||||
<span>On the Edge</span>
|
||||
</h1>
|
||||
|
||||
<p class="hero-subtitle animate-slide">
|
||||
We don't go wherever is trendy. <br/>
|
||||
We set the trends.
|
||||
</p>
|
||||
<div class="hero-buttons">
|
||||
<EdgyButton classes="btn-secondary">View Adventures</EdgyButton>
|
||||
</div>
|
||||
<p class="hero-subtitle animate-slide">
|
||||
We don't go wherever is trendy. <br/>
|
||||
We set the trends.
|
||||
</p>
|
||||
<div class="hero-buttons">
|
||||
<EdgyButton classes="btn-secondary">View Adventures</EdgyButton>
|
||||
</div>
|
||||
|
||||
<div class="scroll-indicator">
|
||||
<div class="scroll-mouse">
|
||||
<div class="scroll-dot"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
<ScrollIndicator />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Hero Section */
|
||||
.hero {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 120%;
|
||||
background-image: url('/img/hero.jpg');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
will-change: transform;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 120%;
|
||||
background-image: url('/img/hero.jpg');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.hero-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 120%;
|
||||
background: var(--gradient-hero);
|
||||
opacity: 0.8;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 120%;
|
||||
background: var(--gradient-hero);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
text-align: center;
|
||||
max-width: 1000px;
|
||||
padding: 0 2rem;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
text-align: center;
|
||||
max-width: 1000px;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: clamp(3rem, 8vw, 6rem);
|
||||
font-weight: bold;
|
||||
margin-bottom: 1.5rem;
|
||||
line-height: 1.1;
|
||||
font-size: clamp(3rem, 8vw, 6rem);
|
||||
font-weight: bold;
|
||||
margin-bottom: 1.5rem;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: clamp(1.125rem, 2.5vw, 1.5rem);
|
||||
color: var(--muted-foreground);
|
||||
margin-bottom: 2rem;
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
font-size: clamp(1.125rem, 2.5vw, 1.5rem);
|
||||
color: var(--muted-foreground);
|
||||
margin-bottom: 2rem;
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.hero-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero-title,
|
||||
.hero-subtitle {
|
||||
opacity: 0;
|
||||
transform: translateY(18px);
|
||||
transition: opacity 1000ms cubic-bezier(.22,.9,.32,1),
|
||||
transform 1000ms cubic-bezier(.22,.9,.32,1);
|
||||
will-change: opacity, transform;
|
||||
opacity: 0;
|
||||
transform: translateY(18px);
|
||||
transition: opacity 1000ms cubic-bezier(.22,.9,.32,1),
|
||||
transform 1000ms cubic-bezier(.22,.9,.32,1);
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
/* visible state toggled by JS */
|
||||
.hero-title.is-visible,
|
||||
.hero-subtitle.is-visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.hero-buttons {
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
.hero-buttons {
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
|
||||
.hero-content {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
.hero-content {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes bounce {
|
||||
0%, 20%, 53%, 80%, 100% {
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
40%, 43% {
|
||||
transform: translateX(-50%) translateY(-15px);
|
||||
}
|
||||
70% {
|
||||
transform: translateX(-50%) translateY(-7px);
|
||||
}
|
||||
90% {
|
||||
transform: translateX(-50%) translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-indicator {
|
||||
position: absolute;
|
||||
bottom: 2rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
animation: bounce 2s infinite;
|
||||
}
|
||||
|
||||
.scroll-mouse {
|
||||
width: 24px;
|
||||
height: 40px;
|
||||
border: 2px solid var(--foreground);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.scroll-dot {
|
||||
width: 4px;
|
||||
height: 12px;
|
||||
background: var(--foreground);
|
||||
border-radius: 2px;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import EdgyButton from "@/components/dredgy/EdgyButton.vue";
|
||||
import GradientText from "@/components/dredgy/GradientText.vue";
|
||||
import {onMounted} from "vue";
|
||||
import { onMounted } from "vue";
|
||||
import ScrollIndicator from "@/components/dredgy/ScrollIndicator.vue";
|
||||
|
||||
const initParallax = (): void => {
|
||||
const heroBackground = document.querySelector('.hero-bg') as HTMLElement | null;
|
||||
|
||||
Reference in New Issue
Block a user