feat: added transitions! (And error pages)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 2m54s

This commit is contained in:
Asher 2025-05-31 15:04:45 +01:00
parent 4c5e62f0b3
commit f5c2085fe1
18 changed files with 614 additions and 146 deletions

43
package-lock.json generated
View File

@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@types/react-syntax-highlighter": "^15.5.13", "@types/react-syntax-highlighter": "^15.5.13",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"framer-motion": "^12.7.4",
"lucide-react": "^0.477.0", "lucide-react": "^0.477.0",
"next": "15.1.7", "next": "15.1.7",
"react": "^19.0.0", "react": "^19.0.0",
@ -3045,6 +3046,33 @@
"url": "https://github.com/sponsors/rawify" "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": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -4263,6 +4291,21 @@
"node": ">=16 || 14 >=14.17" "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": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",

View File

@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@types/react-syntax-highlighter": "^15.5.13", "@types/react-syntax-highlighter": "^15.5.13",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"framer-motion": "^12.7.4",
"lucide-react": "^0.477.0", "lucide-react": "^0.477.0",
"next": "15.1.7", "next": "15.1.7",
"react": "^19.0.0", "react": "^19.0.0",

View File

@ -1,39 +1,66 @@
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import Link from "next/link"; 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"); 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() { export async function generateStaticParams() {
const postFolders = fs.readdirSync(postsDirectory); const allFiles = fs.readdirSync(postsDirectory);
const postFolders = allFiles.filter(isValidPostDirectory);
return postFolders.map((slug) => ({ slug })); return postFolders.map((slug) => ({ slug }));
} }
type PostData = {
slug: string;
title: string;
description: string;
};
export default async function BlogPage() { 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( const posts = await Promise.all(
postFolders.map(async (slug) => { postFolders.map(async (slug): Promise<PostData | null> => {
try {
const { title, description } = await import(`./posts/${slug}/metadata.ts`); const { title, description } = await import(`./posts/${slug}/metadata.ts`);
return { slug, title, description }; 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 ( return (
<PageTransition>
<div> <div>
<div style={{paddingTop: "50px"}} className="relative flex justify-center items-center"> <div style={{paddingTop: "50px"}} className="relative flex justify-center items-center h-[50px]">
<div className="absolute left-0 ml-6"> <div className="absolute left-0 flex items-center h-full ml-6">
<Link href={"/"}> <BackButton />
<IoMdArrowBack size={30}/>
</Link>
</div> </div>
<span className="text-xl">Posts</span> <span className="text-xl">Posts</span>
</div> </div>
<div className="pt-[100px] md:w-[70%] xl:w-[50%] w-[85%] mx-auto"> <div className="pt-[100px] md:w-[70%] xl:w-[50%] w-[85%] mx-auto">
<ul> <ul>
{posts.map(({ slug, title, description }) => ( {validPosts.map(({ slug, title, description }) => (
<li key={slug} className="pb-4"> <li key={slug} className="pb-4">
<Link href={`/blog/posts/${slug}`}> <Link href={`/blog/posts/${slug}`}>
<span className="text-xl">{title}</span> <span className="text-xl">{title}</span>
@ -44,5 +71,6 @@ export default async function BlogPage() {
</ul> </ul>
</div> </div>
</div> </div>
</PageTransition>
); );
} }

View File

@ -1,2 +1,23 @@
import { Metadata } from "next";
// Post content
export const title = "# curl asherfalcon.com"; 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."; 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,
},
};
};

View File

@ -1,21 +1,24 @@
import Head from 'next/head'; "use client";
import { title, description } from './metadata'; import { title, description } from './metadata';
import Link from 'next/link'; import Link from 'next/link';
import { IoMdArrowBack } from 'react-icons/io';
import { LuExternalLink } from 'react-icons/lu'; 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() { export default function ExamplePost() {
// Update the title on the client side
useEffect(() => {
document.title = `${title} | Asher Falcon`;
}, []);
return ( return (
<> <PageTransition>
<Head> <div style={{paddingTop: "50px"}} className="relative flex justify-center items-center h-[50px]">
<title>{title}</title> <div className="absolute left-0 flex items-center h-full ml-6">
<meta name="description" content={description} /> <BackButton />
</Head>
<div style={{paddingTop: "50px"}} className="relative flex justify-center items-center">
<div className="absolute left-0 ml-6">
<Link href={"/blog"}>
<IoMdArrowBack size={30}/>
</Link>
</div> </div>
<span className="text-xl">{title}</span> <span className="text-xl">{title}</span>
</div> </div>
@ -30,7 +33,6 @@ export default function ExamplePost() {
<LuExternalLink /> <span className="">Contact</span> <LuExternalLink /> <span className="">Contact</span>
</Link> </Link>
</div> </div>
</PageTransition>
</>
); );
} }

View File

@ -1,2 +1,23 @@
import { Metadata } from "next";
// Post content
export const title = "docker deployment"; 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."; 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,
},
};
};

View File

@ -1,26 +1,53 @@
"use client" "use client"
import Head from 'next/head';
import { title, description } from './metadata'; import { title, description } from './metadata';
import Link from 'next/link'; import Link from 'next/link';
import { IoMdArrowBack } from 'react-icons/io'; import { PageTransition } from '@/components/PageTransition';
// import { LuExternalLink } from 'react-icons/lu'; import { useEffect } from 'react';
import BackButton from '@/components/BackButton';
import { script, basicScript, charm } from "./redeploy" import { script, basicScript, charm } from "./redeploy"
import CodeBlock from '../../../../components/code' 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() { 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 ( return (
<PageTransition>
<div className='pb-[20px]'> <div className='pb-[20px]'>
<Head> <div style={{paddingTop: "50px"}} className="relative flex justify-center items-center h-[50px]">
<title>{title}</title> <div className="absolute left-0 flex items-center h-full ml-6">
<meta name="description" content={description} /> <BackButton />
</Head>
<div style={{paddingTop: "50px"}} className="relative flex justify-center items-center">
<div className="absolute left-0 ml-6">
<Link href={"/blog"}>
<IoMdArrowBack size={30}/>
</Link>
</div> </div>
<span className="text-xl">{title}</span> <span className="text-xl">{title}</span>
</div> </div>
@ -54,7 +81,7 @@ export default function ExamplePost() {
<br/> <br/>
</div> </div>
<CodeBlock code={script} language="bash" /> <CodeBlock code={script} language="bash" />
</div> </div>
</PageTransition>
); );
} }

View File

@ -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"],
};

View File

@ -1,25 +1,52 @@
"use client" "use client"
import Link from "next/link"; 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 { FaLinkedin } from "react-icons/fa";
import { FaGithub } from "react-icons/fa"; import { FaGithub } from "react-icons/fa";
import { MdOutlineMailOutline } from "react-icons/md"; 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 (
<PageTransition>
<div> <div>
<div style={{paddingTop: "50px"}} className="relative flex justify-center items-center"> <div style={{ paddingTop: "50px" }} className="relative flex justify-center items-center h-[50px]">
<div className="absolute left-0 ml-6"> <div className="absolute left-0 flex items-center h-full ml-6">
<button onClick={() => {router.back()}}> <BackButton />
<IoMdArrowBack size={30}/>
</button>
</div> </div>
<span className="text-xl">Get in touch</span> <span className="text-xl">Get in touch</span>
</div> </div>
<div style={{paddingTop: "50px"}} className=""> <div style={{paddingTop: "50px"}} className="">
<div className="relative flex justify-center items-center"> <div className="relative flex justify-center items-center">
<Link href="https://www.linkedin.com/in/ashfn/" target="_blank" rel="noopener noreferrer" className="flex items-center space-x-1 "> <Link href="https://www.linkedin.com/in/ashfn/" target="_blank" rel="noopener noreferrer" className="flex items-center space-x-1 ">
@ -37,7 +64,7 @@ export default function BlogPage() {
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</PageTransition>
); );
} }

46
src/app/error.tsx Normal file
View File

@ -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 (
<PageTransition>
<div className="min-h-screen flex flex-col items-center justify-center px-4 text-center">
<h1 className="text-2xl mb-2">Something went wrong 😕</h1>
<p className="mb-12">An unexpected error has occurred.</p>
<div className="flex flex-col gap-6">
<button
onClick={reset}
className="hover:underline cursor-pointer"
>
Try again
</button>
<Link href="/" className="hover:underline">
Return to Home
</Link>
</div>
<p className="mt-12">
If this issue persists, please{' '}
<Link href="/contact" className="hover:underline">
contact me
</Link>
</p>
</div>
</PageTransition>
)
}

45
src/app/global-error.tsx Normal file
View File

@ -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 (
<html lang="en">
<body>
<div className="min-h-screen flex flex-col items-center justify-center px-4 text-center">
<h1 className="text-2xl mb-2">Something went wrong 🛠</h1>
<p className="mb-12">A critical error has occurred.</p>
<div className="flex flex-col gap-6">
<button
onClick={reset}
className="hover:underline cursor-pointer"
>
Try again
</button>
<Link href="/" className="hover:underline">
Return to Home
</Link>
</div>
<p className="mt-12">
If this issue persists, please send an email to:{' '}
<span className="font-mono">af [at] asherfalcon.com</span>
</p>
</div>
</body>
</html>
)
}

View File

@ -1,7 +1,7 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Open_Sans } from "next/font/google"; import { Open_Sans } from "next/font/google";
import "./globals.css"; import "./globals.css";
import Head from "next/head"; import ClientLayout from "@/components/ClientLayout";
const open_sans = Open_Sans({ const open_sans = Open_Sans({
weight: '500', weight: '500',
@ -10,8 +10,14 @@ const open_sans = Open_Sans({
}) })
export const metadata: Metadata = { export const metadata: Metadata = {
title: "asher falcon", title: {
description: "Generated by create next app", 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({ export default function RootLayout({
@ -21,11 +27,10 @@ export default function RootLayout({
}>) { }>) {
return ( return (
<html lang="en"> <html lang="en">
<Head>
<meta name="viewport" content="viewport-fit=cover" />
</Head>
<body className={`bg-ashwhite ${open_sans.className} `} style={{height: '100vh'}}> <body className={`bg-ashwhite ${open_sans.className} `} style={{height: '100vh'}}>
<ClientLayout>
{children} {children}
</ClientLayout>
<script defer src="https://cloud.umami.is/script.js" data-website-id="eaac838f-0bef-4455-a834-2e1455d78d8c"></script> <script defer src="https://cloud.umami.is/script.js" data-website-id="eaac838f-0bef-4455-a834-2e1455d78d8c"></script>
</body> </body>
</html> </html>

65
src/app/not-found.tsx Normal file
View File

@ -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 (
<PageTransition>
<div>
<div style={{ paddingTop: "50px" }} className="relative flex justify-center items-center h-[50px]">
<div className="absolute left-0 flex items-center h-full ml-6">
<BackButton />
</div>
<span className="text-xl"> 404 - Page not found</span>
</div>
<div style={{paddingTop: "50px"}} className="">
<div className="relative flex justify-center items-center">
<span>👀 Oops, this page doesn&apos;t exist</span>
</div>
<div className="relative flex justify-center items-center">
<div>
If you think this is an error, please contact me <Link href="/contact" className="text-blue-500 pl-4"> here</Link>
</div>
</div>
<div className="relative flex justify-center items-center">
<div>
Or go back to the <Link href="/" className="text-blue-500 pl-4"> home page</Link>
</div>
</div>
</div>
</div>
</PageTransition>
);
}

View File

@ -1,8 +1,16 @@
import Link from "next/link"; import Link from "next/link";
import { LuExternalLink } from "react-icons/lu"; 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() { export default function Home() {
return ( return (
<PageTransition>
<div className=""> <div className="">
<div className="flex flex-row items-center justify-center pt-[100px] md:w-[60%] xl:w-[40%] w-[85%] mx-auto"> <div className="flex flex-row items-center justify-center pt-[100px] md:w-[60%] xl:w-[40%] w-[85%] mx-auto">
<div> <div>
@ -10,9 +18,7 @@ export default function Home() {
Hi, I&apos;m Asher 👋 Hi, I&apos;m Asher 👋
</span> </span>
<p> <p>
I&apos;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. I&apos;m a Year 12 student with a strong interest in software engineering, problem-solving, and finance. I&apos;m currently studying Economics, Computer Science, Maths, and Chemistry. This site is where I share my projects, ideas, and what I&apos;m learning along the way.
This site is where I share my projects and ideas.
</p> </p>
</div> </div>
</div> </div>
@ -25,5 +31,6 @@ export default function Home() {
</Link> </Link>
</div> </div>
</div> </div>
</PageTransition>
); );
} }

View File

@ -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<string[]>([]);
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 (
<button
onClick={handleBack}
className="flex items-center justify-center p-0 m-0 bg-transparent border-0 cursor-pointer"
aria-label="Go back"
>
<IoMdArrowBack size={30} />
</button>
);
}

View File

@ -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 (
<TransitionProvider>
{children}
</TransitionProvider>
);
}

View File

@ -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 (
<motion.div
variants={variants}
initial="hidden"
animate="enter"
exit="exit"
transition={{ type: 'easeInOut', duration: 0.5 }}
>
{children}
</motion.div>
);
};

View File

@ -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 (
<AnimatePresence mode="wait">
<div key={pathname}>
{children}
</div>
</AnimatePresence>
);
};