11 Commits

Author SHA1 Message Date
c80209ba84 Merge version_4_1779657195832 into main
Merge version_4_1779657195832 into main
2026-05-24 21:13:30 +00:00
8600389e44 Update src/pages/HomePage.tsx 2026-05-24 21:13:27 +00:00
20350b9fd9 Update src/components/ui/LightRaysCornerBackground.tsx 2026-05-24 21:13:25 +00:00
88511021a3 Update src/components/templates/ResultsComparison.tsx 2026-05-24 21:13:23 +00:00
0933ab8163 Update src/components/sections/hero/HeroBrandCarousel.tsx 2026-05-24 21:13:21 +00:00
cfa78e08fa Update src/components/sections/hero/HeroBillboardCarousel.tsx 2026-05-24 21:13:20 +00:00
dc0d96edef Update src/components/sections/footer/FooterSimpleMedia.tsx 2026-05-24 21:13:20 +00:00
22a6b49fc3 Update src/components/sections/blog/BlogArticle.tsx 2026-05-24 21:13:18 +00:00
e908e7a1a1 Merge version_3_1779657104982 into main
Merge version_3_1779657104982 into main
2026-05-24 21:11:55 +00:00
fbe16f482d Update src/pages/HomePage.tsx 2026-05-24 21:11:52 +00:00
f2379d633a Merge version_2_1779657069613 into main
Merge version_2_1779657069613 into main
2026-05-24 21:11:21 +00:00
7 changed files with 3 additions and 540 deletions

View File

@@ -1,94 +0,0 @@
import ScrollReveal from "@/components/ui/ScrollReveal";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
type BlogArticleProps = {
category: string;
title: string;
excerpt?: string;
content: string;
imageSrc: string;
authorName: string;
authorImageSrc: string;
date: string;
readingTime?: string;
backButton?: { text: string; href: string };
};
const BlogArticle = ({
category,
title,
excerpt,
content,
imageSrc,
authorName,
authorImageSrc,
date,
readingTime,
backButton = { text: "Back to Blog", href: "/blog" },
}: BlogArticleProps) => {
return (
<article aria-label="Blog article" className="py-20">
<div className="flex flex-col gap-10">
<ScrollReveal variant="fade-blur">
<div className="flex flex-col gap-6 w-content-width md:max-w-4xl mx-auto">
<div className="flex items-center gap-2 px-3 py-1 text-sm text-foreground/75 card rounded w-fit">
<a
href={backButton.href}
className="hover:text-foreground transition-colors"
>
{backButton.text}
</a>
<span>/</span>
<span className="text-foreground">{category}</span>
</div>
<div className="flex flex-col gap-3">
<h1 className="text-5xl md:text-6xl font-medium leading-tight text-balance">
{title}
</h1>
{excerpt && (
<p className="text-lg md:text-xl leading-tight text-balance">
{excerpt}
</p>
)}
<div className="flex items-center gap-3 mt-3">
<ImageOrVideo
imageSrc={authorImageSrc}
className="size-9 rounded-full object-cover"
/>
<div className="flex flex-col">
<span className="text-sm font-medium">{authorName}</span>
<span className="text-xs text-foreground/75">
{date}
{readingTime && ` · ${readingTime}`}
</span>
</div>
</div>
</div>
</div>
</ScrollReveal>
<ScrollReveal variant="fade-blur">
<div className="w-content-width md:max-w-4xl mx-auto aspect-video card rounded overflow-hidden p-3 xl:p-4 2xl:p-5">
<ImageOrVideo
imageSrc={imageSrc}
className="size-full object-cover"
/>
</div>
</ScrollReveal>
<ScrollReveal variant="fade-blur">
<div
className="w-content-width md:max-w-4xl mx-auto flex flex-col gap-6 [&>h1]:text-4xl [&>h1]:font-semibold [&>h1]:mt-4 [&>h2]:text-3xl [&>h2]:font-semibold [&>h2]:mt-4 [&>h3]:text-2xl [&>h3]:font-semibold [&>h3]:mt-2 [&>h4]:text-xl [&>h4]:font-semibold [&>h4]:mt-2 [&>p]:text-base [&>p]:leading-relaxed [&>p]:text-foreground/85 [&_strong]:font-semibold [&_em]:italic [&_u]:underline [&>ul]:flex [&>ul]:flex-col [&>ul]:gap-2 [&>ul]:list-disc [&>ul]:pl-6 [&>ul]:text-base [&>ul]:leading-relaxed [&>ul]:text-foreground/85 [&>ol]:flex [&>ol]:flex-col [&>ol]:gap-2 [&>ol]:list-decimal [&>ol]:pl-6 [&>ol]:text-base [&>ol]:leading-relaxed [&>ol]:text-foreground/85"
dangerouslySetInnerHTML={{ __html: content }}
/>
</ScrollReveal>
</div>
</article>
);
};
export default BlogArticle;

