diff --git a/package-lock.json b/package-lock.json index 147ff20..c19eb0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@types/react-syntax-highlighter": "^15.5.13", "autoprefixer": "^10.4.20", + "framer-motion": "^12.7.4", "lucide-react": "^0.477.0", "next": "15.1.7", "react": "^19.0.0", @@ -3045,6 +3046,33 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "12.7.4", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.7.4.tgz", + "integrity": "sha512-jX0bPsTmU0oPZTYz/dVyD0dmOyEOEJvdn0TaZBE5I8g2GvVnnQnW9f65cJnoVfUkY3WZWNXGXnPbVA9YnaIfVA==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.7.4", + "motion-utils": "^12.7.2", + "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/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4263,6 +4291,21 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/motion-dom": { + "version": "12.7.4", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.7.4.tgz", + "integrity": "sha512-1ZUHAoSUMMxP6jPqyxlk9XUfb6NxMsnWPnH2YGhrOhTURLcXWbETi6eemoKb60Pe32NVJYduL4B62VQSO5Jq8Q==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.7.2" + } + }, + "node_modules/motion-utils": { + "version": "12.7.2", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.7.2.tgz", + "integrity": "sha512-XhZwqctxyJs89oX00zn3OGCuIIpVevbTa+u82usWBC6pSHUd2AoNWiYa7Du8tJxJy9TFbZ82pcn5t7NOm1PHAw==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/package.json b/package.json index 4453217..0e70e8a 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@types/react-syntax-highlighter": "^15.5.13", "autoprefixer": "^10.4.20", + "framer-motion": "^12.7.4", "lucide-react": "^0.477.0", "next": "15.1.7", "react": "^19.0.0", diff --git a/src/app/blog/page.tsx b/src/app/blog/page.tsx index 708d3c8..704e716 100644 --- a/src/app/blog/page.tsx +++ b/src/app/blog/page.tsx @@ -1,48 +1,76 @@ import fs from "fs"; import path from "path"; import Link from "next/link"; -import { IoMdArrowBack } from "react-icons/io"; +import { PageTransition } from "@/components/PageTransition"; +import { Metadata } from "next"; +import BackButton from "@/components/BackButton"; + +export const metadata: Metadata = { + title: "Blog", + description: "Read Asher Falcon's blog posts on software engineering, projects, and technology", + keywords: ["blog", "software engineering", "coding", "projects", "tech"], +}; const postsDirectory = path.join(process.cwd(), "src/app/blog/posts"); +// Filter out system files and hidden files +const isValidPostDirectory = (dirname: string) => { + return !dirname.startsWith(".") && !dirname.includes(".DS_Store") && fs.statSync(path.join(postsDirectory, dirname)).isDirectory(); +}; + export async function generateStaticParams() { - const postFolders = fs.readdirSync(postsDirectory); + const allFiles = fs.readdirSync(postsDirectory); + const postFolders = allFiles.filter(isValidPostDirectory); return postFolders.map((slug) => ({ slug })); } +type PostData = { + slug: string; + title: string; + description: string; +}; + export default async function BlogPage() { - const postFolders = fs.readdirSync(postsDirectory); + const allFiles = fs.readdirSync(postsDirectory); + const postFolders = allFiles.filter(isValidPostDirectory); + const posts = await Promise.all( - postFolders.map(async (slug) => { - const { title, description } = await import(`./posts/${slug}/metadata.ts`); - return { slug, title, description }; + postFolders.map(async (slug): Promise => { + try { + const { title, description } = await import(`./posts/${slug}/metadata.ts`); + return { slug, title, description }; + } catch (error) { + console.error(`Error loading metadata for ${slug}:`, error); + return null; + } }) ); - posts.reverse(); + // Filter out any posts that failed to load and reverse for newest first + const validPosts: PostData[] = posts.filter((post): post is PostData => post !== null).reverse(); return ( -
-
-
- - - -
- Posts -
-
-
    - {posts.map(({ slug, title, description }) => ( -
  • - - {title} -

    {description}

    - -
  • - ))} -
-
-
+ +
+
+
+ +
+ Posts +
+
+
    + {validPosts.map(({ slug, title, description }) => ( +
  • + + {title} +

    {description}

    + +
  • + ))} +
+
+
+
); } \ No newline at end of file diff --git a/src/app/blog/posts/0/metadata.ts b/src/app/blog/posts/0/metadata.ts index 92f90b5..1fc3f47 100644 --- a/src/app/blog/posts/0/metadata.ts +++ b/src/app/blog/posts/0/metadata.ts @@ -1,2 +1,23 @@ +import { Metadata } from "next"; + +// Post content export const title = "# curl asherfalcon.com"; export const description = "Hello, I thought I'd use this post as a test to see if this blog works and a quick welcome to this site. This is my personal space for sharing projects, notes, ideas and everything inbetween. I like to play around with lots of different technologies and I'm interested in all aspects of software, from developing it, to managing CI pipelines and deploying it with technologies like docker. Feel free to contact me if you have any questions or ideas, I'd love to hear from you."; + +// Next.js metadata +export const generateMetadata = (): Metadata => { + return { + title: title, + description: description, + openGraph: { + title: title, + description: description, + type: 'article', + }, + twitter: { + card: 'summary', + title: title, + description: description, + }, + }; +}; diff --git a/src/app/blog/posts/0/page.tsx b/src/app/blog/posts/0/page.tsx index d0511aa..4547e65 100644 --- a/src/app/blog/posts/0/page.tsx +++ b/src/app/blog/posts/0/page.tsx @@ -1,21 +1,24 @@ -import Head from 'next/head'; +"use client"; import { title, description } from './metadata'; import Link from 'next/link'; -import { IoMdArrowBack } from 'react-icons/io'; import { LuExternalLink } from 'react-icons/lu'; +import { PageTransition } from '@/components/PageTransition'; +import { useEffect } from 'react'; +import BackButton from '@/components/BackButton'; + +// Metadata is handled by the generateMetadata export in metadata.ts export default function ExamplePost() { + // Update the title on the client side + useEffect(() => { + document.title = `${title} | Asher Falcon`; + }, []); + return ( - <> - - {title} - - -
-
- - - + +
+
+
{title}
@@ -30,7 +33,6 @@ export default function ExamplePost() { Contact
- - + ); } \ No newline at end of file diff --git a/src/app/blog/posts/1/metadata.ts b/src/app/blog/posts/1/metadata.ts index 2a0486f..4b1f9aa 100644 --- a/src/app/blog/posts/1/metadata.ts +++ b/src/app/blog/posts/1/metadata.ts @@ -1,2 +1,23 @@ +import { Metadata } from "next"; + +// Post content export const title = "docker deployment"; export const description = "Recently I found myself wanting to deploy a docker-compose file to a server. I wanted to automate this process as much as possible, so I decided to write a shell script to do it. This post will go through the process of writing a shell script to deploy a docker-compose file to a server."; + +// Next.js metadata +export const generateMetadata = (): Metadata => { + return { + title: title, + description: description, + openGraph: { + title: title, + description: description, + type: 'article', + }, + twitter: { + card: 'summary', + title: title, + description: description, + }, + }; +}; diff --git a/src/app/blog/posts/1/page.tsx b/src/app/blog/posts/1/page.tsx index aa6726c..57d878c 100644 --- a/src/app/blog/posts/1/page.tsx +++ b/src/app/blog/posts/1/page.tsx @@ -1,60 +1,87 @@ "use client" -import Head from 'next/head'; import { title, description } from './metadata'; import Link from 'next/link'; -import { IoMdArrowBack } from 'react-icons/io'; -// import { LuExternalLink } from 'react-icons/lu'; +import { PageTransition } from '@/components/PageTransition'; +import { useEffect } from 'react'; +import BackButton from '@/components/BackButton'; import { script, basicScript, charm } from "./redeploy" - import CodeBlock from '../../../../components/code' +// Note: Static metadata is also generated by the generateMetadata export in metadata.ts +// This useEffect hook ensures the title is properly set on the client side + export default function ExamplePost() { + const pageTitle = `${title} | Asher Falcon`; + + // Update the title and metadata on the client side + useEffect(() => { + document.title = pageTitle; + + // Update meta tags + const metaDescription = document.querySelector('meta[name="description"]'); + if (metaDescription) { + metaDescription.setAttribute('content', description); + } + + // Update Open Graph tags + let metaOgTitle = document.querySelector('meta[property="og:title"]'); + if (!metaOgTitle) { + metaOgTitle = document.createElement('meta'); + metaOgTitle.setAttribute('property', 'og:title'); + document.head.appendChild(metaOgTitle); + } + metaOgTitle.setAttribute('content', title); + + let metaOgDesc = document.querySelector('meta[property="og:description"]'); + if (!metaOgDesc) { + metaOgDesc = document.createElement('meta'); + metaOgDesc.setAttribute('property', 'og:description'); + document.head.appendChild(metaOgDesc); + } + metaOgDesc.setAttribute('content', description); + }, []); + return ( -
- - {title} - - -
-
- - - -
- {title} + +
+
+
+ +
+ {title} +
+
+
+

