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": {
"@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",

View File

@ -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",

View File

@ -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<PostData | null> => {
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 (
<div>
<div style={{paddingTop: "50px"}} className="relative flex justify-center items-center">
<div className="absolute left-0 ml-6">
<Link href={"/"}>
<IoMdArrowBack size={30}/>
</Link>
</div>
<span className="text-xl">Posts</span>
</div>
<div className="pt-[100px] md:w-[70%] xl:w-[50%] w-[85%] mx-auto">
<ul>
{posts.map(({ slug, title, description }) => (
<li key={slug} className="pb-4">
<Link href={`/blog/posts/${slug}`}>
<span className="text-xl">{title}</span>
<p className="line-clamp-2 blur-text">{description}</p>
</Link>
</li>
))}
</ul>
</div>
</div>
<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">Posts</span>
</div>
<div className="pt-[100px] md:w-[70%] xl:w-[50%] w-[85%] mx-auto">
<ul>
{validPosts.map(({ slug, title, description }) => (
<li key={slug} className="pb-4">
<Link href={`/blog/posts/${slug}`}>
<span className="text-xl">{title}</span>
<p className="line-clamp-2 blur-text">{description}</p>
</Link>
</li>
))}
</ul>
</div>
</div>
</PageTransition>
);
}

View File

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

View File

@ -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 (
<>
<Head>
<title>{title}</title>
<meta name="description" content={description} />
</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>
<PageTransition>
<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">{title}</span>
</div>
@ -30,7 +33,6 @@ export default function ExamplePost() {
<LuExternalLink /> <span className="">Contact</span>
</Link>
</div>
</>
</PageTransition>
);
}

View File

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

View File