View File

@@ -1,95 +0,0 @@
import { useButtonClick } from "@/hooks/useButtonClick";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
type FooterLink = {
label: string;
href?: string;
onClick?: () => void;
};
type FooterColumn = {
title: string;
items: FooterLink[];
};
const FooterLinkItem = ({ label, href, onClick }: FooterLink) => {
const handleClick = useButtonClick(href, onClick);
return (
<button
onClick={handleClick}
className="text-base text-primary-cta-text hover:opacity-75 transition-opacity cursor-pointer"
>
{label}
</button>
);
};
const FooterBottomLink = ({ label, href, onClick }: FooterLink) => {
const handleClick = useButtonClick(href, onClick);
return (
<button
onClick={handleClick}
className="text-sm opacity-50 hover:opacity-75 transition-opacity cursor-pointer"
>
{label}
</button>
);
};
const FooterSimpleMedia = ({
imageSrc,
videoSrc,
brand,
columns,
copyright,
links,
}: ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never }) & {
brand: string;
columns: FooterColumn[];
copyright: string;
links: FooterLink[];
}) => {
return (
<footer aria-label="Site footer" className="relative w-full mt-20 overflow-hidden">
<div className="w-full aspect-square md:aspect-16/6 mask-fade-top-long">
<ImageOrVideo
imageSrc={imageSrc}
videoSrc={videoSrc}
className="w-full h-full object-cover rounded-none!"
/>
</div>
<div className="w-full py-15 primary-button text-primary-cta-text">
<div className="w-content-width mx-auto">
<div className="flex flex-col md:flex-row gap-10 md:gap-0 justify-between items-start mb-10">
<h2 className="text-4xl font-medium">{brand}</h2>
<div className="w-full md:w-fit flex flex-wrap gap-y-10 md:gap-12">
{columns.map((column) => (
<div key={column.title} className="w-1/2 md:w-auto flex flex-col items-start gap-3">
<h3 className="text-sm opacity-50">{column.title}</h3>
{column.items.map((item) => (
<FooterLinkItem key={item.label} label={item.label} href={item.href} onClick={item.onClick} />
))}
</div>
))}
</div>
</div>
<div className="flex items-center justify-between pt-8 border-t border-primary-cta-text/20">
<span className="text-sm opacity-50">{copyright}</span>
<div className="flex items-center gap-3">
{links.map((link) => (
<FooterBottomLink key={link.label} label={link.label} href={link.href} onClick={link.onClick} />
))}
</div>
</div>
</div>
</div>
</footer>
);
};
export default FooterSimpleMedia;

View File

@@ -1,75 +0,0 @@
import Button from "@/components/ui/Button";
import HeroBackgroundSlot from "@/components/ui/HeroBackgroundSlot";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
type HeroBillboardCarouselProps = {
tag: string;
title: string;
description: string;
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
items: ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never })[];
};
const HeroBillboardCarousel = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: HeroBillboardCarouselProps) => {
const duplicated = [...items, ...items, ...items, ...items];
return (
<section
aria-label="Hero section"
className="relative flex flex-col items-center justify-center gap-8 w-full min-h-svh py-25"
>
<HeroBackgroundSlot />
<div className="flex flex-col items-center gap-2 w-content-width mx-auto text-center">
<div className="px-3 py-1 mb-1 text-sm card rounded w-fit">
<p>{tag}</p>
</div>
<TextAnimation
text={title}
variant="fade-blur"
gradientText={true}
tag="h1"
className="text-6xl font-medium text-balance"
/>
<TextAnimation
text={description}
variant="fade-blur"
gradientText={false}
tag="p"
className="text-base md:text-lg leading-tight text-balance"
/>
<div className="flex flex-wrap justify-center gap-3 mt-3">
<Button text={primaryButton.text} href={primaryButton.href} variant="primary"/>
<Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary"animationDelay={0.1} />
</div>
</div>
<div className="w-content-width mx-auto overflow-hidden mask-fade-x">
<div className="flex w-max animate-marquee-horizontal" style={{ animationDuration: "60s" }}>
{duplicated.map((item, i) => (
<div key={i} className="shrink-0 w-60 md:w-75 2xl:w-80 aspect-4/5 mr-3 md:mr-5 p-1.5 card rounded-lg overflow-hidden">
<ImageOrVideo
imageSrc={item.imageSrc}
videoSrc={item.videoSrc}
className="w-full h-full rounded-lg object-cover"
/>
</div>
))}
</div>
</div>
</section>
);
};
export default HeroBillboardCarousel;

