diff --git a/src/components/AnimatedSection.tsx b/src/components/AnimatedSection.tsx new file mode 100644 index 0000000..c6352f3 --- /dev/null +++ b/src/components/AnimatedSection.tsx @@ -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 ( +
+ {children} +
+ ); +} \ No newline at end of file diff --git a/src/components/BooksSection.astro b/src/components/BooksSection.astro new file mode 100644 index 0000000..5c323e4 --- /dev/null +++ b/src/components/BooksSection.astro @@ -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, + }, +]; +--- + +
+
+
+
+ The Series +

+ The Arai Chronicles +

+

+ A dark fantasy trilogy exploring dreams, destiny, and the price of power in the mystical realm of Arai. +

+
+
+ 3 Books +
+
+
+
+ +
+ {books.map((book, index) => ( +
+ +
+ ))} +
+
+
diff --git a/src/components/CharacterAccordion.tsx b/src/components/CharacterAccordion.tsx new file mode 100644 index 0000000..2161252 --- /dev/null +++ b/src/components/CharacterAccordion.tsx @@ -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 ( +
+ +
+ {description &&

{description}

} +
+
+ ) +} diff --git a/src/components/CharactersSection.astro b/src/components/CharactersSection.astro new file mode 100644 index 0000000..7bb0ab3 --- /dev/null +++ b/src/components/CharactersSection.astro @@ -0,0 +1,47 @@ +--- +import { CharacterAccordion } from './CharacterAccordion'; +import AnimatedSection from './AnimatedSection'; +--- + +
+
+ +
+ Meet +

+ The Characters +

+
+
+ + +
+ + + + + +
+
+
+
diff --git a/src/components/FloatingHeader.tsx b/src/components/FloatingHeader.tsx new file mode 100644 index 0000000..628a841 --- /dev/null +++ b/src/components/FloatingHeader.tsx @@ -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(null) + const navContainerRef = useRef(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) => ( +
+ {item.hasDropdown ? ( +
+ + +
+
+ {item.dropdownItems?.map((dropItem) => ( + setOpenDropdown(null)} + > + {dropItem.label} + + ))} +
+
+
+ ) : ( + + {item.label} + + )} +
+ ) + + return ( +
+
+ +
+
+ + +
+

+ {title} +

+

+ {subtitle} +

+
+ + +
+ +
0.3 ? "auto" : "none", + }} + > + +
+
+
+ ) +} diff --git a/src/components/Footer.astro b/src/components/Footer.astro new file mode 100644 index 0000000..b4c7b5e --- /dev/null +++ b/src/components/Footer.astro @@ -0,0 +1,28 @@ +
+
+
+
+ {["Instagram", "Tumblr", "Etsy"].map((name) => ( + + {name[0]} + + ))} +
+ +
+ INSTA + TUMBLR + ETSY +
+ +
+ +

+ Copyright © 2025. All rights reserved. +

+
+
+
\ No newline at end of file diff --git a/src/components/HeroSection.astro b/src/components/HeroSection.astro new file mode 100644 index 0000000..98c1a62 --- /dev/null +++ b/src/components/HeroSection.astro @@ -0,0 +1,56 @@ +--- +import { Feather } from 'lucide-react'; +import AnimatedSection from './AnimatedSection'; + +--- + +
+
+ +
+
+
+ +
+
+
+ +

+ The Arai + Chronicles +

+ +

+ A fantasy saga of dreams, curses, and the marked souls caught between fate and freedom +

