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": {
|
||||
"@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",
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -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'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'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>
|
||||
);
|
||||
}
|
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,43 +1,70 @@
|
||||
"use client"
|
||||
import Link from "next/link";
|
||||
import { IoMdArrowBack } from "react-icons/io";
|
||||
import { useRouter } from 'next/navigation' // Usage: App router
|
||||
import { FaLinkedin } from "react-icons/fa";
|
||||
import { FaGithub } from "react-icons/fa";
|
||||
import { MdOutlineMailOutline } from "react-icons/md";
|
||||
import { PageTransition } from "@/components/PageTransition";
|
||||
import { useEffect } from "react";
|
||||
import BackButton from "@/components/BackButton";
|
||||
|
||||
export default function BlogPage() {
|
||||
export default function ContactPage() {
|
||||
|
||||
const router = useRouter()
|
||||
// Set the title and metadata on the client side
|
||||
useEffect(() => {
|
||||
document.title = "Contact | Asher Falcon";
|
||||
|
||||
// Set metadata tags
|
||||
const metaDescription = document.querySelector('meta[name="description"]');
|
||||
if (metaDescription) {
|
||||
metaDescription.setAttribute('content', 'Get in touch with Asher Falcon - LinkedIn, GitHub, and email contact information');
|
||||
}
|
||||
|
||||
// Set Open Graph tags
|
||||
let metaOgTitle = document.querySelector('meta[property="og:title"]');
|
||||
if (!metaOgTitle) {
|
||||
metaOgTitle = document.createElement('meta');
|
||||
metaOgTitle.setAttribute('property', 'og:title');
|
||||
document.head.appendChild(metaOgTitle);
|
||||
}
|
||||
metaOgTitle.setAttribute('content', 'Contact | Asher Falcon');
|
||||
|
||||
let metaOgDesc = document.querySelector('meta[property="og:description"]');
|
||||
if (!metaOgDesc) {
|
||||
metaOgDesc = document.createElement('meta');
|
||||
metaOgDesc.setAttribute('property', 'og:description');
|
||||
document.head.appendChild(metaOgDesc);
|
||||
}
|
||||
metaOgDesc.setAttribute('content', 'Get in touch with Asher Falcon - LinkedIn, GitHub, and email contact information');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<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>
|
||||
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
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 { 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
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,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'm Asher 👋
|
||||
</span>
|
||||
<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.
|
||||
|
||||
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'm Asher 👋
|
||||
</span>
|
||||
<p>
|
||||
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.
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
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