View File

@@ -1,109 +0,0 @@
import { useEffect, useState } from "react";
import Button from "@/components/ui/Button";
import HeroBackgroundSlot from "@/components/ui/HeroBackgroundSlot";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
import AutoFillText from "@/components/ui/AutoFillText";
import { cls } from "@/lib/utils";
type HeroBrandCarouselProps = {
brand: string;
description: string;
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
items: ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never })[];
};
const INTERVAL = 4000;
const HeroBrandCarousel = ({
brand,
description,
primaryButton,
secondaryButton,
items,
}: HeroBrandCarouselProps) => {
const [currentIndex, setCurrentIndex] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % items.length);
}, INTERVAL);
return () => clearInterval(interval);
}, [currentIndex, items.length]);
return (
<section
aria-label="Hero section"
className="relative w-full h-svh overflow-hidden flex flex-col justify-end mb-20"
>
<HeroBackgroundSlot />
{items.map((item, index) => (
<div
key={index}
className={cls(
"absolute inset-0 transition-opacity duration-500",
currentIndex === index ? "opacity-100 z-1" : "opacity-0 pointer-events-none"
)}
aria-hidden={currentIndex !== index}
>
<ImageOrVideo
imageSrc={item.imageSrc}
videoSrc={item.videoSrc}
className="absolute inset-0 w-full h-full object-cover rounded-none"
/>
</div>
))}
<div
className="absolute z-10 w-full h-[50svh] md:h-[75svh] left-0 bottom-0 backdrop-blur-xl mask-[linear-gradient(to_bottom,transparent,black_60%)]"
aria-hidden="true"
/>
<div className="relative z-10 w-content-width mx-auto pb-5">
<div className="flex flex-col">
<div className="w-full flex flex-col md:flex-row md:justify-between items-start md:items-end gap-3 md:gap-5">
<TextAnimation
text={description}
variant="fade"
gradientText={false}
tag="p"
className="w-full md:w-1/2 text-lg md:text-2xl text-balance font-normal text-primary-cta-text leading-tight"
/>
<div className="w-full md:w-1/2 flex justify-start md:justify-end">
<div className="flex flex-wrap gap-3">
<Button text={primaryButton.text} href={primaryButton.href} variant="primary"/>
<Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary"animationDelay={0.1} />
</div>
</div>
</div>
<AutoFillText className="font-semibold text-primary-cta-text">{brand}</AutoFillText>
<div className="flex gap-3 pb-5">
{items.map((_, index) => (
<button
key={index}
className="relative h-1 w-full rounded overflow-hidden bg-primary-cta-text/20 cursor-pointer"
onClick={() => setCurrentIndex(index)}
aria-label="Slide"
aria-current={currentIndex === index}
>
<div
className={cls(
"absolute inset-0 bg-primary-cta-text rounded origin-left",
currentIndex === index ? "animate-progress" : (index < currentIndex ? "scale-x-100" : "scale-x-0")
)}
style={{ animationDuration: `${INTERVAL}ms` }}
/>
</button>
))}
</div>
</div>
</div>
</section>
);
};
export default HeroBrandCarousel;

View File