+ + + +
+ +
+ Scroll +
+
+
+
+
+
diff --git a/src/components/ParallaxBackground.tsx b/src/components/ParallaxBackground.tsx new file mode 100644 index 0000000..53d5bf1 --- /dev/null +++ b/src/components/ParallaxBackground.tsx @@ -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 ( +
+
+ +
+
+
+
+ +
+ +
+
+ ) +} diff --git a/src/components/ScrollingBookCard.tsx b/src/components/ScrollingBookCard.tsx new file mode 100644 index 0000000..474e543 --- /dev/null +++ b/src/components/ScrollingBookCard.tsx @@ -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(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 ( +
+
+ + {number} + +
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+ + {coverImage && !comingSoon ? ( + {title} + ) : ( +
+ {title} +
+ + {comingSoon ? "Coming Soon" : "TBA"} + +
+ )} +
+
+
+
+ +
+

{title}

+ {description &&

{description}

} + {!comingSoon && ( + + Read more + + + )} +
+ + {!isLast && ( +
+ +
+ )} +
+ ) +} diff --git a/src/components/SectionDivider.astro b/src/components/SectionDivider.astro new file mode 100644 index 0000000..06996ee --- /dev/null +++ b/src/components/SectionDivider.astro @@ -0,0 +1,8 @@ +
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/src/components/StickyMapSection.tsx b/src/components/StickyMapSection.tsx new file mode 100644 index 0000000..71de162 --- /dev/null +++ b/src/components/StickyMapSection.tsx @@ -0,0 +1,140 @@ +import { useEffect, useRef, useState } from "react" + +export function StickyMapSection() { + const containerRef = useRef(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 ( +
+
+
+
+ The Realm of Arai +
+
+ +
+ +
+ Explore +

+ The Realm of Arai +

+

Scroll to explore

+
+ +
+
+ + The World Awaits + + +

+ Welcome to Arai +

+ +
+ +

+ 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. +

+ +

+ Kingdoms rise and fall, alliances shift like sand, and in the shadows, forces older than memory stir once + more. +

+ +
+ {["12 Regions", "Ancient Magic", "Dark Prophecies"].map((tag, i) => ( + + {tag} + + ))} +
+
+
+
+
+ ) +} diff --git a/src/components/StoriesSection.astro b/src/components/StoriesSection.astro new file mode 100644 index 0000000..317927d --- /dev/null +++ b/src/components/StoriesSection.astro @@ -0,0 +1,43 @@ +--- +import { StoryCard } from './StoryCard'; +import AnimatedSection from './AnimatedSection'; +--- + +
+
+ +
+ +
+ + Tales from the Void + +

+ Short Stories +

+

+ Fragments of myth and memory from the world of Arai +

+
+
+ + +
+ + +
+
+
+
diff --git a/src/components/StoryCard.tsx b/src/components/StoryCard.tsx new file mode 100644 index 0000000..8ad35a7 --- /dev/null +++ b/src/components/StoryCard.tsx @@ -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 ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + {String(index + 1).padStart(2, "0")} + + +
+
+ +
+
+ + {tagline} + +

+ "{title}" +

+
+ +
+ Read +
+ +
+
+
+ +
+

{excerpt}

+
+
+
+ ) +} diff --git a/src/components/SupportSection.astro b/src/components/SupportSection.astro new file mode 100644 index 0000000..98e1766 --- /dev/null +++ b/src/components/SupportSection.astro @@ -0,0 +1,21 @@ +
+
+
+ +
+ +

Support the Journey

+

+ If you enjoy what I create, consider supporting me on Ko-fi. Every bit helps fuel more stories from the realm + of Arai. +

