Refactor src/pages/index.astro to idiomatic Astro architecture
- Extract React components (ParallaxBackground, FloatingHeader, StoryCard, CharacterAccordion, StickyMapSection, ScrollingBookCard) with proper client directives - Create Astro wrapper components (HeroSection, BooksSection, StoriesSection, CharactersSection) for static markup composition - Add reusable AnimatedSection component (client:visible) for scroll animations - Create useScrollAnimation hook for shared intersection observer logic - Update main layout to import global CSS for Tailwind styles - Replace 1130-line monolithic file with modular, maintainable structure - Remove Next.js imports (Image, Link) in favor of native Astro/HTML - Implement islands architecture with strategic client directives for performance Result: Better code organization, reduced client-side JavaScript, improved performance through lazy hydration
This commit is contained in:
parent
e69f8b418a
commit
d2a647b04d
31
src/components/AnimatedSection.tsx
Normal file
31
src/components/AnimatedSection.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import type React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useScrollAnimation } from "@/hooks/useScrollAnimation";
|
||||||
|
|
||||||
|
interface AnimatedSectionProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
delay?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AnimatedSection({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
delay = 0
|
||||||
|
}: AnimatedSectionProps) {
|
||||||
|
const { ref, isVisible } = useScrollAnimation(0.15);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"transition-all duration-1000 ease-out",
|
||||||
|
isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-12",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
style={{ transitionDelay: `${delay}ms` }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
src/components/BooksSection.astro
Normal file
66
src/components/BooksSection.astro
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
import { ScrollingBookCard } from './ScrollingBookCard';
|
||||||
|
|
||||||
|
const books = [
|
||||||
|
{
|
||||||
|
number: 1,
|
||||||
|
title: "The Dreamer and the Marked",
|
||||||
|
description:
|
||||||
|
"In a world where dreams can kill and marks determine destiny, one young woman must navigate the treacherous politics of Arai while unraveling the mystery of her own power.",
|
||||||
|
coverImage: "/fantasy-book-cover-with-fire-and-mystical-symbols-.jpg",
|
||||||
|
comingSoon: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: 2,
|
||||||
|
title: "The Curse of Orias",
|
||||||
|
description: "The curse spreads. The marked fall. And in the shadows, something ancient awakens.",
|
||||||
|
comingSoon: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: 3,
|
||||||
|
title: "Title to be Revealed",
|
||||||
|
description: "The final chapter of the trilogy awaits...",
|
||||||
|
comingSoon: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
---
|
||||||
|
|
||||||
|
<section id="books" class="relative">
|
||||||
|
<div class="flex flex-col lg:flex-row">
|
||||||
|
<div class="lg:w-1/2 lg:sticky lg:top-0 lg:h-screen flex items-center lg:items-start justify-center py-24 lg:pt-48 lg:pb-0">
|
||||||
|
<div class="text-center lg:text-left px-8 lg:px-16 max-w-lg">
|
||||||
|
<span class="text-amber-500/60 text-xs tracking-[0.3em] uppercase">The Series</span>
|
||||||
|
<h2 class="mt-4 font-serif text-4xl md:text-5xl lg:text-6xl text-amber-100/90 tracking-[0.05em] leading-tight">
|
||||||
|
The Arai Chronicles
|
||||||
|
</h2>
|
||||||
|
<p class="mt-6 text-amber-200/50 leading-relaxed">
|
||||||
|
A dark fantasy trilogy exploring dreams, destiny, and the price of power in the mystical realm of Arai.
|
||||||
|
</p>
|
||||||
|
<div class="mt-8 flex items-center gap-4 justify-center lg:justify-start">
|
||||||
|
<div class="w-12 h-px bg-amber-600/40" />
|
||||||
|
<span class="text-amber-400/50 text-sm tracking-wider">3 Books</span>
|
||||||
|
<div class="w-12 h-px bg-amber-600/40" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lg:w-1/2">
|
||||||
|
{books.map((book, index) => (
|
||||||
|
<div
|
||||||
|
key={book.number}
|
||||||
|
class="min-h-screen flex items-center justify-center py-24 lg:py-0 px-8 lg:px-16"
|
||||||
|
>
|
||||||
|
<ScrollingBookCard
|
||||||
|
client:visible
|
||||||
|
number={book.number}
|
||||||
|
title={book.title}
|
||||||
|
description={book.description}
|
||||||
|
coverImage={book.coverImage}
|
||||||
|
comingSoon={book.comingSoon}
|
||||||
|
isLast={index === books.length - 1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
38
src/components/CharacterAccordion.tsx
Normal file
38
src/components/CharacterAccordion.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { useState } from "react"
|
||||||
|
import { Plus } from "lucide-react"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
export function CharacterAccordion({ name, description }: { name: string; description?: string }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border-b border-amber-900/15 last:border-0 group/accordion">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
className="w-full flex items-center justify-between py-6 px-8 hover:bg-amber-900/[0.06] transition-all duration-300 text-left"
|
||||||
|
>
|
||||||
|
<span className="text-amber-100/70 group-hover/accordion:text-amber-100/90 font-serif text-lg tracking-wide transition-colors duration-300">
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"w-9 h-9 rounded-full flex items-center justify-center",
|
||||||
|
"bg-amber-900/15 border border-amber-700/20",
|
||||||
|
"transition-all duration-500",
|
||||||
|
isOpen && "bg-amber-800/30 rotate-45 border-amber-600/30",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4 text-amber-400/60" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"overflow-hidden transition-all duration-500 ease-out",
|
||||||
|
isOpen ? "max-h-48 opacity-100" : "max-h-0 opacity-0",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{description && <p className="px-8 pb-6 text-amber-200/40 leading-relaxed font-sans">{description}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
47
src/components/CharactersSection.astro
Normal file
47
src/components/CharactersSection.astro
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
import { CharacterAccordion } from './CharacterAccordion';
|
||||||
|
import AnimatedSection from './AnimatedSection';
|
||||||
|
---
|
||||||
|
|
||||||
|
<section id="characters" class="py-32 px-6">
|
||||||
|
<div class="max-w-2xl mx-auto">
|
||||||
|
<AnimatedSection client:visible>
|
||||||
|
<div class="text-center mb-16">
|
||||||
|
<span class="text-amber-500/40 text-[10px] tracking-[0.35em] uppercase font-sans">Meet</span>
|
||||||
|
<h2 class="mt-4 font-serif text-4xl md:text-5xl lg:text-6xl text-amber-100/85 tracking-wide">
|
||||||
|
The Characters
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</AnimatedSection>
|
||||||
|
|
||||||
|
<AnimatedSection client:visible delay={200}>
|
||||||
|
<div class="bg-amber-950/10 border border-amber-900/15 rounded-sm overflow-hidden">
|
||||||
|
<CharacterAccordion
|
||||||
|
client:visible
|
||||||
|
name="Krystal Monraais"
|
||||||
|
description="A young dreamer marked by forces beyond her understanding, navigating the treacherous politics of Arai while uncovering the truth of her power."
|
||||||
|
/>
|
||||||
|
<CharacterAccordion
|
||||||
|
client:visible
|
||||||
|
name="Drarja"
|
||||||
|
description="A mysterious figure from the Dread Clans, whose loyalty is as shifting as the shadows they inhabit."
|
||||||
|
/>
|
||||||
|
<CharacterAccordion
|
||||||
|
client:visible
|
||||||
|
name="Javis Zevos"
|
||||||
|
description="A scholar of ancient texts, seeking answers in the forgotten corners of history."
|
||||||
|
/>
|
||||||
|
<CharacterAccordion
|
||||||
|
client:visible
|
||||||
|
name="Talara"
|
||||||
|
description="Guardian of secrets older than the realm itself, bound by oaths that transcend mortal understanding."
|
||||||
|
/>
|
||||||
|
<CharacterAccordion
|
||||||
|
client:visible
|
||||||
|
name="Averil"
|
||||||
|
description="A warrior whose blade has tasted the blood of legends, now seeking redemption in unlikely places."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</AnimatedSection>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
181
src/components/FloatingHeader.tsx
Normal file
181
src/components/FloatingHeader.tsx
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
import { useState, useEffect, useRef } from "react"
|
||||||
|
import { ChevronDown } from "lucide-react"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
export interface NavItem {
|
||||||
|
label: string
|
||||||
|
href: string
|
||||||
|
hasDropdown?: boolean
|
||||||
|
dropdownItems?: { label: string; href: string }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FloatingHeader({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
navItems,
|
||||||
|
}: {
|
||||||
|
title: string
|
||||||
|
subtitle?: string
|
||||||
|
navItems: NavItem[]
|
||||||
|
}) {
|
||||||
|
const [scrollY, setScrollY] = useState(0)
|
||||||
|
const [openDropdown, setOpenDropdown] = useState<string | null>(null)
|
||||||
|
const navContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
setScrollY(window.scrollY)
|
||||||
|
}
|
||||||
|
window.addEventListener("scroll", handleScroll, { passive: true })
|
||||||
|
return () => window.removeEventListener("scroll", handleScroll)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
if (navContainerRef.current && !navContainerRef.current.contains(e.target as Node)) {
|
||||||
|
setOpenDropdown(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener("click", handleClickOutside)
|
||||||
|
return () => document.removeEventListener("click", handleClickOutside)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const leftNavItems = navItems.slice(0, 2)
|
||||||
|
const rightNavItems = navItems.slice(2)
|
||||||
|
|
||||||
|
const headerCompact = scrollY > 80
|
||||||
|
const pillOpacity = Math.max(0, 1 - scrollY / 120)
|
||||||
|
const pillScale = Math.max(0.9, 1 - scrollY / 800)
|
||||||
|
const pillTranslateY = Math.min(0, -scrollY / 3)
|
||||||
|
const flankingOpacity = Math.max(0, Math.min(1, (scrollY - 150) / 100))
|
||||||
|
const flankingTranslateX = Math.max(0, 30 - (scrollY - 150) / 3)
|
||||||
|
|
||||||
|
const toggleDropdown = (label: string, e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setOpenDropdown(openDropdown === label ? null : label)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderNavItem = (item: NavItem, position?: "left" | "right", inPill?: boolean) => (
|
||||||
|
<div key={`${item.href}-${position || "pill"}`} className="relative">
|
||||||
|
{item.hasDropdown ? (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={(e) => toggleDropdown(item.label, e)}
|
||||||
|
className="flex items-center gap-1 text-amber-200/60 hover:text-amber-100 transition-colors text-xs tracking-[0.12em] uppercase font-serif px-3 py-2"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
<ChevronDown
|
||||||
|
className={cn("w-3 h-3 transition-transform duration-300", openDropdown === item.label && "rotate-180")}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute top-full pt-3 z-[60]",
|
||||||
|
"transition-all duration-300",
|
||||||
|
position === "left" ? "left-0" : position === "right" ? "right-0" : "left-1/2 -translate-x-1/2",
|
||||||
|
openDropdown === item.label ? "opacity-100 visible translate-y-0" : "opacity-0 invisible -translate-y-2",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="bg-[#1a2222]/95 backdrop-blur-md border border-amber-900/30 rounded-lg overflow-hidden shadow-2xl min-w-[200px]">
|
||||||
|
{item.dropdownItems?.map((dropItem) => (
|
||||||
|
<a
|
||||||
|
key={dropItem.href}
|
||||||
|
href={dropItem.href}
|
||||||
|
className="block px-5 py-3 text-amber-200/60 hover:text-amber-100 hover:bg-amber-900/20 text-xs tracking-[0.1em] transition-all duration-200"
|
||||||
|
onClick={() => setOpenDropdown(null)}
|
||||||
|
>
|
||||||
|
{dropItem.label}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
href={item.href}
|
||||||
|
className="text-amber-200/60 hover:text-amber-100 transition-colors text-xs tracking-[0.12em] uppercase font-serif px-3 py-2"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header
|
||||||
|
className={cn(
|
||||||
|
"fixed top-0 left-0 right-0 z-50 transition-all duration-700 ease-out",
|
||||||
|
headerCompact ? "py-3" : "py-6",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute top-0 left-0 right-0 bg-[#1f2828] backdrop-blur-md shadow-2xl shadow-black/30 transition-all duration-500 -z-10",
|
||||||
|
headerCompact ? "h-full" : "h-0",
|
||||||
|
)}
|
||||||
|
style={{ opacity: headerCompact ? 1 : 0 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div ref={navContainerRef} className="max-w-6xl mx-auto px-6">
|
||||||
|
<div className="flex items-center justify-center w-full">
|
||||||
|
<nav
|
||||||
|
className="flex items-center gap-1 transition-all duration-300 ease-out"
|
||||||
|
style={{
|
||||||
|
opacity: flankingOpacity,
|
||||||
|
transform: `translateX(-${flankingTranslateX}px)`,
|
||||||
|
pointerEvents: flankingOpacity > 0.5 ? "auto" : "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{leftNavItems.map((item) => renderNavItem(item, "left"))}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className={cn("text-center transition-all duration-700 ease-out", headerCompact ? "px-6" : "px-0")}>
|
||||||
|
<h1
|
||||||
|
className={cn(
|
||||||
|
"font-serif tracking-[0.35em] text-amber-100/90 uppercase transition-all duration-700 ease-out whitespace-nowrap",
|
||||||
|
headerCompact ? "text-lg" : "text-3xl md:text-4xl",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
"text-amber-200/40 tracking-[0.25em] uppercase text-xs mt-2 transition-all duration-500 ease-out",
|
||||||
|
headerCompact ? "opacity-0 h-0 mt-0 overflow-hidden" : "opacity-100",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{subtitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav
|
||||||
|
className="flex items-center gap-1 transition-all duration-300 ease-out"
|
||||||
|
style={{
|
||||||
|
opacity: flankingOpacity,
|
||||||
|
transform: `translateX(${flankingTranslateX}px)`,
|
||||||
|
pointerEvents: flankingOpacity > 0.5 ? "auto" : "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{rightNavItems.map((item) => renderNavItem(item, "right"))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="absolute left-1/2 transition-all duration-300 ease-out"
|
||||||
|
style={{
|
||||||
|
top: "100%",
|
||||||
|
marginTop: "1rem",
|
||||||
|
opacity: pillOpacity,
|
||||||
|
transform: `translateX(-50%) scale(${pillScale}) translateY(${pillTranslateY}px)`,
|
||||||
|
pointerEvents: pillOpacity > 0.3 ? "auto" : "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<nav className="flex items-center gap-2 md:gap-4 px-6 py-3 bg-[#1a2222]/60 backdrop-blur-sm rounded-full border border-amber-900/20">
|
||||||
|
{navItems.map((item) => renderNavItem(item, undefined, true))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
28
src/components/Footer.astro
Normal file
28
src/components/Footer.astro
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<footer class="relative border-t border-amber-900/15 py-20 mt-32">
|
||||||
|
<div class="max-w-6xl mx-auto px-6">
|
||||||
|
<div class="flex flex-col items-center gap-8">
|
||||||
|
<div class="flex gap-3">
|
||||||
|
{["Instagram", "Tumblr", "Etsy"].map((name) => (
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="w-11 h-11 rounded-full bg-amber-900/15 hover:bg-amber-800/25 flex items-center justify-center text-amber-200/40 hover:text-amber-100/80 text-xs font-serif transition-all duration-500 hover:scale-110 border border-amber-800/15 hover:border-amber-700/30"
|
||||||
|
>
|
||||||
|
{name[0]}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-8 text-[10px]">
|
||||||
|
<a href="#" class="text-amber-200/30 hover:text-amber-100/70 tracking-[0.2em] transition-colors duration-300 font-sans">INSTA</a>
|
||||||
|
<a href="#" class="text-amber-200/30 hover:text-amber-100/70 tracking-[0.2em] transition-colors duration-300 font-sans">TUMBLR</a>
|
||||||
|
<a href="#" class="text-amber-200/30 hover:text-amber-100/70 tracking-[0.2em] transition-colors duration-300 font-sans">ETSY</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-16 h-px bg-gradient-to-r from-transparent via-amber-700/20 to-transparent" />
|
||||||
|
|
||||||
|
<p class="text-amber-200/20 text-[11px] tracking-[0.15em] font-sans">
|
||||||
|
Copyright © 2025. All rights reserved.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
56
src/components/HeroSection.astro
Normal file
56
src/components/HeroSection.astro
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
import { Feather } from 'lucide-react';
|
||||||
|
import AnimatedSection from './AnimatedSection';
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="relative min-h-screen flex items-center justify-center pt-32">
|
||||||
|
<div class="relative z-10 text-center px-6 max-w-4xl mx-auto">
|
||||||
|
<AnimatedSection client:visible>
|
||||||
|
<div
|
||||||
|
class="transition-all duration-1000 delay-300 opacity-100 translate-y-0"
|
||||||
|
>
|
||||||
|
<div class="inline-flex items-center gap-3 mb-8">
|
||||||
|
<div class="w-16 h-px bg-gradient-to-r from-transparent to-amber-600/40" />
|
||||||
|
<Feather class="w-5 h-5 text-amber-500/50" />
|
||||||
|
<div class="w-16 h-px bg-gradient-to-l from-transparent to-amber-600/40" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="font-serif text-4xl md:text-6xl lg:text-7xl text-amber-100/90 tracking-[0.06em] leading-tight transition-all duration-1000 delay-500 opacity-100 translate-y-0">
|
||||||
|
<span class="block">The Arai</span>
|
||||||
|
<span class="block mt-2">Chronicles</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="mt-8 text-amber-200/40 text-lg md:text-xl max-w-xl mx-auto leading-relaxed font-sans transition-all duration-1000 delay-700 opacity-100 translate-y-0">
|
||||||
|
A fantasy saga of dreams, curses, and the marked souls caught between fate and freedom
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-14 flex flex-col sm:flex-row gap-4 justify-center transition-all duration-1000 delay-900 opacity-100 translate-y-0">
|
||||||
|
<a
|
||||||
|
href="#books"
|
||||||
|
class="group px-8 py-4 bg-amber-800/70 hover:bg-amber-700/80 text-amber-50 font-serif tracking-[0.12em] uppercase text-sm transition-all duration-500 hover:shadow-xl hover:shadow-amber-900/20 hover:-translate-y-1"
|
||||||
|
>
|
||||||
|
<span class="relative">
|
||||||
|
Explore the Books
|
||||||
|
<span class="absolute -bottom-1 left-0 w-0 h-px bg-amber-200/50 transition-all duration-500 group-hover:w-full" />
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#map"
|
||||||
|
class="group px-8 py-4 bg-transparent hover:bg-amber-900/20 text-amber-200/60 hover:text-amber-100 font-serif tracking-[0.12em] uppercase text-sm border border-amber-700/20 hover:border-amber-600/40 transition-all duration-500"
|
||||||
|
>
|
||||||
|
View the Realm
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</AnimatedSection>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="absolute bottom-16 left-1/2 -translate-x-1/2 flex flex-col items-center gap-4 text-amber-200/25">
|
||||||
|
<span class="text-[10px] tracking-[0.25em] uppercase font-serif">Scroll</span>
|
||||||
|
<div class="relative w-px h-16">
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-b from-amber-500/40 to-transparent" />
|
||||||
|
<div class="absolute top-0 w-px h-8 bg-amber-400/60 animate-[scrollPulse_2s_ease-in-out_infinite]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
54
src/components/ParallaxBackground.tsx
Normal file
54
src/components/ParallaxBackground.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { useState, useEffect } from "react"
|
||||||
|
|
||||||
|
export function ParallaxBackground() {
|
||||||
|
const [scrollY, setScrollY] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => setScrollY(window.scrollY)
|
||||||
|
window.addEventListener("scroll", handleScroll, { passive: true })
|
||||||
|
return () => window.removeEventListener("scroll", handleScroll)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 pointer-events-none overflow-hidden">
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-b from-[#252e2e] via-[#1a2222] to-[#141a1a]" />
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="absolute w-[800px] h-[800px] rounded-full bg-amber-900/[0.03] blur-[100px]"
|
||||||
|
style={{
|
||||||
|
top: "5%",
|
||||||
|
left: "-20%",
|
||||||
|
transform: `translateY(${scrollY * 0.08}px)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute w-[600px] h-[600px] rounded-full bg-rose-950/[0.04] blur-[80px]"
|
||||||
|
style={{
|
||||||
|
top: "35%",
|
||||||
|
right: "-10%",
|
||||||
|
transform: `translateY(${scrollY * 0.12}px)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute w-[500px] h-[500px] rounded-full bg-amber-800/[0.025] blur-[60px]"
|
||||||
|
style={{
|
||||||
|
top: "65%",
|
||||||
|
left: "10%",
|
||||||
|
transform: `translateY(${scrollY * 0.06}px)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute w-[400px] h-[400px] rounded-full bg-teal-900/[0.03] blur-[70px]"
|
||||||
|
style={{
|
||||||
|
bottom: "10%",
|
||||||
|
right: "20%",
|
||||||
|
transform: `translateY(${scrollY * 0.1}px)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="absolute inset-0 opacity-[0.15] mix-blend-soft-light bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMDAiIGhlaWdodD0iMzAwIj48ZmlsdGVyIGlkPSJhIj48ZmVUdXJidWxlbmNlIHR5cGU9ImZyYWN0YWxOb2lzZSIgYmFzZUZyZXF1ZW5jeT0iLjc1Ii8+PC9maWx0ZXI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsdGVyPSJ1cmwoI2EpIi8+PC9zdmc+')]" />
|
||||||
|
|
||||||
|
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,transparent_0%,rgba(10,14,14,0.4)_100%)]" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
125
src/components/ScrollingBookCard.tsx
Normal file
125
src/components/ScrollingBookCard.tsx
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react"
|
||||||
|
import { ChevronDown } from "lucide-react"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
export function ScrollingBookCard({
|
||||||
|
title,
|
||||||
|
number,
|
||||||
|
coverImage,
|
||||||
|
description,
|
||||||
|
comingSoon = false,
|
||||||
|
isLast = false,
|
||||||
|
}: {
|
||||||
|
title: string
|
||||||
|
number: number
|
||||||
|
coverImage?: string
|
||||||
|
description?: string
|
||||||
|
comingSoon?: boolean
|
||||||
|
isLast?: boolean
|
||||||
|
}) {
|
||||||
|
const cardRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [progress, setProgress] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (!cardRef.current) return
|
||||||
|
const rect = cardRef.current.getBoundingClientRect()
|
||||||
|
const windowHeight = window.innerHeight
|
||||||
|
const cardCenter = rect.top + rect.height / 2
|
||||||
|
const viewportCenter = windowHeight / 2
|
||||||
|
const distance = cardCenter - viewportCenter
|
||||||
|
const maxDistance = windowHeight
|
||||||
|
const prog = 1 - Math.min(Math.abs(distance) / maxDistance, 1)
|
||||||
|
setProgress(prog)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("scroll", handleScroll, { passive: true })
|
||||||
|
handleScroll()
|
||||||
|
return () => window.removeEventListener("scroll", handleScroll)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={cardRef}
|
||||||
|
className="w-full max-w-md transition-all duration-300"
|
||||||
|
style={{
|
||||||
|
opacity: 0.3 + progress * 0.7,
|
||||||
|
transform: `scale(${0.9 + progress * 0.1}) translateY(${(1 - progress) * 20}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4 mb-8">
|
||||||
|
<span className="inline-flex items-center justify-center w-10 h-10 rounded-full bg-amber-900/40 border border-amber-700/30 text-amber-300/80 font-serif text-lg">
|
||||||
|
{number}
|
||||||
|
</span>
|
||||||
|
<div className="flex-1 h-px bg-gradient-to-r from-amber-700/30 to-transparent" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="group relative mb-8">
|
||||||
|
<div className="absolute -inset-4 bg-amber-600/10 blur-2xl rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-700" />
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute -bottom-4 left-1/2 -translate-x-1/2 w-3/4 h-8 bg-black/40 blur-xl rounded-full transition-all duration-500 group-hover:w-full" />
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative w-full aspect-[2/3] max-w-xs mx-auto",
|
||||||
|
"transition-all duration-700 ease-out",
|
||||||
|
"group-hover:-translate-y-3 group-hover:rotate-1",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="absolute top-2 -right-2 bottom-2 w-3 bg-gradient-to-r from-stone-300 to-stone-200 rounded-r-sm" />
|
||||||
|
<div className="absolute top-2 -right-1 bottom-2 w-1 bg-stone-400/30" />
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative w-full h-full overflow-hidden rounded-sm",
|
||||||
|
"shadow-[8px_8px_30px_rgba(0,0,0,0.4)]",
|
||||||
|
"transition-shadow duration-700",
|
||||||
|
"group-hover:shadow-[12px_16px_40px_rgba(0,0,0,0.5)]",
|
||||||
|
comingSoon ? "bg-stone-900" : "bg-stone-200",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="absolute left-0 top-0 bottom-0 w-4 bg-gradient-to-r from-black/30 via-black/10 to-transparent z-10" />
|
||||||
|
|
||||||
|
{coverImage && !comingSoon ? (
|
||||||
|
<img
|
||||||
|
src={coverImage || "/placeholder.svg"}
|
||||||
|
alt={title}
|
||||||
|
className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-105"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full p-6 text-center">
|
||||||
|
<span className="text-amber-100/80 font-serif text-lg mb-4 leading-tight">{title}</span>
|
||||||
|
<div className="w-12 h-px bg-amber-100/30 mb-3" />
|
||||||
|
<span className="text-amber-100/40 text-xs tracking-[0.2em] uppercase">
|
||||||
|
{comingSoon ? "Coming Soon" : "TBA"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="font-serif text-2xl md:text-3xl text-amber-100/90 tracking-[0.05em]">{title}</h3>
|
||||||
|
{description && <p className="mt-4 text-amber-200/50 leading-relaxed">{description}</p>}
|
||||||
|
{!comingSoon && (
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="inline-flex items-center gap-2 mt-6 text-amber-400/70 hover:text-amber-300 text-sm tracking-wide transition-colors group/link"
|
||||||
|
>
|
||||||
|
<span>Read more</span>
|
||||||
|
<span className="transition-transform group-hover/link:translate-x-1">→</span>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isLast && (
|
||||||
|
<div className="flex justify-center mt-12">
|
||||||
|
<ChevronDown className="w-5 h-5 text-amber-500/30 animate-bounce" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
8
src/components/SectionDivider.astro
Normal file
8
src/components/SectionDivider.astro
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<div class="flex items-center justify-center py-28">
|
||||||
|
<div class="w-20 h-px bg-gradient-to-r from-transparent via-amber-600/20 to-transparent" />
|
||||||
|
<div class="mx-6 relative">
|
||||||
|
<div class="w-1.5 h-1.5 rounded-full bg-amber-600/30" />
|
||||||
|
<div class="absolute inset-0 w-1.5 h-1.5 rounded-full bg-amber-500/20 animate-ping" />
|
||||||
|
</div>
|
||||||
|
<div class="w-20 h-px bg-gradient-to-r from-transparent via-amber-600/20 to-transparent" />
|
||||||
|
</div>
|
||||||
140
src/components/StickyMapSection.tsx
Normal file
140
src/components/StickyMapSection.tsx
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react"
|
||||||
|
|
||||||
|
export function StickyMapSection() {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [scrollProgress, setScrollProgress] = useState(0)
|
||||||
|
const [mapFixed, setMapFixed] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (!containerRef.current) return
|
||||||
|
|
||||||
|
const rect = containerRef.current.getBoundingClientRect()
|
||||||
|
const containerHeight = containerRef.current.offsetHeight
|
||||||
|
const viewportHeight = window.innerHeight
|
||||||
|
|
||||||
|
const scrolled = -rect.top
|
||||||
|
const totalScrollable = containerHeight - viewportHeight
|
||||||
|
const progress = Math.max(0, Math.min(1, scrolled / totalScrollable))
|
||||||
|
|
||||||
|
setScrollProgress(progress)
|
||||||
|
setMapFixed(progress > 0.22)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("scroll", handleScroll, { passive: true })
|
||||||
|
handleScroll()
|
||||||
|
|
||||||
|
return () => window.removeEventListener("scroll", handleScroll)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const scalePhase = Math.min(1, scrollProgress / 0.22)
|
||||||
|
const contentPhase = Math.max(0, (scrollProgress - 0.4) / 0.2)
|
||||||
|
|
||||||
|
const mapScale = 0.7 + scalePhase * 0.3
|
||||||
|
const titleOpacity = 1 - scalePhase
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={containerRef} className="relative" style={{ height: "300vh" }}>
|
||||||
|
<div
|
||||||
|
className={`${mapFixed ? "fixed" : "sticky"} top-0 h-screen w-full overflow-hidden`}
|
||||||
|
style={{ zIndex: mapFixed ? 0 : 1 }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 flex items-center justify-center"
|
||||||
|
style={{
|
||||||
|
transform: `scale(${mapScale})`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="relative w-full h-full">
|
||||||
|
<img
|
||||||
|
src="/images/arai-map-for-book-update-1024x786.jpg"
|
||||||
|
alt="The Realm of Arai"
|
||||||
|
className="w-full h-full object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute inset-0 bg-[#1a2424]/70" style={{ opacity: contentPhase * 0.9 }} />
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 flex flex-col items-center justify-center z-10 pointer-events-none"
|
||||||
|
style={{ opacity: titleOpacity }}
|
||||||
|
>
|
||||||
|
<span className="text-amber-500/60 text-xs tracking-[0.3em] uppercase mb-4">Explore</span>
|
||||||
|
<h2 className="font-serif text-4xl md:text-6xl lg:text-7xl text-amber-100/90 tracking-wide text-center">
|
||||||
|
The Realm of Arai
|
||||||
|
</h2>
|
||||||
|
<p className="mt-6 text-amber-200/40 text-sm italic tracking-wide">Scroll to explore</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 flex items-center justify-center z-20"
|
||||||
|
style={{
|
||||||
|
opacity: contentPhase,
|
||||||
|
transform: `translateY(${(1 - contentPhase) * 30}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="max-w-5xl mx-auto px-8 text-center">
|
||||||
|
<span
|
||||||
|
className="inline-block text-amber-400/70 text-xs tracking-[0.4em] uppercase mb-6"
|
||||||
|
style={{
|
||||||
|
opacity: contentPhase,
|
||||||
|
transform: `translateY(${(1 - contentPhase) * 20}px)`,
|
||||||
|
transition: "transform 0.3s ease-out",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
The World Awaits
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<h3
|
||||||
|
className="font-serif text-4xl md:text-6xl lg:text-7xl text-amber-100 tracking-wide mb-8"
|
||||||
|
style={{
|
||||||
|
textShadow: "0 4px 30px rgba(0,0,0,0.8), 0 0 60px rgba(0,0,0,0.5)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Welcome to Arai
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="w-24 h-px bg-gradient-to-r from-transparent via-amber-500/50 to-transparent mx-auto mb-8" />
|
||||||
|
|
||||||
|
<p
|
||||||
|
className="text-amber-100/80 leading-relaxed text-lg md:text-xl max-w-3xl mx-auto mb-6"
|
||||||
|
style={{
|
||||||
|
textShadow: "0 2px 20px rgba(0,0,0,0.9)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
A realm where ancient magic flows through the veins of the land itself. From the frozen Wastes in the
|
||||||
|
north to the mysterious forests of Erothel, every corner holds secrets waiting to be discovered.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p
|
||||||
|
className="text-amber-200/60 leading-relaxed text-base md:text-lg max-w-2xl mx-auto"
|
||||||
|
style={{
|
||||||
|
textShadow: "0 2px 20px rgba(0,0,0,0.9)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Kingdoms rise and fall, alliances shift like sand, and in the shadows, forces older than memory stir once
|
||||||
|
more.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-12 flex flex-wrap justify-center gap-6 md:gap-10">
|
||||||
|
{["12 Regions", "Ancient Magic", "Dark Prophecies"].map((tag, i) => (
|
||||||
|
<span
|
||||||
|
key={tag}
|
||||||
|
className="text-amber-300/60 text-sm tracking-widest uppercase"
|
||||||
|
style={{
|
||||||
|
opacity: contentPhase,
|
||||||
|
transform: `translateY(${(1 - contentPhase) * (20 + i * 10)}px)`,
|
||||||
|
textShadow: "0 2px 15px rgba(0,0,0,0.9)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
43
src/components/StoriesSection.astro
Normal file
43
src/components/StoriesSection.astro
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
import { StoryCard } from './StoryCard';
|
||||||
|
import AnimatedSection from './AnimatedSection';
|
||||||
|
---
|
||||||
|
|
||||||
|
<section id="stories" class="py-32 md:py-48 px-6 relative overflow-hidden">
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-b from-transparent via-amber-950/[0.03] to-transparent pointer-events-none" />
|
||||||
|
|
||||||
|
<div class="max-w-5xl mx-auto relative">
|
||||||
|
<AnimatedSection client:visible>
|
||||||
|
<div class="mb-24 md:mb-32">
|
||||||
|
<span class="text-amber-500/40 text-[10px] tracking-[0.35em] uppercase block mb-5 font-sans">
|
||||||
|
Tales from the Void
|
||||||
|
</span>
|
||||||
|
<h2 class="font-serif text-5xl md:text-7xl lg:text-8xl text-amber-100/85 tracking-wide">
|
||||||
|
Short Stories
|
||||||
|
</h2>
|
||||||
|
<p class="mt-8 text-amber-200/35 text-lg max-w-lg leading-relaxed font-sans">
|
||||||
|
Fragments of myth and memory from the world of Arai
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</AnimatedSection>
|
||||||
|
|
||||||
|
<AnimatedSection client:visible delay={200}>
|
||||||
|
<div class="space-y-0">
|
||||||
|
<StoryCard
|
||||||
|
title="I Am Not Orias"
|
||||||
|
tagline="A tale of identity"
|
||||||
|
excerpt="In the shadow of the Dread Clans, a young scribe discovers that the name she carries belongs to someone far more dangerous than she ever imagined..."
|
||||||
|
href="#"
|
||||||
|
index={0}
|
||||||
|
/>
|
||||||
|
<StoryCard
|
||||||
|
title="The Stars Are Drowning"
|
||||||
|
tagline="A cosmic lament"
|
||||||
|
excerpt="When the night sky begins to weep, the seers of Talmeya speak of an ancient prophecy—one that foretells the end of all light..."
|
||||||
|
href="#"
|
||||||
|
index={1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</AnimatedSection>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
63
src/components/StoryCard.tsx
Normal file
63
src/components/StoryCard.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { useState } from "react"
|
||||||
|
import { ArrowRight } from "lucide-react"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
export function StoryCard({
|
||||||
|
title,
|
||||||
|
tagline,
|
||||||
|
excerpt,
|
||||||
|
href,
|
||||||
|
index,
|
||||||
|
}: {
|
||||||
|
title: string
|
||||||
|
tagline: string
|
||||||
|
excerpt: string
|
||||||
|
href: string
|
||||||
|
index: number
|
||||||
|
}) {
|
||||||
|
const [isHovered, setIsHovered] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
className="group relative block"
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
>
|
||||||
|
<span className="absolute -left-4 md:-left-16 top-4 font-serif text-7xl md:text-9xl text-amber-600/[0.07] select-none transition-all duration-700 group-hover:text-amber-500/[0.12] group-hover:scale-105">
|
||||||
|
{String(index + 1).padStart(2, "0")}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="relative border-b border-amber-800/20 py-14 md:py-20 pl-8 md:pl-0">
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-amber-900/0 via-amber-900/5 to-amber-900/0 opacity-0 group-hover:opacity-100 transition-opacity duration-700" />
|
||||||
|
|
||||||
|
<div className="relative flex flex-col md:flex-row md:items-end justify-between gap-6 md:gap-8">
|
||||||
|
<div className="flex-1">
|
||||||
|
<span className="text-amber-500/40 text-[10px] tracking-[0.35em] uppercase mb-3 block font-sans">
|
||||||
|
{tagline}
|
||||||
|
</span>
|
||||||
|
<h3 className="font-serif text-3xl md:text-5xl lg:text-6xl text-amber-100/70 tracking-wide transition-all duration-500 group-hover:text-amber-100/90">
|
||||||
|
"{title}"
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 text-amber-500/40 group-hover:text-amber-400/80 transition-all duration-500">
|
||||||
|
<span className="text-[10px] tracking-[0.3em] uppercase hidden md:block font-sans">Read</span>
|
||||||
|
<div className="w-14 h-14 rounded-full border border-current flex items-center justify-center transition-all duration-500 group-hover:bg-amber-500/10 group-hover:scale-110 group-hover:border-amber-400/50">
|
||||||
|
<ArrowRight className="w-5 h-5 transition-transform duration-500 group-hover:translate-x-1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"overflow-hidden transition-all duration-700 ease-out",
|
||||||
|
isHovered ? "max-h-40 opacity-100 mt-8" : "max-h-0 opacity-0 mt-0",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<p className="text-amber-200/40 font-serif text-lg md:text-xl italic max-w-3xl leading-relaxed">{excerpt}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
21
src/components/SupportSection.astro
Normal file
21
src/components/SupportSection.astro
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<section class="py-24 px-6">
|
||||||
|
<div class="text-center max-w-lg mx-auto">
|
||||||
|
<div class="inline-flex items-center justify-center w-14 h-14 rounded-full bg-rose-900/20 border border-rose-800/20 mb-8">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-5 h-5 text-rose-400/60 lucide lucide-heart"><path d="M19 14c1.49-1.46 3-3.73 3-5.5 1.55-1.82 0-3.3-1.73-3.3-1.73 0-3.13 1.47-3.13 3.3C14.44 8.62 13.11 10 11 10c-2.11 0-3.44-1.38-4.27-2.48C5.44 7.47 4.04 6 2.73 6 1 6 .27 7.48 1.73 9.5c0 1.77 1.51 4.04 3 5.5-1.07.89-2.45 2.59-2.63 5.41-.18 2.82 2.32 4.09 4.59 4.09 1.04 0 2.08-.44 3.05-1.29 1.04.85 2.08 1.29 3.05 1.29 2.27 0 4.77-1.27 4.59-4.09-.18-2.82-1.56-4.51-2.63-5.41Z"/></svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="font-serif text-2xl md:text-3xl text-amber-100/85 tracking-[0.08em]">Support the Journey</h3>
|
||||||
|
<p class="mt-5 text-amber-200/40 leading-relaxed font-sans">
|
||||||
|
If you enjoy what I create, consider supporting me on Ko-fi. Every bit helps fuel more stories from the realm
|
||||||
|
of Arai.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="mt-10 inline-flex items-center gap-3 px-8 py-4 bg-rose-900/60 hover:bg-rose-800/70 text-rose-50/90 font-serif tracking-[0.1em] text-sm transition-all duration-500 hover:shadow-xl hover:shadow-rose-950/30 hover:-translate-y-1 group border border-rose-700/20"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4 transition-transform duration-300 group-hover:rotate-12 lucide lucide-coffee"><path d="M10 2v10l5.09-3M2 10h9.09l5.91 3"/></svg>
|
||||||
|
<span>Buy me a coffee</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
29
src/hooks/useScrollAnimation.ts
Normal file
29
src/hooks/useScrollAnimation.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import type React from "react";
|
||||||
|
|
||||||
|
export function useScrollAnimation(threshold: number = 0.1): {
|
||||||
|
ref: React.RefObject<HTMLDivElement>;
|
||||||
|
isVisible: boolean;
|
||||||
|
} {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
setIsVisible(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ref.current) {
|
||||||
|
observer.observe(ref.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [threshold]);
|
||||||
|
|
||||||
|
return { ref, isVisible };
|
||||||
|
}
|
||||||
@ -1,37 +1,20 @@
|
|||||||
---
|
---
|
||||||
import type React from "react"
|
import '@/styles/global.css';
|
||||||
import type { Metadata } from "next"
|
|
||||||
import { Cinzel, Crimson_Text } from "next/font/google"
|
const { title = "The Realm of Arai - Component Library", description = "A fantasy-themed component library inspired by the world of Arai" } = Astro.props;
|
||||||
import { Analytics } from "@vercel/analytics/next"
|
|
||||||
import "./globals.css"
|
|
||||||
---
|
---
|
||||||
const cinzel = Cinzel({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-cinzel",
|
|
||||||
})
|
|
||||||
|
|
||||||
const crimsonText = Crimson_Text({
|
<!doctype html>
|
||||||
subsets: ["latin"],
|
<html lang="en" class="font-sans antialiased">
|
||||||
weight: ["400", "600", "700"],
|
<head>
|
||||||
variable: "--font-crimson",
|
<meta charset="UTF-8" />
|
||||||
})
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
export const metadata: Metadata = {
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
title: "The Realm of Arai - Component Library",
|
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;500;600;700&family=Crimson+Text:wght@400;600;700&display=swap" rel="stylesheet">
|
||||||
description: "A fantasy-themed component library inspired by the world of Arai",
|
<link rel="stylesheet" href="/styles/global.css" />
|
||||||
}
|
</head>
|
||||||
|
<body class="--font-cinzel --font-crimson">
|
||||||
export default function RootLayout({
|
<slot />
|
||||||
children,
|
</body>
|
||||||
}: Readonly<{
|
</html>
|
||||||
children: React.ReactNode
|
|
||||||
}>) {
|
|
||||||
return (
|
|
||||||
<html lang="en">
|
|
||||||
<body className={`${cinzel.variable} ${crimsonText.variable} font-sans antialiased`}>
|
|
||||||
{children}
|
|
||||||
<Analytics />
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user