@@ -1,110 +0,0 @@
import Button from "@/components/ui/Button";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
import ScrollReveal from "@/components/ui/ScrollReveal";
import { cls } from "@/lib/utils";
type ResultItem = {
treatment: string;
detail: string;
beforeSrc: string;
afterSrc: string;
};
interface ResultsComparisonProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: ResultItem[];
}
const ImageLabel = ({ text, side }: { text: string; side: "left" | "right" }) => (
<div
className={cls(
"absolute bottom-2 xl:bottom-3 2xl:bottom-4 px-3 py-1 w-fit text-sm card rounded",
side === "left" ? "left-2 xl:left-3 2xl:left-4" : "right-2 xl:right-3 2xl:right-4"
)}
>
<p className="font-medium text-foreground">{text}</p>
</div>
);
const ResultsComparison = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: ResultsComparisonProps) => {
const duplicated = [...items, ...items, ...items, ...items];
return (
<section aria-label="Results section" className="py-20">
<div className="flex flex-col gap-8">
<div className="flex flex-col items-center w-content-width mx-auto gap-2">
<div className="px-3 py-1 mb-1 text-sm card rounded w-fit">
<p>{tag}</p>
</div>
<TextAnimation
text={title}
variant="slide-up"
gradientText={true}
tag="h2"
className="text-6xl font-medium text-center text-balance"
/>
<TextAnimation
text={description}
variant="slide-up"
gradientText={false}
tag="p"
className="md:max-w-6/10 text-lg leading-tight text-center"
/>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-3 mt-3">
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary"/>}
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animationDelay={0.1} />}
</div>
)}
</div>
<ScrollReveal variant="slide-up">
<div className="w-content-width mx-auto overflow-hidden mask-fade-x">
<div className="flex w-max animate-marquee-horizontal" style={{ animationDuration: "60s" }}>
{duplicated.map((item, i) => (
<div key={i} className="shrink-0 w-80 md:w-120 2xl:w-140 mr-3 md:mr-5 p-3 xl:p-4 2xl:p-5 card rounded">
<div className="relative flex w-full aspect-3/2 mb-3 xl:mb-4 2xl:mb-5">
<div className="relative overflow-hidden w-1/2 rounded-l-lg rounded-r-none">
<ImageOrVideo imageSrc={item.beforeSrc} className="absolute inset-0 object-cover w-full h-full rounded-l rounded-r-none" />
<ImageLabel text="Before" side="left" />
</div>
<div className="absolute z-10 left-1/2 top-0 bottom-0 w-0.5 bg-background -translate-x-1/2" />
<div className="relative overflow-hidden w-1/2 rounded-r-lg rounded-l-none">
<ImageOrVideo imageSrc={item.afterSrc} className="absolute inset-0 object-cover w-full h-full rounded-r rounded-l-none" />
<ImageLabel text="After" side="right" />
</div>
</div>
<div className="flex flex-col gap-1">
<h4 className="truncate text-2xl font-medium leading-tight text-foreground">
{item.treatment}
</h4>
<p className="text-base leading-tight text-foreground/75">
{item.detail}
</p>
</div>
</div>
))}
</div>
</div>
</ScrollReveal>
</div>
</section>
);
};
export default ResultsComparison;

View File