@ -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 (
<div className='pb-[20px]'>
<Head>
<title>{title}</title>
<meta name="description" content={description} />
</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>
<span className="text-xl">{title}</span>
<PageTransition>
<div className='pb-[20px]'>
<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">{title}</span>
</div>
<div style={{paddingTop: "30px"}} className='flex justify-center items-center md:w-[70%] xl:w-[50%] w-[85%] mx-auto'>
<div>
<p>{description}</p>
</div>
</div>
<div style={{paddingTop: "30px"}} className='flex justify-center items-center md:w-[70%] xl:w-[50%] w-[85%] mx-auto'>
<div>
<p>{description}</p>
<div style={{paddingTop: "30px"}} className='flex flex-col justify-center items-center md:w-[70%] xl:w-[50%] w-[85%] mx-auto'>
<p>TLDR, if you just want to check out the script and how to use it, you can find it <Link className='text-blue-500' href={"https://git.asherfalcon.com/asher/compose-deploy"} target="_blank" rel="noopener noreferrer">here</Link></p>
<br/>
<p>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 <b>docker-compose.yml</b> 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. </p>
<br/>
<p>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.</p>
<br />
</div>
</div>
<div style={{paddingTop: "30px"}} className='flex flex-col justify-center items-center md:w-[70%] xl:w-[50%] w-[85%] mx-auto'>
<p>TLDR, if you just want to check out the script and how to use it, you can find it <Link className='text-blue-500' href={"https://git.asherfalcon.com/asher/compose-deploy"} target="_blank" rel="noopener noreferrer">here</Link></p>
<br/>
<p>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 <b>docker-compose.yml</b> 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. </p>
<br/>
<p>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.</p>
<br />
</div>
<CodeBlock code={basicScript} language="bash" />
<div style={{paddingTop: "30px"}} className='flex flex-col justify-center items-center md:w-[70%] xl:w-[50%] w-[85%] mx-auto'>
<p>So what&apos;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.</p>
<br/>
<p>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.</p>
<br />
<p>Now to make the commit history not look like groundhog day, and to add some commit messages, we can use <Link className='text-blue-500' href="https://github.com/charmbracelet/gum" target="_blank" rel="noopener noreferrer">Gum</Link>, 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.</p>
<br />
</div>
<CodeBlock code={charm} language="bash" />
<div style={{paddingTop: "30px"}} className='flex flex-col justify-center items-center md:w-[70%] xl:w-[50%] w-[85%] mx-auto'>
<p>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:</p>
<br/>
</div>
<CodeBlock code={script} language="bash" />
</div>
<CodeBlock code={basicScript} language="bash" />
<div style={{paddingTop: "30px"}} className='flex flex-col justify-center items-center md:w-[70%] xl:w-[50%] w-[85%] mx-auto'>
<p>So what&apos;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.</p>
<br/>
<p>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.</p>
<br />
<p>Now to make the commit history not look like groundhog day, and to add some commit messages, we can use <Link className='text-blue-500' href="https://github.com/charmbracelet/gum" target="_blank" rel="noopener noreferrer">Gum</Link>, 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.</p>
<br />
</div>
<CodeBlock code={charm} language="bash" />
<div style={{paddingTop: "30px"}} className='flex flex-col justify-center items-center md:w-[70%] xl:w-[50%] w-[85%] mx-auto'>
<p>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:</p>
<br/>
</div>
<CodeBlock code={script} language="bash" />
</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,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";
return (
<div>
<div style={{paddingTop: "50px"}} className="relative flex justify-center items-center">
<div className="absolute left-0 ml-6">
<button onClick={() => {router.back()}}>
<IoMdArrowBack size={30}/>
</button>
// 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 (
<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">Get in touch</span>
</div>
<div style={{paddingTop: "50px"}} className="">
<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 ">
<FaLinkedin /> <span className="">LinkedIn</span>
</Link>
</div>
<div className="relative flex justify-center items-center">
<Link href="https://github.com/ashfn" target="_blank" rel="noopener noreferrer" className="flex items-center space-x-1 ">
<FaGithub /> <span className=""> GitHub</span>
</Link>
</div>
<div className="relative flex justify-center items-center">
<div className="flex items-center space-x-1 ">
<MdOutlineMailOutline /> <span className=""> af [at] asherfalcon.com</span>
</div>
</div>
</div>
</div>
<div style={{paddingTop: "50px"}} className="">
<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 ">
<FaLinkedin /> <span className="">LinkedIn</span>
</Link>
</div>
<div className="relative flex justify-center items-center">
<Link href="https://github.com/ashfn" target="_blank" rel="noopener noreferrer" className="flex items-center space-x-1 ">
<FaGithub /> <span className=""> GitHub</span>
</Link>
</div>
<div className="relative flex justify-center items-center">
<div className="flex items-center space-x-1 ">
<MdOutlineMailOutline /> <span className=""> af [at] asherfalcon.com</span>
</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 { 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 (
<html lang="en">
<Head>
<meta name="viewport" content="viewport-fit=cover" />
</Head>
<body className={`bg-ashwhite ${open_sans.className} `} style={{height: '100vh'}}>
{children}
<ClientLayout>
{children}
</ClientLayout>
<script defer src="https://cloud.umami.is/script.js" data-website-id="eaac838f-0bef-4455-a834-2e1455d78d8c"></script>
</body>
</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,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 (
<div className="">
<div className="flex flex-row items-center justify-center pt-[100px] md:w-[60%] xl:w-[40%] w-[85%] mx-auto">
<div>
<span className="text-ashgray text-2xl">
Hi, I&apos;m Asher 👋
</span>
<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.
This site is where I share my projects and ideas.
</p>
<PageTransition>
<div className="">
<div className="flex flex-row items-center justify-center pt-[100px] md:w-[60%] xl:w-[40%] w-[85%] mx-auto">
<div>
<span className="text-ashgray text-2xl">
Hi, I&apos;m Asher 👋
</span>
<p>
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.
</p>
</div>
</div>
<div className="mt-[20] flex flex-row items-center justify-center space-x-4">
<Link href="/blog" className="flex items-center space-x-1 ">
<LuExternalLink /> <span className="">Blog</span>
</Link>
<Link href="/contact" className="flex items-center space-x-1 ">
<LuExternalLink /> <span className="">Contact</span>
</Link>
</div>
</div>
<div className="mt-[20] flex flex-row items-center justify-center space-x-4">
<Link href="/blog" className="flex items-center space-x-1 ">
<LuExternalLink /> <span className="">Blog</span>
</Link>
<Link href="/contact" className="flex items-center space-x-1 ">
<LuExternalLink /> <span className="">Contact</span>
</Link>
</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>
);
};