{description}

+
-
-
-

{description}

+
+

TLDR, if you just want to check out the script and how to use it, you can find it here

+
+ +

As I started deploying more and more services to my server using docker compose, I kept just copying the whole file, sshing into the server, deleting the file and creating a new one, pasting the content. As you can imagine, that got old fast. Realising I was losing an extra minute or two every time I wanted to re-deploy my docker-compose.yml file, I created a simple git repository, and cloned it on both hosts. This meant I could simply push on my laptop and ssh into the server and run git pull, and I would never have to mess around with copying the whole file manually again.

+
+

Now I was still having to ssh in and stop all the containers, pull the repo and then start them again, and I though it would be nice if I could just do this from the terminal in the VSCode editor I used to edit the docker-compose.yml file, so I wrote a simple script which does all of it for me.

+
+
-
-
-

TLDR, if you just want to check out the script and how to use it, you can find it here

-
- -

As I started deploying more and more services to my server using docker compose, I kept just copying the whole file, sshing into the server, deleting the file and creating a new one, pasting the content. As you can imagine, that got old fast. Realising I was losing an extra minute or two every time I wanted to re-deploy my docker-compose.yml file, I created a simple git repository, and cloned it on both hosts. This meant I could simply push on my laptop and ssh into the server and run git pull, and I would never have to mess around with copying the whole file manually again.