+ + + + Buy me a coffee + +
+
\ No newline at end of file diff --git a/src/hooks/useScrollAnimation.ts b/src/hooks/useScrollAnimation.ts new file mode 100644 index 0000000..2300bdc --- /dev/null +++ b/src/hooks/useScrollAnimation.ts @@ -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; + isVisible: boolean; +} { + const ref = useRef(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 }; +} diff --git a/src/layouts/main.astro b/src/layouts/main.astro index b702b8f..c16116d 100644 --- a/src/layouts/main.astro +++ b/src/layouts/main.astro @@ -1,37 +1,20 @@ --- -import type React from "react" -import type { Metadata } from "next" -import { Cinzel, Crimson_Text } from "next/font/google" -import { Analytics } from "@vercel/analytics/next" -import "./globals.css" +import '@/styles/global.css'; + +const { title = "The Realm of Arai - Component Library", description = "A fantasy-themed component library inspired by the world of Arai" } = Astro.props; --- -const cinzel = Cinzel({ - subsets: ["latin"], - variable: "--font-cinzel", -}) -const crimsonText = Crimson_Text({ - subsets: ["latin"], - weight: ["400", "600", "700"], - variable: "--font-crimson", -}) - -export const metadata: Metadata = { - title: "The Realm of Arai - Component Library", - description: "A fantasy-themed component library inspired by the world of Arai", -} - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode -}>) { - return ( - - - {children} - - - - ) -} + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pages/index.astro b/src/pages/index.astro index 00e304c..0fc32da 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,1129 +1,64 @@ --- -import type React from "react" -import { useState, useEffect, useRef } from "react" -import { cn } from "@/lib/utils" -import Image from "next/image" -import Link from "next/link" -import { Plus, ChevronDown, Coffee, Heart, ArrowRight, Feather } from "lucide-react" +import Layout from '@/layouts/main.astro'; +import { ParallaxBackground } from '@/components/ParallaxBackground'; +import { FloatingHeader, type NavItem } from '@/components/FloatingHeader'; +import HeroSection from '@/components/HeroSection.astro'; +import BooksSection from '@/components/BooksSection.astro'; +import SectionDivider from '@/components/SectionDivider.astro'; +import StoriesSection from '@/components/StoriesSection.astro'; +import { StickyMapSection } from '@/components/StickyMapSection'; +import CharactersSection from '@/components/CharactersSection.astro'; +import SupportSection from '@/components/SupportSection.astro'; +import Footer from '@/components/Footer.astro'; + +const navItems: NavItem[] = [ + { + label: "Books", + href: "#books", + hasDropdown: true, + dropdownItems: [ + { label: "The Dreamer and the Marked", href: "#book-1" }, + { label: "The Curse of Orias", href: "#book-2" }, + { label: "Book Three (Coming Soon)", href: "#book-3" }, + ], + }, + { + label: "Short Writing", + href: "#stories", + hasDropdown: true, + dropdownItems: [ + { label: "I Am Not Orias", href: "#story-1" }, + { label: "The Stars Are Drowning", href: "#story-2" }, + ], + }, + { label: "Art", href: "#art" }, + { label: "About & Contact", href: "#about" }, +]; --- -// ============================================ -// SCROLL ANIMATION HOOK -// ============================================ -function useScrollAnimation(threshold = 0.1) { - const ref = useRef(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 } -} - -// ============================================ -// ANIMATED SECTION WRAPPER -// ============================================ -function AnimatedSection({ - children, - className, - delay = 0, -}: { - children: React.ReactNode - className?: string - delay?: number -}) { - const { ref, isVisible } = useScrollAnimation(0.15) - - return ( -
- {children} -
- ) -} - -// ============================================ -// PARALLAX BACKGROUND -// ============================================ -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 ( -
-
- -
-
-
-
- -
- -
-
- ) -} - -// ============================================ -// FLOATING HEADER WITH BLUR -// ============================================ -interface NavItem { - label: string - href: string - hasDropdown?: boolean - dropdownItems?: { label: string; href: string }[] -} - -function FloatingHeader({ title, subtitle, navItems }: { title: string; subtitle?: string; navItems: NavItem[] }) { - const [scrollY, setScrollY] = useState(0) - const [openDropdown, setOpenDropdown] = useState(null) - const navContainerRef = useRef(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) => ( -
- {item.hasDropdown ? ( -
- - -
-
- {item.dropdownItems?.map((dropItem) => ( - setOpenDropdown(null)} - > - {dropItem.label} - - ))} -
-
-
- ) : ( - - {item.label} - - )} -
- ) - - return ( -
-
- -
-
- - -
-

- {title} -

-

- {subtitle} -

-
- - -
- -
0.3 ? "auto" : "none", - }} - > - -
-
-
- ) -} - -// ============================================ -// HERO SECTION -// ============================================ -function HeroSection() { - const { ref, isVisible } = useScrollAnimation(0.1) - - return ( -
-
-
-
-
- -
-
-
- -

- The Arai - Chronicles -

- -

- A fantasy saga of dreams, curses, and the marked souls caught between fate and freedom -

- -
- - - Explore the Books - - - - - View the Realm - -
-
- -
- Scroll -
-
-
-
-
+
+
- ) -} -// ============================================ -// BOOK CARD - Full width style -// ============================================ -function BookCard({ - title, - number, - coverImage, - description, - comingSoon = false, - reverse = false, -}: { - title: string - number: number - coverImage?: string - description?: string - comingSoon?: boolean - reverse?: boolean -}) { - return ( - -
- {/* Book cover */} -
-
+ + + -
- {/* Shadow beneath */} -
- -
- {/* Page edges */} -
-
- - {/* Cover */} -
- {/* Spine shadow */} -
- - {coverImage && !comingSoon ? ( - {title} - ) : ( -
- {title} -
- - {comingSoon ? "Coming Soon" : "TBA"} - -
- )} -
-
-
-
- - {/* Book info */} -
- - Book {number} - -

{title}

- {description &&

{description}

} - {!comingSoon && ( - - Read more - - - )} -
-
- - ) -} - -// ============================================ -// STORY CARD -// ============================================ -function StoryCard({ - title, - tagline, - excerpt, - href, - index, -}: { - title: string - tagline: string - excerpt: string - href: string - index: number -}) { - const [isHovered, setIsHovered] = useState(false) - - return ( - setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > - - {String(index + 1).padStart(2, "0")} - - -
-
- -
-
- - {tagline} - -

