feat: added transitions! (And error pages)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 2m54s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 2m54s
This commit is contained in:
parent
4c5e62f0b3
commit
f5c2085fe1
43
package-lock.json
generated
43
package-lock.json
generated
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -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>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
7
src/app/contact/metadata.ts
Normal file
7
src/app/contact/metadata.ts
Normal 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"],
|
||||||
|
};
|
@ -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
46
src/app/error.tsx
Normal 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
45
src/app/global-error.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -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
65
src/app/not-found.tsx
Normal 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'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>
|
||||||
|
);
|
||||||
|
}
|
@ -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'm Asher 👋
|
Hi, I'm Asher 👋
|
||||||
</span>
|
</span>
|
||||||
<p>
|
<p>
|
||||||
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.
|
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.
|
||||||
|
|
||||||
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
58
src/components/BackButton.tsx
Normal file
58
src/components/BackButton.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
16
src/components/ClientLayout.tsx
Normal file
16
src/components/ClientLayout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
28
src/components/PageTransition.tsx
Normal file
28
src/components/PageTransition.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
21
src/components/TransitionProvider.tsx
Normal file
21
src/components/TransitionProvider.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user