-
-

Now I was still having to ssh in and stop all the containers, pull the repo and then start them again, and I though it would be nice if I could just do this from the terminal in the VSCode editor I used to edit the docker-compose.yml file, so I wrote a simple script which does all of it for me.

-
- -
- -
-

So what's going on here? Firstly we commit and push all the changed files to the repository, this means that the docker-compose.yml and any other files, for example config files for services you may be running, are now on the remote repository.

-
-

Next, it simply connects to the server via ssh, and runs the commands listed. Firstly, stopping the containers so that the docker-compose.yml and any other config files can be updated safely, then pulling the git repo to update the files. Finally restarting the docker compose services, and then exiting so the ssh session closes.

-
-

Now to make the commit history not look like groundhog day, and to add some commit messages, we can use Gum, a tool for easily making interactive bash scripts. It works by installing the application onto your system which can then be called inside your bash scripts to invoke user prompts, for example we can use the code below to get a commit message from the user in the terminal and store it as a variable.

-
-
- -
-

Finally, putting all the pieces together and adding a quick check to see if gum is installed, and trying to install it if not, we can arrive at the following script:

-
-
- - -
+ +
+

So what's going on here? Firstly we commit and push all the changed files to the repository, this means that the docker-compose.yml and any other files, for example config files for services you may be running, are now on the remote repository.

