feat: integrate React support and add AnimatedText component for enhanced animations

This commit is contained in:
mr. M 2024-12-11 18:40:01 +01:00
parent 4059cfb764
commit aa5c4f6ad0
No known key found for this signature in database
GPG key ID: CBD57A2AEDBDA1FB
10 changed files with 319 additions and 42 deletions

View file

@ -2,7 +2,9 @@
import { defineConfig } from 'astro/config';
import tailwind from '@astrojs/tailwind';
import react from '@astrojs/react';
// https://astro.build/config
export default defineConfig({
integrations: [tailwind()],
});
integrations: [tailwind(), react()],
});

143
package-lock.json generated
View file

@ -10,21 +10,26 @@
"dependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/cloudflare": "^12.0.1",
"@astrojs/react": "^4.1.0",
"@astrojs/tailwind": "^5.1.2",
"@fontsource/bricolage-grotesque": "^5.1.0",
"@fortawesome/fontawesome-svg-core": "^6.7.1",
"@fortawesome/free-brands-svg-icons": "^6.7.1",
"@fortawesome/free-solid-svg-icons": "^6.7.1",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2",
"astro": "^5.0.4",
"astro-navbar": "^2.3.7",
"autoprefixer": "10.4.14",
"free-astro-components": "^1.1.1",
"lucide-astro": "^0.460.0",
"motion": "^11.13.1",
"motion": "^11.13.5",
"postcss": "8.4.21",
"prettier": "^3.3.3",
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-tailwindcss": "^0.6.6",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"sharp": "^0.33.5",
"tailwindcss": "^3.4.15",
"typescript": "^5.6.3"
@ -569,6 +574,25 @@
"node": "^18.17.1 || ^20.3.0 || >=22.0.0"
}
},
"node_modules/@astrojs/react": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@astrojs/react/-/react-4.1.0.tgz",
"integrity": "sha512-8F0ncvcCexVeQZMwPouLSFuzCK1KXUIYQ57lW3ZG2p7B5DGAajXGanb/CGF7MMSpX8Z0t9sELQqLHOCV/+78Ig==",
"dependencies": {
"@vitejs/plugin-react": "^4.3.4",
"ultrahtml": "^1.5.3",
"vite": "^6.0.1"
},
"engines": {
"node": "^18.17.1 || ^20.3.0 || >=22.0.0"
},
"peerDependencies": {
"@types/react": "^17.0.50 || ^18.0.21 || ^19.0.0",
"@types/react-dom": "^17.0.17 || ^18.0.6 || ^19.0.0",
"react": "^17.0.2 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@astrojs/tailwind": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@astrojs/tailwind/-/tailwind-5.1.2.tgz",
@ -901,6 +925,34 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-react-jsx-self": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz",
"integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-react-jsx-source": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz",
"integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/template": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
@ -2466,6 +2518,22 @@
"@types/node": "*"
}
},
"node_modules/@types/react": {
"version": "19.0.1",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.1.tgz",
"integrity": "sha512-YW6614BDhqbpR5KtUYzTA+zlA7nayzJRA9ljz9CQoxthR0sDisYZLuvSMsil36t4EH/uAt8T52Xb4sVw17G+SQ==",
"dependencies": {
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-dom": {
"version": "19.0.2",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.2.tgz",
"integrity": "sha512-c1s+7TKFaDRRxr1TxccIX2u7sfCnc3RxkVyBIUA2lCpyqCF+QoAwQ/CBg7bsMdVwP120HEH143VQezKtef5nCg==",
"peerDependencies": {
"@types/react": "^19.0.0"
}
},
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@ -2476,6 +2544,24 @@
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="
},
"node_modules/@vitejs/plugin-react": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz",
"integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==",
"dependencies": {
"@babel/core": "^7.26.0",
"@babel/plugin-transform-react-jsx-self": "^7.25.9",
"@babel/plugin-transform-react-jsx-source": "^7.25.9",
"@types/babel__core": "^7.20.5",
"react-refresh": "^0.14.2"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"peerDependencies": {
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
}
},
"node_modules/@volar/kit": {
"version": "2.4.10",
"resolved": "https://registry.npmjs.org/@volar/kit/-/kit-2.4.10.tgz",
@ -3305,6 +3391,11 @@
"node": ">=4"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/data-uri-to-buffer": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz",
@ -3687,9 +3778,9 @@
}
},
"node_modules/framer-motion": {
"version": "11.13.1",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.13.1.tgz",
"integrity": "sha512-F40tpGTHByhn9h3zdBQPcEro+pSLtzARcocbNqAyfBI+u9S+KZuHH/7O9+z+GEkoF3eqFxfvVw0eBDytohwqmQ==",
"version": "11.13.5",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.13.5.tgz",
"integrity": "sha512-rArI0zPU9VkpS3Wt0J7dmRxAFUWtzPWoSofNQAP0UO276CmJ+Xlf5xN19GMw3w2QsdrS2sU+0+Q2vtuz4IEZaw==",
"dependencies": {
"motion-dom": "^11.13.0",
"motion-utils": "^11.13.0",
@ -3697,8 +3788,8 @@
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0",
"react-dom": "^18.0.0"
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
@ -5608,11 +5699,11 @@
}
},
"node_modules/motion": {
"version": "11.13.1",
"resolved": "https://registry.npmjs.org/motion/-/motion-11.13.1.tgz",
"integrity": "sha512-64+QpZQv8WJJFn+tEEzX04il9s6ReA6lhKRZaxzD6SunGqoaq5g+AFVfcKWme8N83eytUOpGp7mpfJ9cyZlhAA==",
"version": "11.13.5",
"resolved": "https://registry.npmjs.org/motion/-/motion-11.13.5.tgz",
"integrity": "sha512-zmX/dz60w1ZtQB5NP9xYkLcCKwX9gc+pnHp4/mFhD9YW8wUe2ZmT8OPOtrTtq26/huxElSDu3hB7BMTSJa5iIQ==",
"dependencies": {
"framer-motion": "^11.13.1",
"framer-motion": "^11.13.5",
"tslib": "^2.4.0"
},
"peerDependencies": {
@ -6326,6 +6417,33 @@
}
]
},
"node_modules/react": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
"dependencies": {
"scheduler": "^0.25.0"
},
"peerDependencies": {
"react": "^19.0.0"
}
},
"node_modules/react-refresh": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -6757,6 +6875,11 @@
"suf-log": "^2.5.3"
}
},
"node_modules/scheduler": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="
},
"node_modules/section-matter": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",

