diff --git a/astro.config.mjs b/astro.config.mjs
index 7c7ad34..cecdd04 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -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()],
+});
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 07519de..3a1a3f8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 5636482..004c5aa 100644
--- a/package.json
+++ b/package.json
@@ -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"
diff --git a/src/components/AnimatedText.tsx b/src/components/AnimatedText.tsx
new file mode 100644
index 0000000..6aaab2f
--- /dev/null
+++ b/src/components/AnimatedText.tsx
@@ -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 (
+
+ {textArray.join(" ")}
+
+ {textArray.map((line, lineIndex) => (
+
+ {line.split(" ").map((word, wordIndex) => (
+
+ {word.split("").map((char, charIndex) => (
+
+ {char}
+
+ ))}
+
+
+ ))}
+
+ ))}
+
+
+ );
+};
diff --git a/src/components/Community.astro b/src/components/Community.astro
index f07b822..9cfab3c 100644
--- a/src/components/Community.astro
+++ b/src/components/Community.astro
@@ -13,7 +13,7 @@ import {