@@ -1,54 +0,0 @@
import { cls } from "@/lib/utils";
type LightRaysCornerBackgroundProps = {
position: "fixed" | "absolute";
};
const LightRaysCornerBackground = ({ position }: LightRaysCornerBackgroundProps) => {
return (
<div className={cls(position, "inset-0 -z-10 overflow-hidden pointer-events-none select-none")} aria-hidden="true">
<div className="absolute inset-0 bg-background mask-[radial-gradient(50%_50%_at_50%_0%,white_0%,transparent_100%)] bg-[linear-gradient(to_right,color-mix(in_srgb,var(--color-background-accent)_20%,transparent)_1px,transparent_1px),linear-gradient(to_bottom,color-mix(in_srgb,var(--color-background-accent)_10%,transparent)_1px,transparent_1px)] bg-size-[10vw_10vw]" />
<div className="absolute -top-[571px] -left-[373px] w-[1142px] h-[179vh] -rotate-[38deg] overflow-hidden blur-lg mask-[radial-gradient(50%_109%,#000_0%,#000000f6_0%,transparent_96%)]">
<div
className="absolute -top-[352px] -bottom-[920px] left-[calc(50%-17.5px)] w-[35px] origin-top-right overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]"
style={{ "--ray-opacity": 0.85, transform: "rotate(-18deg)", animation: "rotated-ray-pulse 4s ease-in-out 0s infinite both" } as React.CSSProperties}
/>
<div
className="absolute -top-[352px] -bottom-[920px] left-[calc(50%-17.5px)] w-[35px] origin-top-right overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]"
style={{ "--ray-opacity": 0.775, transform: "rotate(-12deg)", animation: "rotated-ray-pulse 3.5s ease-in-out 0.5s infinite both" } as React.CSSProperties}
/>
<div
className="absolute -top-[352px] -bottom-[920px] left-[calc(50%-10px)] w-[20px] origin-top-right overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]"
style={{ "--ray-opacity": 0.65, transform: "scale(0.9) rotate(-5deg)", animation: "rotated-ray-pulse 5s ease-in-out 1.2s infinite both" } as React.CSSProperties}
/>
<div
className="absolute -top-[352px] -bottom-[920px] left-[calc(50%-7.5px)] w-[15px] origin-top-right overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]"
style={{ "--ray-opacity": 0.25, transform: "rotate(-3deg)", animation: "rotated-ray-pulse 3s ease-in-out 0.3s infinite both" } as React.CSSProperties}
/>
<div
className="absolute -top-[352px] -bottom-[920px] left-[calc(50%-20px)] w-[40px] origin-top-right overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]"
style={{ "--ray-opacity": 0.45, transform: "scale(0.79) rotate(0deg)", animation: "rotated-ray-pulse 4.5s ease-in-out 0.8s infinite both" } as React.CSSProperties}
/>
<div
className="absolute -top-[352px] -bottom-[920px] left-[calc(50%-10px)] w-[20px] origin-top-right overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]"
style={{ "--ray-opacity": 0.45, transform: "rotate(6deg)", animation: "rotated-ray-pulse 3.2s ease-in-out 1.5s infinite both" } as React.CSSProperties}
/>
<div
className="absolute -top-[352px] -bottom-[920px] left-[calc(50%-17.5px)] w-[35px] origin-top-right overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]"
style={{ "--ray-opacity": 0.65, transform: "scale(0.83) rotate(9deg)", animation: "rotated-ray-pulse 4.2s ease-in-out 0.2s infinite both" } as React.CSSProperties}
/>
<div
className="absolute -top-[352px] -bottom-[920px] left-[calc(50%-17.5px)] w-[35px] origin-top-right overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]"
style={{ "--ray-opacity": 1, transform: "scale(0.9) rotate(14deg)", animation: "rotated-ray-pulse 3.8s ease-in-out 1s infinite both" } as React.CSSProperties}
/>
<div className="absolute left-[calc(50%-599px)] -top-[352px] -bottom-[46px] w-[1198px] opacity-[0.05] overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]" />
<div className="absolute left-[calc(50%-432.5px)] -top-[252px] w-[865px] h-[929px] opacity-15 overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]" />
<div className="absolute left-[calc(50%-432.5px)] -top-[252px] w-[865px] h-[929px] opacity-15 overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]" />
</div>
</div>
);
};
export default LightRaysCornerBackground;

View File

@@ -19,7 +19,7 @@ export default function HomePage() {
description="Unlock bespoke itineraries, exclusive destinations, and personalized service designed for the discerning traveler. Your extraordinary adventure begins here."
primaryButton={{
text: "Plan Your Escape",
href: "#contact",
href: "https://www.dialedweb.com/",
}}
secondaryButton={{
text: "Explore Destinations",
@@ -27,7 +27,7 @@ export default function HomePage() {
}}
items={[
{
imageSrc: "http://img.b2bpic.net/free-photo/romantic-portrait-woman-white-dress-sailing-large-boat-ferry_343596-2665.jpg",
imageSrc: "https://pixabay.com/get/g6ac6b2d378455c6ee22905d120e345a047c12fc7f754845cd2501f04bd4b46c102d9f6d72e1beae27b1924dd88f3d8054a04eae6fb9d14973650a6f73a631c84_1280.jpg?id=1850804",
},
{
imageSrc: "http://img.b2bpic.net/free-photo/chocolate-fondant-with-ice-cream-tea-top-view_141793-4385.jpg",
@@ -43,7 +43,7 @@ export default function HomePage() {
title="Crafting Your Perfect Journey"
primaryButton={{
text: "Learn More",
href: "#",
href: "https://www.dialedweb.com/",
}}
/>
</SectionErrorBoundary>