View file

@ -13,21 +13,26 @@
"dependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/cloudflare": "^12.0.1",
"@astrojs/react": "^4.1.0",
"@astrojs/tailwind": "^5.1.2",
"@fontsource/bricolage-grotesque": "^5.1.0",
"@fortawesome/fontawesome-svg-core": "^6.7.1",
"@fortawesome/free-brands-svg-icons": "^6.7.1",
"@fortawesome/free-solid-svg-icons": "^6.7.1",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2",
"astro": "^5.0.4",
"astro-navbar": "^2.3.7",
"autoprefixer": "10.4.14",
"free-astro-components": "^1.1.1",
"lucide-astro": "^0.460.0",
"motion": "^11.13.1",
"motion": "^11.13.5",
"postcss": "8.4.21",
"prettier": "^3.3.3",
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-tailwindcss": "^0.6.6",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"sharp": "^0.33.5",
"tailwindcss": "^3.4.15",
"typescript": "^5.6.3"

View file

@ -0,0 +1,98 @@
import { motion, useInView, useAnimation } from "motion/react";
import { useEffect, useRef, type JSX } from "react";
type AnimatedTextProps = {
text: string | string[];
el?: keyof JSX.IntrinsicElements;
className?: string;
once?: boolean;
repeatDelay?: number;
animation?: {
hidden: any;
visible: any;
};
};
const defaultAnimations = {
hidden: {
opacity: 0,
y: 20,
},
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.1,
},
},
};
export const AnimatedText = ({
text,
el: Wrapper = "p",
className,
once,
repeatDelay,
animation = defaultAnimations,
}: AnimatedTextProps) => {
const controls = useAnimation();
const textArray = Array.isArray(text) ? text : [text];
const ref = useRef(null);
const isInView = useInView(ref, { amount: 0.5, once });
useEffect(() => {
let timeout: NodeJS.Timeout;
const show = () => {
controls.start("visible");
if (repeatDelay) {
timeout = setTimeout(async () => {
await controls.start("hidden");
controls.start("visible");
}, repeatDelay);
}
};
if (isInView) {
show();
} else {
controls.start("hidden");
}
return () => clearTimeout(timeout);
}, [isInView]);
return (
<Wrapper className={className}>
<span className="sr-only">{textArray.join(" ")}</span>
<motion.span
ref={ref}
initial="hidden"
animate={controls}
variants={{
visible: { transition: { staggerChildren: 0.1 } },
hidden: {},
}}
aria-hidden
>
{textArray.map((line, lineIndex) => (
<span className="block" key={`${line}-${lineIndex}`}>
{line.split(" ").map((word, wordIndex) => (
<span className="inline-block" key={`${word}-${wordIndex}`}>
{word.split("").map((char, charIndex) => (
<motion.span
key={`${char}-${charIndex}`}
className="inline-block"
variants={animation}
>
{char}
</motion.span>
))}
<span className="inline-block">&nbsp;</span>
</span>
))}
</span>
))}
</motion.span>
</Wrapper>
);
};