+
+

Next, it simply connects to the server via ssh, and runs the commands listed. Firstly, stopping the containers so that the docker-compose.yml and any other config files can be updated safely, then pulling the git repo to update the files. Finally restarting the docker compose services, and then exiting so the ssh session closes.

+
+

Now to make the commit history not look like groundhog day, and to add some commit messages, we can use Gum, a tool for easily making interactive bash scripts. It works by installing the application onto your system which can then be called inside your bash scripts to invoke user prompts, for example we can use the code below to get a commit message from the user in the terminal and store it as a variable.

+
+
+ +
+

Finally, putting all the pieces together and adding a quick check to see if gum is installed, and trying to install it if not, we can arrive at the following script:

+
+
+ +
+
); } \ No newline at end of file diff --git a/src/app/contact/metadata.ts b/src/app/contact/metadata.ts new file mode 100644 index 0000000..9d52edd --- /dev/null +++ b/src/app/contact/metadata.ts @@ -0,0 +1,7 @@ +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Contact", + description: "Get in touch with Asher Falcon - LinkedIn, GitHub, and email contact information", + keywords: ["contact", "Asher Falcon", "LinkedIn", "GitHub", "email"], +}; \ No newline at end of file diff --git a/src/app/contact/page.tsx b/src/app/contact/page.tsx index 3745e83..5a077b1 100644 --- a/src/app/contact/page.tsx +++ b/src/app/contact/page.tsx @@ -1,43 +1,70 @@ "use client" import Link from "next/link"; -import { IoMdArrowBack } from "react-icons/io"; -import { useRouter } from 'next/navigation' // Usage: App router import { FaLinkedin } from "react-icons/fa"; import { FaGithub } from "react-icons/fa"; import { MdOutlineMailOutline } from "react-icons/md"; +import { PageTransition } from "@/components/PageTransition"; +import { useEffect } from "react"; +import BackButton from "@/components/BackButton"; -export default function BlogPage() { +export default function ContactPage() { - const router = useRouter() + // Set the title and metadata on the client side + useEffect(() => { + document.title = "Contact | Asher Falcon"; + + // Set metadata tags + const metaDescription = document.querySelector('meta[name="description"]'); + if (metaDescription) { + metaDescription.setAttribute('content', 'Get in touch with Asher Falcon - LinkedIn, GitHub, and email contact information'); + } + + // Set Open Graph tags + let metaOgTitle = document.querySelector('meta[property="og:title"]'); + if (!metaOgTitle) { + metaOgTitle = document.createElement('meta'); + metaOgTitle.setAttribute('property', 'og:title'); + document.head.appendChild(metaOgTitle); + } + metaOgTitle.setAttribute('content', 'Contact | Asher Falcon'); + + let metaOgDesc = document.querySelector('meta[property="og:description"]'); + if (!metaOgDesc) { + metaOgDesc = document.createElement('meta'); + metaOgDesc.setAttribute('property', 'og:description'); + document.head.appendChild(metaOgDesc); + } + metaOgDesc.setAttribute('content', 'Get in touch with Asher Falcon - LinkedIn, GitHub, and email contact information'); + }, []); - return ( -
-
-
- + return ( + +
+
+
+
Get in touch
-
-
- - LinkedIn - -
-
- - GitHub - -
-
-
- af [at] asherfalcon.com -
-
-
-
+
+
+ + LinkedIn + +
+
+ + GitHub + +
+
+
+ af [at] asherfalcon.com +
+
+
+
+ ); } \ No newline at end of file diff --git a/src/app/error.tsx b/src/app/error.tsx new file mode 100644 index 0000000..c9ef270 --- /dev/null +++ b/src/app/error.tsx @@ -0,0 +1,46 @@ +'use client' + +import { useEffect } from 'react' +import Link from 'next/link' +import { PageTransition } from '@/components/PageTransition' + +export default function Error({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + useEffect(() => { + // Log the error to an error reporting service + console.error(error) + }, [error]) + + return ( + +
+

Something went wrong 😕

+

An unexpected error has occurred.

+ +
+ + + ← Return to Home + +
+ +

+ If this issue persists, please{' '} + + contact me + +

+
+
+ ) +} \ No newline at end of file diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx new file mode 100644 index 0000000..45eb60c --- /dev/null +++ b/src/app/global-error.tsx @@ -0,0 +1,45 @@ +'use client' + +import { useEffect } from 'react' +import Link from 'next/link' + +export default function GlobalError({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + useEffect(() => { + // Log the error to an error reporting service + console.error(error) + }, [error]) + + return ( + + +
+

Something went wrong 🛠️

+

A critical error has occurred.

+ +
+ + + ← Return to Home + +
+ +

+ If this issue persists, please send an email to:{' '} + af [at] asherfalcon.com +

+
+ + + ) +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 68ef597..c78bf10 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,7 +1,7 @@ import type { Metadata } from "next"; import { Open_Sans } from "next/font/google"; import "./globals.css"; -import Head from "next/head"; +import ClientLayout from "@/components/ClientLayout"; const open_sans = Open_Sans({ weight: '500', @@ -10,8 +10,14 @@ const open_sans = Open_Sans({ }) export const metadata: Metadata = { - title: "asher falcon", - description: "Generated by create next app", + title: { + default: "Asher Falcon", + template: "%s | Asher Falcon" + }, + description: "Asher Falcon's personal website - Software engineer and student", + keywords: ["Asher Falcon", "Software Engineer", "Student", "Developer", "Portfolio"], + authors: [{ name: "Asher Falcon" }], + creator: "Asher Falcon", }; export default function RootLayout({ @@ -21,11 +27,10 @@ export default function RootLayout({ }>) { return ( - - - - {children} + + {children} + diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx new file mode 100644 index 0000000..5336eda --- /dev/null +++ b/src/app/not-found.tsx @@ -0,0 +1,65 @@ +"use client" +import Link from "next/link"; +import { PageTransition } from "@/components/PageTransition"; +import { useEffect } from "react"; +import BackButton from "@/components/BackButton"; + +export default function ContactPage() { + + // Set the title and metadata on the client side + useEffect(() => { + document.title = "404"; + + // Set metadata tags + const metaDescription = document.querySelector('meta[name="description"]'); + if (metaDescription) { + metaDescription.setAttribute('content', '404 - Page not found'); + } + + // Set Open Graph tags + let metaOgTitle = document.querySelector('meta[property="og:title"]'); + if (!metaOgTitle) { + metaOgTitle = document.createElement('meta'); + metaOgTitle.setAttribute('property', 'og:title'); + document.head.appendChild(metaOgTitle); + } + metaOgTitle.setAttribute('content', '404 - Page not found'); + + let metaOgDesc = document.querySelector('meta[property="og:description"]'); + if (!metaOgDesc) { + metaOgDesc = document.createElement('meta'); + metaOgDesc.setAttribute('property', 'og:description'); + document.head.appendChild(metaOgDesc); + } + metaOgDesc.setAttribute('content', '404 - Page not found'); + }, []); + + return ( + +
+
+
+ +
+ 404 - Page not found +
+ +
+
+ 👀 Oops, this page doesn't exist +
+
+
+ If you think this is an error, please contact me here +
+
+
+
+ Or go back to the home page +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index f884f10..a727c9b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,29 +1,36 @@ import Link from "next/link"; import { LuExternalLink } from "react-icons/lu"; +import { PageTransition } from "@/components/PageTransition"; +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Home", + description: "Asher Falcon's personal website - Year 12 student passionate about software engineering and problem-solving", +}; export default function Home() { return ( -
-
-
- - Hi, I'm Asher 👋 - -

- I'm a Year 12 student passionate about software engineering and problem-solving. Studying Economics, Computing, Maths, and Chemistry, I enjoy coding, tackling challenges, and building practical solutions. - - This site is where I share my projects and ideas. -

+ +
+
+
+ + Hi, I'm Asher 👋 + +

+I'm a Year 12 student with a strong interest in software engineering, problem-solving, and finance. I'm currently studying Economics, Computer Science, Maths, and Chemistry. This site is where I share my projects, ideas, and what I'm learning along the way. +

+
+
+
+ + Blog + + + Contact +
-
- - Blog - - - Contact - -
-
+ ); } diff --git a/src/components/BackButton.tsx b/src/components/BackButton.tsx new file mode 100644 index 0000000..6163ff0 --- /dev/null +++ b/src/components/BackButton.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { IoMdArrowBack } from "react-icons/io"; +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; + +export default function BackButton() { + const router = useRouter(); + const [pageVisits, setPageVisits] = useState([]); + + useEffect(() => { + // Get page history from session storage + const storedHistory = sessionStorage.getItem('pageHistory'); + const history = storedHistory ? JSON.parse(storedHistory) : []; + + // Add current page to history if it's not the most recent + const currentPath = window.location.pathname; + if (history.length === 0 || history[history.length - 1] !== currentPath) { + const newHistory = [...history, currentPath]; + sessionStorage.setItem('pageHistory', JSON.stringify(newHistory)); + setPageVisits(newHistory); + } else { + setPageVisits(history); + } + }, []); + + const handleBack = () => { + // If we have at least 2 pages in history (current + previous) + if (pageVisits.length > 1) { + // Remove current page from history + const newHistory = [...pageVisits]; + newHistory.pop(); + sessionStorage.setItem('pageHistory', JSON.stringify(newHistory)); + + // Go to previous page in our history + router.push(newHistory[newHistory.length - 1]); + } else { + // If no internal history, go to home page first + const currentPath = window.location.pathname; + if (currentPath !== '/') { + router.push('/'); + } else { + // If already on home page, then go back normally + window.history.back(); + } + } + }; + + return ( + + ); +} \ No newline at end of file diff --git a/src/components/ClientLayout.tsx b/src/components/ClientLayout.tsx new file mode 100644 index 0000000..5f7ef1e --- /dev/null +++ b/src/components/ClientLayout.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { ReactNode } from "react"; +import { TransitionProvider } from "./TransitionProvider"; + +interface ClientLayoutProps { + children: ReactNode; +} + +export default function ClientLayout({ children }: ClientLayoutProps) { + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/src/components/PageTransition.tsx b/src/components/PageTransition.tsx new file mode 100644 index 0000000..c3575cf --- /dev/null +++ b/src/components/PageTransition.tsx @@ -0,0 +1,28 @@ +"use client"; +import { motion } from 'framer-motion'; +import { ReactNode } from 'react'; + +// Animation variants - only using opacity for smoother transitions +const variants = { + hidden: { opacity: 0 }, + enter: { opacity: 1 }, + exit: { opacity: 0 }, +}; + +interface PageTransitionProps { + children: ReactNode; +} + +export const PageTransition = ({ children }: PageTransitionProps) => { + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/src/components/TransitionProvider.tsx b/src/components/TransitionProvider.tsx new file mode 100644 index 0000000..058cdb3 --- /dev/null +++ b/src/components/TransitionProvider.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { ReactNode } from "react"; +import { AnimatePresence } from "framer-motion"; +import { usePathname } from "next/navigation"; + +interface TransitionProviderProps { + children: ReactNode; +} + +export const TransitionProvider = ({ children }: TransitionProviderProps) => { + const pathname = usePathname(); + + return ( + +
+ {children} +
+
+ ); +}; \ No newline at end of file