diff --git a/images/orgs/findr.webp b/images/orgs/findr.webp new file mode 100644 index 0000000..9987551 Binary files /dev/null and b/images/orgs/findr.webp differ diff --git a/images/orgs/patr.jpg b/images/orgs/patr.jpg new file mode 100644 index 0000000..5ccf21b Binary files /dev/null and b/images/orgs/patr.jpg differ diff --git a/package-lock.json b/package-lock.json index ff53906..3da07a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "thebatproject", - "version": "0.1.2", + "version": "0.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "thebatproject", - "version": "0.1.2", + "version": "0.1.3", "dependencies": { "@tsparticles/all": "^3.7.1", "@tsparticles/engine": "^3.7.1", "@tsparticles/react": "^3.0.0", + "framer-motion": "^11.15.0", "next": "15.1.0", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -7338,6 +7339,32 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/framer-motion": { + "version": "11.15.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.15.0.tgz", + "integrity": "sha512-MLk8IvZntxOMg7lDBLw2qgTHHv664bYoYmnFTmE0Gm/FW67aOJk0WM3ctMcG+Xhcv+vh5uyyXwxvxhSeJzSe+w==", + "dependencies": { + "motion-dom": "^11.14.3", + "motion-utils": "^11.14.3", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fs-extra": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", @@ -8800,6 +8827,16 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/motion-dom": { + "version": "11.14.3", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.14.3.tgz", + "integrity": "sha512-lW+D2wBy5vxLJi6aCP0xyxTxlTfiu+b+zcpVbGVFUxotwThqhdpPRSmX8xztAgtZMPMeU0WGVn/k1w4I+TbPqA==" + }, + "node_modules/motion-utils": { + "version": "11.14.3", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.14.3.tgz", + "integrity": "sha512-Xg+8xnqIJTpr0L/cidfTTBFkvRw26ZtGGuIhA94J9PQ2p4mEa06Xx7QVYZH0BP+EpMSaDlu+q0I0mmvwADPsaQ==" + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", diff --git a/package.json b/package.json index ce0cf30..5cb97c5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "thebatproject", - "version": "0.1.2", + "version": "0.1.3", "private": true, "type": "module", "scripts": { @@ -20,6 +20,7 @@ "@tsparticles/all": "^3.7.1", "@tsparticles/engine": "^3.7.1", "@tsparticles/react": "^3.0.0", + "framer-motion": "^11.15.0", "next": "15.1.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/src/app/_components/BatLogo.tsx b/src/app/_components/BatLogo.tsx index 60bf13d..e721764 100644 --- a/src/app/_components/BatLogo.tsx +++ b/src/app/_components/BatLogo.tsx @@ -1,13 +1,20 @@ "use client"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import Particles, { initParticlesEngine } from "@tsparticles/react"; import { loadAll } from "@tsparticles/all"; -import { batLogoOptions } from "@/lib/tsparticles"; +import { motion, useReducedMotion } from "framer-motion"; +import { getBatLogoOptions } from "@/lib/tsparticles"; export const BatLogo = () => { const [hasInitialized, setHasInitialized] = useState(false); + const reduceMotion = useReducedMotion(); + + const batLogoOptions = useMemo(() => { + return getBatLogoOptions(!!reduceMotion); + }, [reduceMotion]); + useEffect(() => { (async () => { try { @@ -24,11 +31,23 @@ export const BatLogo = () => { return ( hasInitialized && ( -
+
-
+ ) ); }; diff --git a/src/app/_components/ExperienceCard.tsx b/src/app/_components/ExperienceCard.tsx new file mode 100644 index 0000000..94a90f6 --- /dev/null +++ b/src/app/_components/ExperienceCard.tsx @@ -0,0 +1,96 @@ +"use client"; + +import { Experience } from "@/utils/experiences"; +import Image from "next/image"; +import React from "react"; +import { motion } from "framer-motion"; +import { ExperienceDescription } from "./ExperienceDescription"; +import { CustomLink } from "@/components/CustomLink"; + +type Props = { + experience: Experience; +}; + +const container = { + hidden: { opacity: 0, y: 100 }, + show: { + opacity: 1, + y: 0, + transition: { + staggerChildren: 0.1, + }, + }, +}; + +const item = { + hidden: { opacity: 0, y: 100 }, + show: { opacity: 1, y: 0 }, +}; + +export const ExperienceCard: React.FC = ({ experience }) => { + return ( +
+ + + {experience.title} + + + + {experience.company} + +  • {experience.date} + + + + {experience.imageAlt} + + + + {experience.subheading} + +
    + {experience.descriptions.map((description, index) => ( +
  • + +
  • + ))} +
+
+
+ ); +}; diff --git a/src/app/_components/ExperienceDescription.tsx b/src/app/_components/ExperienceDescription.tsx new file mode 100644 index 0000000..660e997 --- /dev/null +++ b/src/app/_components/ExperienceDescription.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { + animate, + useInView, + useMotionValue, + useTransform, +} from "framer-motion"; +import React, { useEffect, useRef } from "react"; +import { motion } from "framer-motion"; + +type Props = { + description: string; + index: number; +}; + +export const ExperienceDescription: React.FC = ({ + description, + index, +}) => { + const descriptionRef = useRef(null); + + const duration = 4; + const delay = 0.75 + duration * index; + + const count = useMotionValue(0); + const rounded = useTransform(count, (latest) => Math.round(latest)); + const displayText = useTransform(rounded, (latest) => + description.slice(0, latest), + ); + + const isInView = useInView(descriptionRef); + + useEffect(() => { + const controls = animate(count, description.length, { + type: "tween", + duration, + ease: "linear", + autoplay: false, + delay, + }); + if (isInView) { + controls.play(); + } else { + controls.stop(); + } + return controls.stop; + }, [description.length, isInView, count, delay, duration]); + + return ( + + {displayText} + + ); +}; diff --git a/src/app/_components/HeroCopy.tsx b/src/app/_components/HeroCopy.tsx index 99a9c92..f76f8bf 100644 --- a/src/app/_components/HeroCopy.tsx +++ b/src/app/_components/HeroCopy.tsx @@ -1,21 +1,55 @@ +"use client"; + import React from "react"; import { CustomLink } from "@/components/CustomLink"; import { ExternalLinks } from "@/utils/constants"; +import { motion } from "framer-motion"; + +const container = { + hidden: { opacity: 0 }, + show: { + opacity: 1, + transition: { + delay: 0.5, + duration: 2.5, + staggerChildren: 0.3, + }, + }, +}; + +const item = { + hidden: { opacity: 0 }, + show: { opacity: 1 }, +}; export const HeroCopy = () => { return ( -
-

+ + Developer by day,
Vigilante by night -

-

+ + “If I can't change things here,{" "}
if I can't have an effect,
I don't care what happens to me.”
-

-
+ + { target="_blank" > SUMMON THE KNIGHT - - (Schedule a call) - + (Schedule a call) { target="_blank" > VIGILANTE PROFILE - - (Download resume) - + (Download resume) -
-
+ + ); }; diff --git a/src/app/_components/HeroSection.tsx b/src/app/_components/HeroSection.tsx index 8af4850..1a00698 100644 --- a/src/app/_components/HeroSection.tsx +++ b/src/app/_components/HeroSection.tsx @@ -2,6 +2,7 @@ import React from "react"; import dynamic from "next/dynamic"; import { HeroCopy } from "./HeroCopy"; import { PageSection } from "./PageSection"; +import { HomePageSections } from "@/utils/constants"; const BatLogo = dynamic( () => import("./BatLogo").then((module) => module.BatLogo), @@ -13,8 +14,8 @@ const BatLogo = dynamic( export const HeroSection = () => { return (
diff --git a/src/app/_components/NightSky.tsx b/src/app/_components/NightSky.tsx index 8a5cf56..946249d 100644 --- a/src/app/_components/NightSky.tsx +++ b/src/app/_components/NightSky.tsx @@ -1,13 +1,20 @@ "use client"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import Particles, { initParticlesEngine } from "@tsparticles/react"; import { loadAll } from "@tsparticles/all"; -import { nightSkyOptions } from "@/lib/tsparticles"; +import { useReducedMotion } from "framer-motion"; +import { getNightSkyOptions } from "@/lib/tsparticles"; export const NightSky = () => { const [hasInitialized, setHasInitialized] = useState(false); + const reduceMotion = useReducedMotion(); + + const nightSkyOptions = useMemo(() => { + return getNightSkyOptions(!!reduceMotion); + }, [reduceMotion]); + useEffect(() => { (async () => { try { diff --git a/src/app/_components/WorkSection.tsx b/src/app/_components/WorkSection.tsx new file mode 100644 index 0000000..e7ec872 --- /dev/null +++ b/src/app/_components/WorkSection.tsx @@ -0,0 +1,65 @@ +"use client"; + +import React from "react"; +import { PageSection } from "./PageSection"; +import { HomePageSections } from "@/utils/constants"; +import { motion } from "framer-motion"; +import { Experiences } from "@/utils/experiences"; +import { ExperienceCard } from "./ExperienceCard"; + +const container = { + hidden: { opacity: 0, y: 100 }, + show: { + opacity: 1, + y: 0, + transition: { + staggerChildren: 0.2, + }, + }, +}; + +const item = { + hidden: { opacity: 0, y: 100 }, + show: { opacity: 1, y: 0 }, +}; + +export const WorkSection = () => { + return ( + +
+ + + The Knight's journey + + + Every vigilante needs a backstory. Here's my professional + journey, from sidekick to full-fledged hero of the web development + world. + + +
+ {Experiences.map((experience, index) => ( + + ))} +
+
+
+ ); +}; diff --git a/src/app/error.tsx b/src/app/error.tsx new file mode 100644 index 0000000..adefbd8 --- /dev/null +++ b/src/app/error.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { Button } from "@/components/Button"; + +interface Props { + error: Error; + reset: () => void; +} + +const ErrorPage: React.FC = ({ reset }) => { + return ( +
+

+ The dark has taken over +

+

+ The system is broken, consumed by the shadows.
We are working in + silence to bring back control—patience is your ally. +

+ +
+ ); +}; + +export default ErrorPage; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 2d1433c..d50122c 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,11 +1,12 @@ -import { Bebas_Neue, Inter } from "next/font/google"; +import { Bebas_Neue, Rethink_Sans } from "next/font/google"; import "@/styles/globals.css"; import { getMetaData, getViewPort } from "@/utils/helpers"; import { ServiceWorker } from "@/components/ServiceWorker"; +import { MotionConfig } from "framer-motion"; -const rethinkSans = Inter({ +const rethinkSans = Rethink_Sans({ subsets: ["latin"], - variable: "--font-inter", + variable: "--font-rethink-sans", }); const bebasNeue = Bebas_Neue({ @@ -24,11 +25,11 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + - {children} + {children} diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index aee1562..b007738 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -1,17 +1,18 @@ import { CustomLink } from "@/components/CustomLink"; -import { InternalRoutes } from "@/utils/constants"; +import { HomePageSections } from "@/utils/constants"; const NotFound = () => { return (

- Holy Broken Links, Batman! + Lost in the shadows

-

- Even Batman can't find this page... It might have vanished into the - Batcave or been taken down by the Joker. Either way, it's not here.{" "} +

+ The path you seek does not exist.
Every choice leads + somewhere—this one ends here.
Return to the light and find + your way.

- + Back to Home
diff --git a/src/app/page.tsx b/src/app/page.tsx index ec0e87a..40f3169 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -2,6 +2,7 @@ import dynamic from "next/dynamic"; import { HeroSection } from "./_components/HeroSection"; import { Header } from "@/components/Header"; import { Footer } from "@/components/Footer"; +import { WorkSection } from "./_components/WorkSection"; const NightSky = dynamic( () => import("./_components/NightSky").then((module) => module.NightSky), @@ -17,8 +18,9 @@ export default function Home() { <>
-
+
+