Oops probably should've committed, not even sure what's changed. Set up MDX, set up cv and print, set up dark mode, look at react query
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import million from "million/compiler";
|
||||
import createMDX from '@next/mdx'
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
|
||||
swcMinify: true,
|
||||
reactStrictMode: true,
|
||||
output: "standalone",
|
||||
@@ -11,4 +13,8 @@ const millionConfig = {
|
||||
auto: { rsc: true }, rsc: true
|
||||
}
|
||||
|
||||
export default million.next(nextConfig, millionConfig);
|
||||
const withMDX = createMDX({
|
||||
// Add markdown plugins here, as desired
|
||||
})
|
||||
|
||||
export default withMDX(million.next(nextConfig, millionConfig));
|
||||
|
||||
3313
package-lock.json
generated
3313
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -12,20 +12,24 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.1.3",
|
||||
"@mdx-js/loader": "^3.0.1",
|
||||
"@mdx-js/react": "^3.0.1",
|
||||
"@next/bundle-analyzer": "^14.2.2",
|
||||
"@next/mdx": "^14.2.3",
|
||||
"@tailwindcss/typography": "^0.5.12",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^20.12.7",
|
||||
"@types/react": "^18.2.79",
|
||||
"@types/react-dom": "^18.2.25",
|
||||
"@typescript-eslint/eslint-plugin": "^7.7.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-next": "14.2.2",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-next": "14.2.5",
|
||||
"exif-reader": "^2.0.1",
|
||||
"framer-motion": "^11.1.7",
|
||||
"glob": "^10.3.12",
|
||||
"glob": "^10.4.5",
|
||||
"million": "^3.0.6",
|
||||
"next": "14.2.2",
|
||||
"next": "^14.2.2",
|
||||
"next-auth": "^4.24.7",
|
||||
"postcss": "^8.4.38",
|
||||
"radash": "^12.1.0",
|
||||
|
||||
15
src/app/(root)/cv/page.tsx
Normal file
15
src/app/(root)/cv/page.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import Cv from '@/components/cv';
|
||||
|
||||
export default function CvPage(): React.JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<div className='flex flex-row justify-center pb-4'>
|
||||
<button className='py-2 px-4 border'>
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
<Cv/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
src/app/(root)/layout.tsx
Normal file
21
src/app/(root)/layout.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import "../globals.css";
|
||||
|
||||
import NavBar from '@/components/navbar';
|
||||
import Footer from '@/components/footer';
|
||||
import LogIn from "@/components/auth/login";
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>): React.JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<NavBar LogIn={<LogIn/>}/>
|
||||
<main className="px-6 py-4 w-full mx-auto flex-1 align-middle lg:max-w-5xl">
|
||||
{children}
|
||||
</main>
|
||||
<Footer/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import Sim from '@/components/sim';
|
||||
import HomeMdx from '@/markdown/page.mdx';
|
||||
|
||||
export default function Home(): React.JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Sim/>
|
||||
<HomeMdx/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
32
src/app/(root)/photos/page.tsx
Normal file
32
src/app/(root)/photos/page.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import Image from "next/image";
|
||||
import Lightbox from "@/components/lightbox";
|
||||
import { type GetPhotos } from "@/app/api/photos/route";
|
||||
|
||||
async function getImageData(): Promise<GetPhotos> {
|
||||
const res = await fetch(`http://localhost:3000/api/photos`, { next: { revalidate: false, tags: ['photos'] } });
|
||||
console.log(res);
|
||||
return res.json() as Promise<GetPhotos>;
|
||||
}
|
||||
|
||||
export default async function Photos(): Promise<React.JSX.Element> {
|
||||
const {data: imageData} = await getImageData();
|
||||
|
||||
return (
|
||||
<Lightbox imageData={imageData.images}>
|
||||
{imageData.images.map((image) => (
|
||||
<Image
|
||||
key={image.src}
|
||||
alt={image.src}
|
||||
src={image.src}
|
||||
className="object-contain h-60 w-80"
|
||||
sizes="100vw"
|
||||
loading="lazy"
|
||||
width={image.width}
|
||||
height={image.height}
|
||||
blurDataURL={image.blur}
|
||||
placeholder="blur"
|
||||
/>
|
||||
))}
|
||||
</Lightbox>
|
||||
);
|
||||
}
|
||||
32
src/app/(root)/posts/[...slug]/page.tsx
Normal file
32
src/app/(root)/posts/[...slug]/page.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { glob } from "glob";
|
||||
|
||||
// type postMdx = {
|
||||
// metadata: {
|
||||
// title: string,
|
||||
// date: string,
|
||||
// coverImage: string,
|
||||
// blurb: string,
|
||||
// shortBlurb: string,
|
||||
// tags: string[]
|
||||
// }
|
||||
// }
|
||||
|
||||
export async function generateStaticParams(): Promise<{slug: string[]}[]> {
|
||||
const posts = await glob(`src/markdown/posts/[...slug]/**/*.mdx`, {
|
||||
nodir: true,
|
||||
});
|
||||
|
||||
const slugs = posts.map((post) => ({
|
||||
slug: post.replace('src/markdown/posts/[...slug]/', '').replace(/\.mdx$/, '').split('/')
|
||||
}));
|
||||
|
||||
return slugs;
|
||||
}
|
||||
|
||||
export default function Post({params}: {params: { slug: string[] }}): React.JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{params.slug}
|
||||
</>
|
||||
);
|
||||
}
|
||||
9
src/app/(root)/posts/layout.tsx
Normal file
9
src/app/(root)/posts/layout.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Post({children}: {children: React.JSX.Element}): React.JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
7
src/app/(root)/posts/page.tsx
Normal file
7
src/app/(root)/posts/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function Posts(): React.JSX.Element {
|
||||
return (
|
||||
<>
|
||||
Actually this should be custom
|
||||
</>
|
||||
);
|
||||
}
|
||||
9
src/app/(unique)/cv/print/page.tsx
Normal file
9
src/app/(unique)/cv/print/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
import Cv from '@/components/cv';
|
||||
|
||||
export default function CvPrint(): React.JSX.Element {
|
||||
return (
|
||||
<Cv/>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
import Image from "next/image";
|
||||
import exifReader from "exif-reader";
|
||||
import { glob } from "glob";
|
||||
import sharp from 'sharp';
|
||||
import exifReader from 'exif-reader';
|
||||
import { pick } from 'radash';
|
||||
import Lightbox from "@/components/lightbox";
|
||||
import { NextResponse } from "next/server";
|
||||
import { pick } from "radash";
|
||||
import sharp from "sharp";
|
||||
|
||||
type ImageData = {
|
||||
export type ImageData = {
|
||||
width: number,
|
||||
height: number,
|
||||
blur: `data:image/${string}`,
|
||||
@@ -21,12 +20,19 @@ type ImageData = {
|
||||
}>
|
||||
}
|
||||
|
||||
export async function getImages(): Promise<{images: ImageData[]}> {
|
||||
export type GetPhotos = {
|
||||
status: number,
|
||||
data: {
|
||||
images: ImageData[]
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(): Promise<Response> {
|
||||
const photosGlob = await glob(`public/photos/**/*.{png,jpeg,jpg}`, {
|
||||
nodir: true,
|
||||
});
|
||||
|
||||
const images = photosGlob.map(async (fileName) => {
|
||||
const imageData = photosGlob.map(async (fileName: string) => {
|
||||
const { width, height, exif } = await sharp(fileName).metadata();
|
||||
const blur = await sharp(fileName)
|
||||
.resize({ width: 12, height: 12, fit: 'inside' })
|
||||
@@ -43,29 +49,7 @@ export async function getImages(): Promise<{images: ImageData[]}> {
|
||||
};
|
||||
});
|
||||
|
||||
return { images: await Promise.all(images) };
|
||||
}
|
||||
const images = await Promise.all(imageData);
|
||||
|
||||
export default async function Home(): Promise<React.JSX.Element> {
|
||||
const { images } = await getImages();
|
||||
|
||||
return (
|
||||
<Lightbox imageData={images}>
|
||||
{images.map((image) => (
|
||||
<div className="relative" key={image.src}>
|
||||
<Image
|
||||
alt={image.src}
|
||||
src={image.src}
|
||||
className="object-contain h-auto w-full"
|
||||
sizes="(min-width: 808px) 50vw, 100vw"
|
||||
loading="lazy"
|
||||
width={image.width}
|
||||
height={image.height}
|
||||
blurDataURL={image.blur}
|
||||
placeholder={image.blur}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Lightbox>
|
||||
);
|
||||
return NextResponse.json<GetPhotos>({ status: 200, data: { images } });
|
||||
}
|
||||
@@ -7,4 +7,3 @@
|
||||
text-wrap: balance;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,6 @@ import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
import NavBar from '@/components/navbar';
|
||||
import Footer from '@/components/footer';
|
||||
import LogIn from "@/components/auth/login";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-inter',
|
||||
@@ -22,13 +18,22 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>): React.JSX.Element {
|
||||
return (
|
||||
<html className={`${inter.variable} font-sans`} lang="en">
|
||||
<body className="min-h-screen flex flex-col bg-dracula-bg">
|
||||
<NavBar LogIn={<LogIn/>}/>
|
||||
<main className="px-6 py-4 w-full mx-auto flex-1 align-middle lg:max-w-5xl">
|
||||
{children}
|
||||
</main>
|
||||
<Footer/>
|
||||
// Use suppress hydration warnings to add the dark theme class on client
|
||||
<html className={`${inter.variable} font-sans`} lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<script id="SetTheme"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}`,
|
||||
}}>
|
||||
</script>
|
||||
</head>
|
||||
<body className="min-h-screen flex flex-col bg-dracula-bg-lightest dark:bg-dracula-bg print:white">
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -4,8 +4,8 @@ import { signIn } from "next-auth/react";
|
||||
|
||||
export function LoginButton(): React.JSX.Element {
|
||||
return (
|
||||
<button className="p-1 hover:bg-dracula-bglight rounded-3xl transition-colors group" onClick={() => void signIn('cognito')}>
|
||||
<UserCircleIcon className='stroke-dracula-cyan h-8 w-auto group-hover:stroke-dracula-orange transition-colors'/>
|
||||
<button className="p-1 dark:hover:bg-dracula-bglight rounded-3xl transition-colors group" onClick={() => void signIn('cognito')}>
|
||||
<UserCircleIcon className='dark:stroke-dracula-cyan h-8 w-auto dark:group-hover:stroke-dracula-orange transition-colors'/>
|
||||
<span className="sr-only">Log in</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -4,8 +4,8 @@ import { signOut } from "next-auth/react";
|
||||
|
||||
export function LogoutButton(): React.JSX.Element {
|
||||
return (
|
||||
<button className="p-1 hover:bg-dracula-bglight rounded-3xl transition-colors group" onClick={() => void signOut()}>
|
||||
<UserCircleIcon className='stroke-dracula-cyan h-8 w-auto group-hover:stroke-dracula-red transition-colors'/>
|
||||
<button className="p-1 dark:hover:bg-dracula-bglight rounded-3xl transition-colors group" onClick={() => void signOut()}>
|
||||
<UserCircleIcon className='dark:stroke-dracula-cyan h-8 w-auto dark:group-hover:stroke-dracula-red transition-colors'/>
|
||||
<span className="sr-only">Log out</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
123
src/components/cv.tsx
Normal file
123
src/components/cv.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import { PaperAirplaneIcon } from "@heroicons/react/24/outline";
|
||||
import React from "react";
|
||||
|
||||
type ExperienceContent = {
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
title: string,
|
||||
tech: string,
|
||||
company: string,
|
||||
content: string | React.JSX.Element
|
||||
}
|
||||
|
||||
function PrintBreak({count}: {count?: number}): React.JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{Array.from({length: count ?? 1}).map((_, i) => <br key={i} className="hidden print:block"/>)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const content: ExperienceContent[] = [
|
||||
{
|
||||
company: "Tes",
|
||||
endDate: "Present",
|
||||
startDate: "Feb 2023",
|
||||
tech: "TS/NodeJS/React/DotNet/AWS/K8s/GitOps",
|
||||
title: "Technical Lead",
|
||||
content: "As a technical lead, my role moved mostly into communicating with the wider business to ensure my team has clear, achievable objectives, then helping them release those objectives. I've been particularly involved with cross team collaboration to continue pushing improvements to our development process, being part of front end and back end guilds as well as having a constant input into our service architecture to set development wide architectural decisions. During this time I've worked in multiple tech stacks, needing to learn frameworks and languages to a high enough level of competence to help upskill my team within a short amount of time. Some of the projects I've led my team in have been: to rebuild of some of the most used pages, releasing web apps to millions of monthly users which included complexity such as searching, with filters and via a map along with user specific context while still scoring high SEO scores; the creation of a completely new email workflow, utilising multiple email senders, audit trails and handling bounces; and a genetic algorithm built to scale so users can track the progress in crunching complex sets of data."
|
||||
},
|
||||
{
|
||||
company: "Tes",
|
||||
endDate: "Feb 2023",
|
||||
startDate: "Jun 2022",
|
||||
tech: "TS/JS/NodeJS/React",
|
||||
title: "Senior Developer",
|
||||
content: "Changing fields into web development, I utilised my previous knowledge to pivot quickly into the technologies needed for full stack development. I have since worked on and improved many products across multiple teams, while using my experience to provide individual support within my team. I have also created internal initiatives to improve our developer experience as well as getting involved in architecture discussions to keep pushing our development forwards."
|
||||
},
|
||||
{
|
||||
company: "Live 5",
|
||||
endDate: "Jun 2022",
|
||||
startDate: "Oct 2019",
|
||||
tech: "TS/JS/WebGL/NodeJS",
|
||||
title: "Development Manager",
|
||||
content: <>
|
||||
As development manager, I oversaw all of the developers at Live 5 and had a responsibility to oversee production of over 20 games a year from my teams. I kept each stage of game development on track to meet both internal and external deadlines, able to work with my teams to either change the scope of the project or move developers to get the games back on target. By implementing a proper code review process, frequent stand ups and additional tooling for developers, qa and artists, we produced far more complex games in less time with fewer bugs. <PrintBreak count={3}/> In addition, I mentored both junior and senior members of my team to develop their technical skills, knowledge and soft skills. While managing the team was my foremost responsibility, I was still heavily involved with development. I tackled any particularly difficult coding problems for the team and architecture large-scale changes within the codebase. For example, I integrated new business vital services and rebuilt our base renderer and loading core in TypeScript. One of the more interesting projects I directed was to rebuild our backend, focusing on providing local and remote interfaces to the data generation that allowed for faster development of more reliable game backends. The deployment process was also rebuilt to allow deploying into AWS for browser game access, as a package for a separate serverless game build and to run a statistical analysis on a bare metal local kubernetes cluster which I also administered.
|
||||
</>
|
||||
},
|
||||
{
|
||||
company: "Live 5",
|
||||
endDate: "Oct 2019",
|
||||
startDate: "Sept 2018",
|
||||
tech: "TS/JS/WebGL/NodeJS",
|
||||
title: "Game Developer",
|
||||
content: "I was hired to continue as a C++ Developer, but soon transitioned to the mobile team due to company priorities. Despite having no prior experience with JavaScript, I quickly became proficient in the language and its ecosystem, which enabled me to promptly integrate into my new role.I started with creating games, but similar to my time at Inspired, I wrote extra scripts and improved the game libraries to assist development across the team."
|
||||
},
|
||||
{
|
||||
company: "Inspired Gaming",
|
||||
endDate: "Sept 2018",
|
||||
startDate: "Mar 2016",
|
||||
tech: "C++/DirectX/Python",
|
||||
title: "Engine/Game Developer",
|
||||
content: "My initial responsibilities involved converting existing games to work on a variety of hardware, though I quickly moved up to work on building some of the more complex games and tooling myself, before going on to mentor new starters.Soon after, I advanced into the game engine development team, which explored ways of improving the development cycle and coding efficiency for other developers. We improved the libraries, build steps and used middleware such as Conan and custom VS plugins to provide prebuilt binaries and improve cohesion and standards across the teams."
|
||||
}
|
||||
];
|
||||
|
||||
function Experience({content}: {content: ExperienceContent}): React.JSX.Element {
|
||||
return (
|
||||
<div className="flex flex-row gap-4 border-b-dracula-orange border-b last:border-b-0">
|
||||
<div className="w-20 justify-center text-center">
|
||||
{content.endDate}
|
||||
<br/>
|
||||
-
|
||||
<br/>
|
||||
{content.startDate}
|
||||
</div>
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex flex-row w-full pb-1 mb-1 border-b-[1px] border-dracula-bg-light">
|
||||
<div className="text-left">
|
||||
{content.title}
|
||||
</div>
|
||||
<div className="text-right flex-grow">
|
||||
{content.tech}
|
||||
</div>
|
||||
<div className="w-20 ml-3 text-right border-l-[1px] border-dracula-bg-light">
|
||||
{content.company}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-justify pb-2">
|
||||
{content.content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Cv(): React.JSX.Element {
|
||||
return (
|
||||
<div className='w-[20cm] print:pt-[0.5cm] mx-auto dark:text-white'>
|
||||
<div className='flex flex-col justify-center'>
|
||||
<h1 className='text-center py-1 text-2xl font-medium uppercase text-white bg-dracula-bg-light'>Joe Lewis Monk</h1>
|
||||
<div className="p-2 flex flex-col gap-2">
|
||||
<div className="grid grid-cols-3 border-b-2 border-dracula-bg-light pb-2">
|
||||
<span className="border-r-[1px] border-dracula-bg-light text-left">joemonk.co.uk</span>
|
||||
<span className="border-x-[1px] border-dracula-bg-light text-center">07757 017587</span>
|
||||
<span className="border-l-[1px] border-dracula-bg-light text-right">joemonk@hotmail.co.uk</span>
|
||||
</div>
|
||||
<p className="text-justify">
|
||||
As a highly motivated and adaptive developer, my enthusiasm for learning new technologies along with years of rapid game and web development has driven my proficiency with many languages and tools, allowing me to be flexible when tackling problems. Over the last few years I have enjoyed expanding my role to include management of multiple large teams, and have picked up new tech stacks while moving between roles.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-dracula-bg-light flex flex-row px-2 py-1 gap-2 text-white">
|
||||
<PaperAirplaneIcon className="h-5 my-[2px]"/>
|
||||
<h2 className="font-medium">Experience</h2>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 p-2">
|
||||
{content.map((expContent) => (
|
||||
<Experience content={expContent} key={`${expContent.company}_${expContent.title}`}/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
export default function NavBar(): React.JSX.Element {
|
||||
return (
|
||||
<footer className="bg-dracula-bg-darker border-t-2 border-dracula-purple">
|
||||
<footer className="dark:bg-dracula-bg-darker border-t-2 dark:border-dracula-purple">
|
||||
<div className="mx-auto max-w-7xl px-4">
|
||||
<div className="relative flex flex-row-reverse h-12 items-center justify-between">
|
||||
<span className='text-white select-none'>© Joe Monk 2024</span>
|
||||
<span className='dark:text-white select-none'>© Joe Monk 2024</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -15,28 +15,7 @@ import "yet-another-react-lightbox/styles.css";
|
||||
import "yet-another-react-lightbox/plugins/thumbnails.css";
|
||||
import "yet-another-react-lightbox/plugins/captions.css";
|
||||
|
||||
|
||||
type ImageData = {
|
||||
width: number,
|
||||
height: number,
|
||||
blur: `data:image/${string}`,
|
||||
src: string,
|
||||
camera?: string,
|
||||
exif: Partial<{
|
||||
ExposureBiasValue: number,
|
||||
FNumber: number,
|
||||
ISOSpeedRatings: number,
|
||||
FocalLength: number,
|
||||
DateTimeOriginal: Date,
|
||||
LensModel: string
|
||||
}>
|
||||
}
|
||||
|
||||
type LightboxProps = {
|
||||
children: React.JSX.Element[],
|
||||
imageData: ImageData[]
|
||||
}
|
||||
|
||||
import { type ImageData } from "@/app/api/photos/route";
|
||||
|
||||
function NextJsImage({ slide, offset, rect, unoptimized = false }: {slide: ImageData, offset: number, rect: {width: number, height: number}, unoptimized: boolean}): React.JSX.Element {
|
||||
const {
|
||||
@@ -60,8 +39,6 @@ function NextJsImage({ slide, offset, rect, unoptimized = false }: {slide: Image
|
||||
)
|
||||
: rect.height;
|
||||
|
||||
console.log(slide);
|
||||
|
||||
return (
|
||||
<div style={{ position: "relative", width, height }}>
|
||||
<Image
|
||||
@@ -72,7 +49,7 @@ function NextJsImage({ slide, offset, rect, unoptimized = false }: {slide: Image
|
||||
unoptimized={unoptimized}
|
||||
draggable={false}
|
||||
blurDataURL={slide.blur}
|
||||
placeholder={slide.blur}
|
||||
placeholder="blur"
|
||||
style={{
|
||||
objectFit: cover ? "cover" : "contain",
|
||||
cursor: click ? "pointer" : undefined,
|
||||
@@ -86,28 +63,31 @@ function NextJsImage({ slide, offset, rect, unoptimized = false }: {slide: Image
|
||||
);
|
||||
}
|
||||
|
||||
export default function MyLightbox(props: LightboxProps): React.JSX.Element {
|
||||
export default function MyLightbox({imageData, children}: {imageData: ImageData[], children: React.JSX.Element[]}): React.JSX.Element {
|
||||
const [active, setActive] = useState<number | null>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid gap-2 grid-cols-3">
|
||||
{props.children.map((image, index) => (
|
||||
<div className="mx-auto">
|
||||
<div className="flex flex-row flex-wrap">
|
||||
{children.map((image, index) => (
|
||||
<button key={`lightbox_img_${index}`} onClick={(() => {
|
||||
setActive(index);
|
||||
})}>
|
||||
{image}
|
||||
<div className="relative">
|
||||
{image}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<YARL
|
||||
open={!!active}
|
||||
open={typeof active === 'number'}
|
||||
close={() => setActive(null)}
|
||||
index={active ?? undefined}
|
||||
slides={props.imageData}
|
||||
slides={imageData}
|
||||
// @ts-expect-error - Todo - This just passes the slide through, but it doesn't know the type
|
||||
render={{ slide: (args) => NextJsImage({...args, unoptimized: true }), thumbnail: NextJsImage }}
|
||||
plugins={[Thumbnails, Zoom, Captions]}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,43 +1,55 @@
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { HomeModernIcon, Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline';
|
||||
import { AnimatePresence, m, LazyMotion, domAnimation } from "framer-motion";
|
||||
import ThemeSwitcher from './theme-switcher';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
const navigation = [
|
||||
{ name: 'Blog', href: '#', current: true },
|
||||
{ name: 'Projects', href: '#', current: false },
|
||||
const defaultNavigation = [
|
||||
{ name: 'Posts', href: '/posts', current: false },
|
||||
{ name: 'Projects', href: '/projects', current: false },
|
||||
{ name: 'Photos', href: '/photos', current: false },
|
||||
{ name: 'CV', href: '#', current: false },
|
||||
{ name: 'Contact', href: '#', current: false },
|
||||
{ name: 'CV', href: '/cv', current: false },
|
||||
{ name: 'Contact', href: '/contact', current: false },
|
||||
];
|
||||
|
||||
export default function NavBar({LogIn}: {LogIn: React.JSX.Element}): React.JSX.Element {
|
||||
const [open, setOpen] = useState(false);
|
||||
const pathname = usePathname();
|
||||
|
||||
const navigation = useMemo((): typeof defaultNavigation => {
|
||||
const nav = structuredClone(defaultNavigation);
|
||||
const current = nav.find((nav) => nav.href === pathname);
|
||||
if (current) {
|
||||
current.current = true;
|
||||
}
|
||||
return nav;
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<nav className="bg-dracula-bg-darker border-b-2 border-dracula-purple">
|
||||
<nav className="dark:bg-dracula-bg-darker border-b-2 dark:border-dracula-purple">
|
||||
<LazyMotion features={domAnimation}>
|
||||
<div className="mx-auto max-w-7xl px-4">
|
||||
<div className="relative flex h-16 items-center justify-between">
|
||||
<div className="flex">
|
||||
<button className='sm:hidden hover:bg-dracula-bglight transition-colors duration-100 rounded-sm p-1' onClick={() => setOpen(!open)}>
|
||||
<button className='sm:hidden dark:hover:bg-dracula-bglight transition-colors duration-100 rounded-sm p-1' onClick={() => setOpen(!open)}>
|
||||
{open ? (
|
||||
<XMarkIcon className='rounded-sm stroke-dracula-cyan h-8 w-auto'/>
|
||||
<XMarkIcon className='rounded-sm dark:stroke-dracula-cyan h-8 w-auto'/>
|
||||
) : (
|
||||
<Bars3Icon className='rounded-sm stroke-dracula-cyan h-8 w-auto'/>
|
||||
<Bars3Icon className='rounded-sm dark:stroke-dracula-cyan h-8 w-auto'/>
|
||||
)}
|
||||
</button>
|
||||
<Link className='hidden sm:flex items-center p-1 hover:bg-dracula-bglight transition-colors' href='/'>
|
||||
<HomeModernIcon className='stroke-dracula-cyan rounded-sm h-8 w-auto'/>
|
||||
<Link className='hidden sm:flex items-center p-1 dark:hover:bg-dracula-bglight transition-colors' href='/'>
|
||||
<HomeModernIcon className='dark:stroke-dracula-cyan rounded-sm h-8 w-auto'/>
|
||||
</Link>
|
||||
<div className='space-x-5 hidden sm:flex ml-10'>
|
||||
{navigation.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={`hover:bg-dracula-bglight transition-colors duration-100 text-white rounded-sm px-3 pt-2 pb-1.5 font-normal border-b-2 border-transparent ${
|
||||
item.current ? 'border-b-dracula-pink' : ''
|
||||
className={`dark:hover:bg-dracula-bglight transition-colors duration-100 dark:text-white rounded-sm px-3 pt-2 pb-1.5 font-normal border-b-2 border-transparent ${
|
||||
item.current ? 'dark:border-b-dracula-pink' : ''
|
||||
}`}
|
||||
aria-current={item.current ? 'page' : undefined}
|
||||
>
|
||||
@@ -46,9 +58,10 @@ export default function NavBar({LogIn}: {LogIn: React.JSX.Element}): React.JSX.E
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className='space-x-4'>
|
||||
<ThemeSwitcher/>
|
||||
{LogIn}
|
||||
</div>
|
||||
{LogIn}
|
||||
</div>
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
@@ -65,8 +78,8 @@ export default function NavBar({LogIn}: {LogIn: React.JSX.Element}): React.JSX.E
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={`hover:bg-dracula-bglight transition-colors duration-100 text-white px-2 py-2 font-normal border-l-4 border-transparent ${
|
||||
item.current ? 'border-l-dracula-pink' : ''
|
||||
className={`dark:hover:bg-dracula-bglight transition-colors duration-100 dark:text-white px-2 py-2 font-normal border-l-4 border-transparent ${
|
||||
item.current ? 'dark:border-l-dracula-pink' : ''
|
||||
}`}
|
||||
aria-current={item.current ? 'page' : undefined}
|
||||
>
|
||||
|
||||
@@ -99,7 +99,7 @@ export default function Sim(): React.JSX.Element {
|
||||
|
||||
return (
|
||||
<>
|
||||
<form className="text-white" onSubmit={onSubmit}>
|
||||
<form className="dark:text-white" onSubmit={onSubmit}>
|
||||
<label>Prizes</label>
|
||||
<br />
|
||||
<input className="text-black" type="text" name="prizes" />
|
||||
@@ -116,14 +116,14 @@ export default function Sim(): React.JSX.Element {
|
||||
<br />
|
||||
<input className="text-black" type="text" name="ticketsTotal" />
|
||||
<br />
|
||||
<button className="p-2 bg-dracula-bglighter rounded-sm" type="submit">
|
||||
<button className="p-2 dark:bg-dracula-bglighter rounded-sm" type="submit">
|
||||
Run
|
||||
</button>
|
||||
<br />
|
||||
</form>
|
||||
<br />
|
||||
{outputs.length ? (
|
||||
<table className={"text-white"}>
|
||||
<table className={"dark:text-white"}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Runs</th>
|
||||
|
||||
24
src/components/theme-switcher.tsx
Normal file
24
src/components/theme-switcher.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
import { MoonIcon, SunIcon } from "@heroicons/react/24/outline";
|
||||
import React from "react";
|
||||
|
||||
export default function ThemeSwitcher(): React.JSX.Element {
|
||||
const toggleTheme = function(): void {
|
||||
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.theme = 'light';
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.theme = 'dark';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button className="h-8 w-8" onClick={toggleTheme}>
|
||||
<MoonIcon className="dark:hidden block"/>
|
||||
<SunIcon className="hidden dark:block dark:stroke-dracula-cyan"/>
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
7
src/lib/current-url.ts
Normal file
7
src/lib/current-url.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export function getCurrentUrl(): string {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
return "https://joemonk.co.uk";
|
||||
} else {
|
||||
return "https://3000.vscode.home.joemonk.co.uk";
|
||||
}
|
||||
}
|
||||
33
src/lib/query-client.ts
Normal file
33
src/lib/query-client.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { QueryClient, defaultShouldDehydrateQuery } from '@tanstack/react-query';
|
||||
|
||||
function makeQueryClient(): QueryClient {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 60 * 1000,
|
||||
},
|
||||
dehydrate: {
|
||||
// include pending queries in dehydration
|
||||
shouldDehydrateQuery: (query) =>
|
||||
defaultShouldDehydrateQuery(query) ||
|
||||
query.state.status === 'pending',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let browserQueryClient: QueryClient | undefined = undefined;
|
||||
|
||||
export function getQueryClient(): QueryClient {
|
||||
if (typeof window === 'undefined') {
|
||||
// Server: always make a new query client
|
||||
return makeQueryClient();
|
||||
} else {
|
||||
// Browser: make a new query client if we don't already have one
|
||||
// This is very important, so we don't re-make a new client if React
|
||||
// suspends during the initial render. This may not be needed if we
|
||||
// have a suspense boundary BELOW the creation of the query client
|
||||
if (!browserQueryClient) browserQueryClient = makeQueryClient();
|
||||
return browserQueryClient;
|
||||
}
|
||||
}
|
||||
6
src/markdown/page.mdx
Normal file
6
src/markdown/page.mdx
Normal file
@@ -0,0 +1,6 @@
|
||||
A small personal site I use to practice and try things out. When I remember I'll use it to log interesting or difficult projects.
|
||||
|
||||
Built to try and use multiple modern web tecnologies in tandem to produce a great user experience from real data. Most parts have been built in a way in which I can swap it out with more interesting methods and projects.
|
||||
|
||||
I'm using Next.js with react query to make the pages load nicely and tailwindcss to make them look good easily. Page content is loaded from mdx files, including preview pages locked behind AWS Cognito. The thought being I'll offload them to load from a database instead of direct files, inserting via a wysiwyg editor behind the auth.
|
||||
Photos are currently loaded from the filesystem, the metadata and EXIF data is read and a small image created which are all passed back as part of the page. This allows the initial page to have a small blur image, correctly sorted, allowing the page to be loaded quickly. When the page is loaded, the images can then be lazy loaded and optimized to reduce the impact on the server and the data to the client. Opening a full image will then load an unoptimized version to allow detail to be viewed.
|
||||
1
src/markdown/posts/[...slug]/a-thing.mdx
Normal file
1
src/markdown/posts/[...slug]/a-thing.mdx
Normal file
@@ -0,0 +1 @@
|
||||
# HEADER
|
||||
22
src/markdown/posts/[...slug]/developer.mdx
Normal file
22
src/markdown/posts/[...slug]/developer.mdx
Normal file
@@ -0,0 +1,22 @@
|
||||
export const metadata = {
|
||||
title: "Being a Developer",
|
||||
date: "2020-05-12",
|
||||
path: "/posts/being-a-developer",
|
||||
coverImage: "../images/being-a-developer/being-a-developer.jpg",
|
||||
blurb: "My thoughts on being a \"developer\", being a \"programmer\" and the differences between them.",
|
||||
shortBlurb: "My thoughts on being a developer vs being a programmer.",
|
||||
tags: ["Blog", "Development"]
|
||||
}
|
||||
|
||||
So over the last few years, I've had plenty of discussions about "being a developer".
|
||||
|
||||
These conversations usually start from people saying "such and such is a good developer", because maybe they created an excellent interface or made a beautifully reusable class. Good code is excellent and people love talking about it. The conversation then usually steers somewhere towards "but they forgot about the product", overran the deadline or missed a visual issue. And I think that defines the difference in my mind between a programmer and a developer. I'd call that person a good programmer.
|
||||
|
||||
To me, a developer thinks about the product, a programmer thinks about the code itself. I'm not saying they don't have a real link - obviously if your product is code, then the code quality matters and will directly affect the product. But it's not the full picture.
|
||||
|
||||
I've worked in creating gambling games for 4 years now, and I certainly don't think I'm the best programmer. I do however like to think I'm a pretty good developer, I try think about the product, how the user will play through and see the game. This does sometimes mean I don't plan my code out the best, or do something today that'll fill the gap for the next couple of games, and will be written already marked for a re-write down the road.
|
||||
This does of course mean you end up doing more work in total, but I believe this can frequently be worth it to save the time up front or in this particular game to put more effort in elsewhere.
|
||||
|
||||
A good programmer can think about the codebase moving forwards, optimise it and keep it readable, they're the person you turn to when you need a code review scrutinised. I have a good friend that I'd lean more towards the programmer than the developer, he has a massive love for all things software but has a problem with over complicating things towards the "perfect code". I turn to him when I have a problem or interest in a certain tech.
|
||||
|
||||
I do think both are necessary and important for a good product, and both sides need to keep each other in check.
|
||||
0
src/markdown/posts/page.mdx
Normal file
0
src/markdown/posts/page.mdx
Normal file
15
src/mdx-components.tsx
Normal file
15
src/mdx-components.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { MDXComponents } from 'mdx/types';
|
||||
import React from 'react';
|
||||
|
||||
export function useMDXComponents(components: MDXComponents): MDXComponents {
|
||||
return {
|
||||
wrapper: ({children}: { children: React.JSX.Element[]}): React.JSX.Element => {
|
||||
return (
|
||||
<article className='prose prose-slate dark:prose-invert mx-auto'>
|
||||
{children}
|
||||
</article>
|
||||
);
|
||||
},
|
||||
...components,
|
||||
};
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
darkMode: 'selector',
|
||||
content: [
|
||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
@@ -15,6 +17,7 @@ const config: Config = {
|
||||
colors: {
|
||||
// Nicked from the vs code version of the theme https://github.com/dracula/visual-studio-code/blob/master/src/dracula.yml
|
||||
dracula: {
|
||||
'bg-lightest': '#F8F8F2',
|
||||
'bg-lighter': '#424450',
|
||||
'bg-light': '#343746',
|
||||
'bg': '#282A36',
|
||||
|
||||
Reference in New Issue
Block a user