- "{title}" -

-
- -
- Read -
- -
-
-
- -
-

{excerpt}

-
-
- - ) -} - -// ============================================ -// CHARACTER ACCORDION -// ============================================ -function CharacterAccordion({ name, description }: { name: string; description?: string }) { - const [isOpen, setIsOpen] = useState(false) - - return ( -
- -
- {description &&

{description}

} -
-
- ) -} - -// ============================================ -// MAP SECTION - Complete redesign with sticky scroll effect -// ============================================ -function StickyMapSection() { - const containerRef = useRef(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 ( -
-
-
-
- The Realm of Arai -
-
- -
- -
- Explore -

- The Realm of Arai -

-

Scroll to explore

-
- -
-
- - The World Awaits - - -

- Welcome to Arai -

- -
- -

- 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. -

- -

- Kingdoms rise and fall, alliances shift like sand, and in the shadows, forces older than memory stir once - more. -

- -
- {["12 Regions", "Ancient Magic", "Dark Prophecies"].map((tag, i) => ( - - {tag} - - ))} -
-
-
-
-
- ) -} - -// ============================================ -// BOOKS SECTION -// ============================================ -function BooksSection() { - 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, - }, - ] - - return ( -
-
-
-
- The Series -

- The Arai Chronicles -

-

- A dark fantasy trilogy exploring dreams, destiny, and the price of power in the mystical realm of Arai. -

-
-
- 3 Books -
-
-
-
- -
- {books.map((book, index) => ( -
- -
- ))} -
-
+
+
- ) -} -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(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 ( -
-
- - {number} - -
-
- -
-
- -
-
- -
-
-
- -
-
- - {coverImage && !comingSoon ? ( - {title} - ) : ( -
- {title} -
- - {comingSoon ? "Coming Soon" : "TBA"} - -
- )} -
-
-
-
- -
-

{title}

- {description &&

{description}

} - {!comingSoon && ( - - Read more - - - )} -
- - {!isLast && ( -
- -
- )} -
- ) -} - -// ============================================ -// SUPPORT SECTION -// ============================================ -function SupportSection() { - return ( - -
-
- -
- -

Support the Journey

-

- If you enjoy what I create, consider supporting me on Ko-fi. Every bit helps fuel more stories from the realm - of Arai. -

- - - - Buy me a coffee - -
-
- ) -} - -// ============================================ -// FOOTER -// ============================================ -function Footer() { - return ( -
-
-
-
- {["Instagram", "Tumblr", "Etsy"].map((name) => ( - - {name[0]} - - ))} -
- -
- {["INSTA", "TUMBLR", "ETSY"].map((link) => ( - - {link} - - ))} -
- -
- -

- Copyright © 2025. All rights reserved. -

-
-
-
- ) -} - -// ============================================ -// SECTION DIVIDER -// ============================================ -function SectionDivider() { - return ( -
-
-
-
-
-
-
-
- ) -} - -// ============================================ -// MAIN PAGE COMPONENT -// ============================================ -export default function FantasyComponentLibrary() { - const navItems: NavItem[] = [ - { - label: "Books", - href: "#books", - hasDropdown: true, - dropdownItems: [ - { label: "The Dreamer and the Marked", href: "#book-1" }, - { label: "The Curse of Orias", href: "#book-2" }, - { label: "Book Three (Coming Soon)", href: "#book-3" }, - ], - }, - { - label: "Short Writing", - href: "#stories", - hasDropdown: true, - dropdownItems: [ - { label: "I Am Not Orias", href: "#story-1" }, - { label: "The Stars Are Drowning", href: "#story-2" }, - ], - }, - { label: "Art", href: "#art" }, - { label: "About & Contact", href: "#about" }, - ] - - return ( -
- - - - - {/* Hero */} - - - {/* Books Section */} - - - - -
-
- -
- -
- - Tales from the Void - -

- Short Stories -

-

- Fragments of myth and memory from the world of Arai -

-
-
- - -
- - -
-
-
-
- - - -
- -
- - {/* Characters Section */} -
-
- -
- Meet -

- The Characters -

-
-
- - -
- - - - - -
-
-
-
- - - - {/* Support Section */} -
- -
- -
-
- ) -} +
+