View file

@ -13,7 +13,7 @@ import {
<section
id="Community"
class="flex min-h-screen w-full flex-col items-center text-center relative"
class="flex w-full flex-col items-center text-center relative"
>
<Title>Community Driven</Title>
<Description class="px-4 lg:px-0 lg:w-1/2">

View file

@ -4,7 +4,23 @@ import Description from '../components/Description.astro'
import Button from '../components/Button.astro'
import { Image } from 'astro:assets'
import myImage from '../assets/browser.png'
import { ArrowRight } from 'lucide-astro'
import { ArrowRight } from 'lucide-astro';
import { motion } from 'framer-motion';
import { AnimatedText } from './AnimatedText';
let titleAnimationCounter = 0;
function getNewAnimationDelay() {
titleAnimationCounter++;
return titleAnimationCounter * 0.15;
}
function getTitleAnimation() {
return {
initial: { opacity: 0, translateY: 20, filter: 'blur(4px)' },
animate: { opacity: 1, translateY: 0, filter: 'blur(0px)', transition: { duration: 0.3, delay: getNewAnimationDelay() } },
};
}
---
<header
@ -12,18 +28,43 @@ import { ArrowRight } from 'lucide-astro'
class="flex py-56 min-h-screen w-full flex-col items-center gap-24 text-center gap-[20%] lg:gap-[15%]"
>
<div class="flex flex-col items-center h-full justify-center">
<Title class='text-left px-12 lg:px-0 lg:text-center leading-[108px] md:!text-9xl !font-normal'>Welcome to<br class="hidden md:block" /> a <span class="italic">calmer</span> internet</Title>
<Description class='text-left px-12 lg:px-0 lg:text-center'
>Beautifully designed, privacy-focused, and packed with features.<br
/>We care about your experience, not your data.</Description
>
<Title class='text-left px-12 lg:px-0 lg:text-center leading-[108px] md:!text-9xl !font-normal'>
<motion.span client:load {...getTitleAnimation()}>
Welcome
</motion.span>
<motion.span client:load {...getTitleAnimation()}>
to
</motion.span>
<br class="hidden md:block" />
<motion.span client:load {...getTitleAnimation()}>
a
</motion.span>
<motion.span client:load {...getTitleAnimation()} className='italic'>
calmer
</motion.span>
<motion.span client:load {...getTitleAnimation()}>
internet
</motion.span>
</Title>
<motion.span client:load {...getTitleAnimation()}>
<Description class='text-left px-12 lg:px-0 lg:text-center'
>Beautifully designed, privacy-focused, and packed with features.<br
/>We care about your experience, not your data.</Description
>
</motion.span>
<div class="mt-6 gap-3 sm:gap-6 flex flex-row">
<Button href="/download" isPrimary>
Download
<ArrowRight class="size-4" />
</Button>
<Button href="#features">Start Exploring</Button>
<motion.span client:load {...getTitleAnimation()}>
<Button href="/download" isPrimary>
Download
<ArrowRight class="size-4" />
</Button>
</motion.span>
<motion.span client:load {...getTitleAnimation()}>
<Button href="#features">Start Exploring</Button>
</motion.span>
</div>
</div>
</header>
<Image src={myImage} alt="Zen browser" class="top-full -translate-y-[20%] mx-auto" />
<motion.span client:load {...getTitleAnimation()}>
<Image src={myImage} alt="Zen browser" class="top-full -translate-y-[20%] mx-auto" />
</motion.span>

View file

@ -4,16 +4,16 @@ import Title from '../components/Title.astro'
import Description from '../components/Description.astro'
import Button from '../components/Button.astro'
import { Astronav, MenuItems, MenuIcon, Dropdown, DropdownItems, DropdownSubmenu } from "astro-navbar";
import { ArrowRight, ChevronDown } from 'lucide-astro'
import { ArrowRight, ChevronDown, Download, DownloadCloud } from 'lucide-astro'
import Logo from './Logo.astro';
---
<nav
id="nav-bar"
class="fixed flex justify-between w-full items-center pt-6 px-6 z-10 top-0 left-0"
class="fixed flex justify-between w-full items-center lg:pt-6 lg:px-6 z-10 top-0 left-0"
>
<Astronav>
<MenuItems class="w-fit rounded-full border-dark border-2 p-2 backdrop-blur-2xl bg-paper dark:bg-paper dark:shadow-md mx-auto flex gap-2 lg:gap-20">
<MenuItems class="w-fit lg:rounded-full border-dark border-b-2 lg:border-2 p-2 backdrop-blur-2xl bg-paper dark:bg-paper dark:shadow-md mx-auto flex gap-2 lg:gap-20">
<a class="font-bold text-lg items-center flex gap-2" href="/">
<Logo class="text-dark" /> <span class="hidden lg:block">zen browser</span>
</a>
@ -24,7 +24,7 @@ import Logo from './Logo.astro';
<ChevronDown class="group-open:rotate-180 transform transition-transform duration-200" />
</button>
<DropdownItems>
<div class="navbar-dropdown top-16 left-0 right-1/4">
<div class="navbar-dropdown w-full lg:w-fit top-16 left-0 right-1/4">
<a class="dropdown-item row-span-2 bg-dark/5" href="/mods">
<div class="dropdown-title">
Zen Mods
@ -62,7 +62,7 @@ import Logo from './Logo.astro';
<ChevronDown class="group-open:rotate-180 transform transition-transform duration-200" />
</button>
<DropdownItems>
<div class="navbar-dropdown !grid-cols-1 gap-1 top-16 left-1/3 right-1/4">
<div class="navbar-dropdown w-full lg:w-fit left-0 !grid-cols-1 gap-1 top-16 lg:left-1/3 lg:right-1/4">
<a class="dropdown-item" href="/donate">
<div class="dropdown-title">
Donate ❤️
@ -98,13 +98,18 @@ import Logo from './Logo.astro';
</div>
</DropdownItems>
</Dropdown>
<a class="flex items-center" href="/mods">
<a class="flex items-center hidden lg:block" href="/mods">
<span>Mods</span>
</a>
</div>
<Button href="/download" isPrimary>
Download
<ArrowRight class="size-4" />
<span class="hidden lg:block">
Download
<ArrowRight class="size-4" />
</span>
<span class="lg:hidden">
<Download class="size-4" />
</span>
</Button>
</MenuItems>
</Astronav>

View file

@ -1,12 +1,12 @@
---
const { class: className } = Astro.props
const { class: className } = Astro.props;
---
<h1 class:list={["text-dark", className]}>
<h1 class:list={["title text-dark", className]}>
<slot />
</h1>
<style>
h1 {
.title {
line-height: 0.9;
margin-bottom: 0.4rem;
font-family: 'Junicode', serif;
@ -14,6 +14,6 @@ const { class: className } = Astro.props
font-style: normal;
font-feature-settings: 'swsh' 1;
@apply text-4xl lg:text-5xl xl:text-6xl;
@apply text-5xl xl:text-6xl;
}
</style>

View file

@ -74,8 +74,7 @@ import Footer from "../components/Footer.astro";
scroll-behavior: smooth;
}
*:not(h1) > * {
font-size: 18px;
body, body > * {
font-family: "Bricolage Grotesque", sans-serif;
font-optical-sizing: auto;
font-style: normal;

View file

@ -1,3 +1,7 @@
{
"extends": "astro/tsconfigs/strict"
}
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react"
}
}