├── .tool-versions ├── app ├── favicon.ico ├── robots.txt ├── opengraph-image.png ├── blog │ └── post │ │ ├── layout.tsx │ │ ├── just-read-user-friendly │ │ └── page.mdx │ │ ├── no-remote-early-in-my-career │ │ └── page.mdx │ │ ├── falling-out-of-love-with-twin-macro │ │ └── page.mdx │ │ └── please-do-an-accessibility-audit │ │ └── page.mdx ├── sitemap.ts ├── layout.tsx └── page.tsx ├── images ├── rw-og.png ├── My_Peep.png ├── big-burgh.png ├── vesper-og.png ├── Danny_Subs.png ├── Encore_PGH.jpg ├── GradeMyAid.png ├── grosseries.png ├── virtual-bar.png ├── CSN_Homepage.png ├── Food_Phantoms.png ├── grademyaid-old.png ├── hayden-ai-og.png ├── safe-opengraph.jpg ├── user-friendly.webp ├── GradeMyAid_Small.png ├── NYT_Critic_Picks.png ├── twin-macro-code.png ├── Belly_Button_Design.jpg ├── Pitt_CSC_Screenshot.png ├── Pollock_is_Shit_OG.png ├── alcohol101plus-og.jpg ├── alexander-grattan.jpg ├── resu_me_Screenshot.png ├── secret_pittsburgh.png ├── Autos2050_Screenshot.jpg ├── Big_Burgh_Screenshot.jpg ├── Conor_Lamb_Screenshot.jpg ├── Tiny_Jims_Smokehouse.png ├── WAVE-evaluation-tool.png ├── Allegory_Cave_Screenshot.jpg ├── Green_Pages_Screenshot.png ├── NYT_Critic_Picks_Small.png ├── Virtual_Safari_Snippet.png ├── resu_me_Screenshot_Small.png ├── secret_pittsburgh_small.png ├── Artificial_Unintelligence.png ├── Belly_Button_Design_Small.jpg ├── Pitt_CSC_Figma_Screenshot.png ├── Pitt_CSC_Screenshot_Small.png ├── Pollock_Is_Shit_Screenshot.png ├── Virtual_Safari_Screenshot.png ├── Conor_Lamb_Screenshot_Small.jpg ├── Grademyaid_Figma_Screenshot.png ├── Green_Pages_Figma_Screenshot.png ├── Green_Pages_Screenshot_Small.png ├── Off_World_Colonies_Inquirer.png ├── Original_CSC_Site_Screenshot.png ├── Todo_List_Website_Screenshot.png ├── dunning-kruger-accessibility.png ├── nytcriticpicks.netlify.app_.png ├── Allegory_Cave_Screenshot_Small.jpg └── Virtual_Safari_Screenshot_Small.png ├── styles ├── variables.module.scss ├── global.scss ├── intro-overlay.module.scss ├── common.module.scss ├── header.module.scss ├── post.module.scss ├── _reset.scss ├── project-listing.module.scss ├── project.module.scss └── home.module.scss ├── pnpm-workspace.yaml ├── next-env.d.ts ├── components ├── smooth-scroll.tsx ├── intro-overlay.tsx ├── header.tsx └── project-listing.tsx ├── eslint.config.mjs ├── next.config.mjs ├── .gitignore ├── tsconfig.json ├── mdx-components.tsx ├── README.md ├── package.json ├── CLAUDE.md └── utils ├── hooks └── use-ball-animation.tsx └── project-data.ts /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 18.17.1 -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /app/robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | Allow: / 3 | 4 | Sitemap: https://agrattan.com/sitemap.xml -------------------------------------------------------------------------------- /images/rw-og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/rw-og.png -------------------------------------------------------------------------------- /images/My_Peep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/My_Peep.png -------------------------------------------------------------------------------- /images/big-burgh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/big-burgh.png -------------------------------------------------------------------------------- /images/vesper-og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/vesper-og.png -------------------------------------------------------------------------------- /styles/variables.module.scss: -------------------------------------------------------------------------------- 1 | $black: #121212; 2 | $white: #fff; 3 | $font: var(--font-raleway); 4 | -------------------------------------------------------------------------------- /images/Danny_Subs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/Danny_Subs.png -------------------------------------------------------------------------------- /images/Encore_PGH.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/Encore_PGH.jpg -------------------------------------------------------------------------------- /images/GradeMyAid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/GradeMyAid.png -------------------------------------------------------------------------------- /images/grosseries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/grosseries.png -------------------------------------------------------------------------------- /images/virtual-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/virtual-bar.png -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | onlyBuiltDependencies: 2 | - "@parcel/watcher" 3 | - sharp 4 | - unrs-resolver 5 | -------------------------------------------------------------------------------- /app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/app/opengraph-image.png -------------------------------------------------------------------------------- /images/CSN_Homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/CSN_Homepage.png -------------------------------------------------------------------------------- /images/Food_Phantoms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/Food_Phantoms.png -------------------------------------------------------------------------------- /images/grademyaid-old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/grademyaid-old.png -------------------------------------------------------------------------------- /images/hayden-ai-og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/hayden-ai-og.png -------------------------------------------------------------------------------- /images/safe-opengraph.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/safe-opengraph.jpg -------------------------------------------------------------------------------- /images/user-friendly.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/user-friendly.webp -------------------------------------------------------------------------------- /images/GradeMyAid_Small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/GradeMyAid_Small.png -------------------------------------------------------------------------------- /images/NYT_Critic_Picks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/NYT_Critic_Picks.png -------------------------------------------------------------------------------- /images/twin-macro-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/twin-macro-code.png -------------------------------------------------------------------------------- /images/Belly_Button_Design.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/Belly_Button_Design.jpg -------------------------------------------------------------------------------- /images/Pitt_CSC_Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/Pitt_CSC_Screenshot.png -------------------------------------------------------------------------------- /images/Pollock_is_Shit_OG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/Pollock_is_Shit_OG.png -------------------------------------------------------------------------------- /images/alcohol101plus-og.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/alcohol101plus-og.jpg -------------------------------------------------------------------------------- /images/alexander-grattan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/alexander-grattan.jpg -------------------------------------------------------------------------------- /images/resu_me_Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/resu_me_Screenshot.png -------------------------------------------------------------------------------- /images/secret_pittsburgh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/secret_pittsburgh.png -------------------------------------------------------------------------------- /images/Autos2050_Screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/Autos2050_Screenshot.jpg -------------------------------------------------------------------------------- /images/Big_Burgh_Screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/Big_Burgh_Screenshot.jpg -------------------------------------------------------------------------------- /images/Conor_Lamb_Screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/Conor_Lamb_Screenshot.jpg -------------------------------------------------------------------------------- /images/Tiny_Jims_Smokehouse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/Tiny_Jims_Smokehouse.png -------------------------------------------------------------------------------- /images/WAVE-evaluation-tool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/WAVE-evaluation-tool.png -------------------------------------------------------------------------------- /images/Allegory_Cave_Screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/Allegory_Cave_Screenshot.jpg -------------------------------------------------------------------------------- /images/Green_Pages_Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/Green_Pages_Screenshot.png -------------------------------------------------------------------------------- /images/NYT_Critic_Picks_Small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/NYT_Critic_Picks_Small.png -------------------------------------------------------------------------------- /images/Virtual_Safari_Snippet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/Virtual_Safari_Snippet.png -------------------------------------------------------------------------------- /images/resu_me_Screenshot_Small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/resu_me_Screenshot_Small.png -------------------------------------------------------------------------------- /images/secret_pittsburgh_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/secret_pittsburgh_small.png -------------------------------------------------------------------------------- /images/Artificial_Unintelligence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/Artificial_Unintelligence.png -------------------------------------------------------------------------------- /images/Belly_Button_Design_Small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/Belly_Button_Design_Small.jpg -------------------------------------------------------------------------------- /images/Pitt_CSC_Figma_Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/Pitt_CSC_Figma_Screenshot.png -------------------------------------------------------------------------------- /images/Pitt_CSC_Screenshot_Small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/Pitt_CSC_Screenshot_Small.png -------------------------------------------------------------------------------- /images/Pollock_Is_Shit_Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/Pollock_Is_Shit_Screenshot.png -------------------------------------------------------------------------------- /images/Virtual_Safari_Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/Virtual_Safari_Screenshot.png -------------------------------------------------------------------------------- /images/Conor_Lamb_Screenshot_Small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/Conor_Lamb_Screenshot_Small.jpg -------------------------------------------------------------------------------- /images/Grademyaid_Figma_Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/Grademyaid_Figma_Screenshot.png -------------------------------------------------------------------------------- /images/Green_Pages_Figma_Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/Green_Pages_Figma_Screenshot.png -------------------------------------------------------------------------------- /images/Green_Pages_Screenshot_Small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/Green_Pages_Screenshot_Small.png -------------------------------------------------------------------------------- /images/Off_World_Colonies_Inquirer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/Off_World_Colonies_Inquirer.png -------------------------------------------------------------------------------- /images/Original_CSC_Site_Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/Original_CSC_Site_Screenshot.png -------------------------------------------------------------------------------- /images/Todo_List_Website_Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/Todo_List_Website_Screenshot.png -------------------------------------------------------------------------------- /images/dunning-kruger-accessibility.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/dunning-kruger-accessibility.png -------------------------------------------------------------------------------- /images/nytcriticpicks.netlify.app_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/nytcriticpicks.netlify.app_.png -------------------------------------------------------------------------------- /images/Allegory_Cave_Screenshot_Small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/Allegory_Cave_Screenshot_Small.jpg -------------------------------------------------------------------------------- /images/Virtual_Safari_Screenshot_Small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agrattan0820/portfolio-site/HEAD/images/Virtual_Safari_Screenshot_Small.png -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | import "./.next/types/routes.d.ts"; 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 7 | -------------------------------------------------------------------------------- /styles/global.scss: -------------------------------------------------------------------------------- 1 | @import "./reset"; 2 | 3 | body, 4 | html { 5 | padding: 0; 6 | margin: 0; 7 | box-sizing: border-box; 8 | } 9 | 10 | body { 11 | background: #121212; 12 | } 13 | 14 | * { 15 | &::selection { 16 | color: black; 17 | background-color: white; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /components/smooth-scroll.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ReactLenis } from "lenis/react"; 4 | 5 | export default function SmoothScroll({ 6 | children, 7 | }: { 8 | children: React.ReactNode; 9 | }) { 10 | return ( 11 | 12 | {children} 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig, globalIgnores } from "eslint/config"; 2 | import nextVitals from "eslint-config-next/core-web-vitals"; 3 | 4 | const eslintConfig = defineConfig([ 5 | ...nextVitals, 6 | // Override default ignores of eslint-config-next. 7 | globalIgnores([ 8 | // Default ignores of eslint-config-next: 9 | ".next/**", 10 | "out/**", 11 | "build/**", 12 | "next-env.d.ts", 13 | ]), 14 | ]); 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /app/blog/post/layout.tsx: -------------------------------------------------------------------------------- 1 | import Header from "../../../components/header"; 2 | 3 | import styles from "../../../styles/post.module.scss"; 4 | 5 | export default function PostLayout({ 6 | children, 7 | }: { 8 | children: React.ReactNode; 9 | }) { 10 | return ( 11 |
12 |
13 |
14 |
{children}
15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | import createMDX from "@next/mdx"; 2 | import rehypeShiki from "@shikijs/rehype"; 3 | 4 | /** @type {import('next').NextConfig} */ 5 | const nextConfig = { 6 | pageExtensions: ["js", "jsx", "mdx", "ts", "tsx"], 7 | }; 8 | 9 | const withMDX = createMDX({ 10 | options: { 11 | remarkPlugins: [], 12 | rehypePlugins: [ 13 | [ 14 | rehypeShiki, 15 | { 16 | theme: "one-dark-pro", 17 | }, 18 | ], 19 | ], 20 | }, 21 | }); 22 | 23 | export default withMDX(nextConfig); 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /styles/intro-overlay.module.scss: -------------------------------------------------------------------------------- 1 | @import "./variables.module.scss"; 2 | 3 | .introOverlay { 4 | background: $white; 5 | position: fixed; 6 | min-height: 100dvh; 7 | width: 100%; 8 | z-index: 3; 9 | .ball { 10 | position: absolute; 11 | top: -50vh; 12 | left: 50%; 13 | transform: translate(-50%, -50%); 14 | border-radius: 50%; 15 | width: 50px; 16 | height: 50px; 17 | background: $black; 18 | margin: 0 auto; 19 | @media (min-width: 768px) { 20 | width: 100px; 21 | height: 100px; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import { MetadataRoute } from "next"; 2 | 3 | export default function sitemap(): MetadataRoute.Sitemap { 4 | return [ 5 | { 6 | url: "https://agrattan.com", 7 | lastModified: new Date(), 8 | priority: 1, 9 | }, 10 | { 11 | url: "https://www.agrattan.com/blog/post/please-do-an-accessibility-audit", 12 | lastModified: new Date(), 13 | priority: 0.8, 14 | }, 15 | { 16 | url: "https://www.agrattan.com/blog/post/no-remote-early-in-my-career", 17 | lastModified: new Date(), 18 | priority: 0.8, 19 | }, 20 | { 21 | url: "https://www.agrattan.com/blog/post/falling-out-of-love-with-twin-macro", 22 | lastModified: new Date(), 23 | priority: 0.8, 24 | }, 25 | ]; 26 | } 27 | -------------------------------------------------------------------------------- /components/intro-overlay.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useSearchParams } from "next/navigation"; 4 | import { useBallAnimation } from "../utils/hooks/use-ball-animation"; 5 | import { useState } from "react"; 6 | 7 | import styles from "../styles/intro-overlay.module.scss"; 8 | 9 | export default function IntroOverlay() { 10 | const [animationComplete, setAnimationComplete] = useState(false); 11 | const searchParams = useSearchParams(); 12 | const back = searchParams?.get("back"); 13 | 14 | useBallAnimation({ 15 | enabled: back !== "true", 16 | onComplete: () => { 17 | setAnimationComplete(true); 18 | }, 19 | }); 20 | 21 | if (animationComplete) { 22 | return null; 23 | } 24 | 25 | return ( 26 |
27 |
28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "bundler", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "react-jsx", 20 | "incremental": true, 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ], 26 | "strictNullChecks": true 27 | }, 28 | "include": [ 29 | "next-env.d.ts", 30 | "**/*.ts", 31 | "**/*.tsx", 32 | ".next/types/**/*.ts", 33 | ".next/dev/types/**/*.ts" 34 | ], 35 | "exclude": [ 36 | "node_modules" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /mdx-components.tsx: -------------------------------------------------------------------------------- 1 | import Image, { ImageProps } from "next/image"; 2 | import type { MDXComponents } from "mdx/types"; 3 | 4 | import commonStyles from "./styles/common.module.scss"; 5 | import postStyles from "./styles/post.module.scss"; 6 | 7 | export function useMDXComponents(components: MDXComponents): MDXComponents { 8 | return { 9 | ...components, 10 | a: ({ children, ...props }) => ( 11 | 12 | {children} 13 | 14 | ), 15 | Image: (props) => ( 16 | {props.alt 17 | ), 18 | img: (props) => {props.alt, 19 | figure: ({ children, ...props }) => ( 20 |
21 |
{children}
22 |
23 | ), 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/global.scss"; 2 | import { Analytics } from "@vercel/analytics/next"; 3 | import { Metadata } from "next"; 4 | import { Raleway } from "next/font/google"; 5 | import SmoothScroll from "../components/smooth-scroll"; 6 | 7 | export const metadata: Metadata = { 8 | title: "Alexander Grattan", 9 | description: 10 | "Alexander's blog and portfolio, following his journeys as a software developer in Pittsburgh, PA", 11 | }; 12 | 13 | const raleway = Raleway({ 14 | subsets: ["latin"], 15 | display: "swap", 16 | variable: "--font-raleway", 17 | }); 18 | 19 | export default function RootLayout({ 20 | children, 21 | }: { 22 | children: React.ReactNode; 23 | }) { 24 | return ( 25 | 26 | 27 | {children} 28 | 29 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alexander Grattan Portfolio Site 2 | 3 | Showcases my work and the experience I have in web development. 4 | 5 | **Link to live verison [here](https://agrattan.com/)** 6 | 7 | ## Getting Started 8 | 9 | First, run the development server: 10 | 11 | ```bash 12 | npm run dev 13 | # or 14 | yarn dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 20 | 21 | ## Tools Used 22 | 23 | - Next.js 24 | - GSAP 25 | - Framer Motion 26 | - Scss 27 | 28 | ## Contributing 29 | 30 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 31 | 32 | ## License 33 | 34 | [MIT](https://opensource.org/licenses/MIT) 35 | 36 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 37 | -------------------------------------------------------------------------------- /styles/common.module.scss: -------------------------------------------------------------------------------- 1 | .hiddenText { 2 | // visible-hidden class from A11y-101 3 | clip: rect(1px, 1px, 1px, 1px); 4 | height: 1px; 5 | overflow: hidden; 6 | position: absolute; 7 | white-space: nowrap; 8 | width: 1px; 9 | &:focus { 10 | clip: auto; 11 | height: auto; 12 | overflow: auto; 13 | position: absolute; 14 | width: auto; 15 | } 16 | } 17 | 18 | .playful { 19 | transition: all 0.4s cubic-bezier(0.15, 0.75, 0.45, 0.95) 0s; 20 | animation: 10s ease 0s infinite alternate none running rainbow; 21 | } 22 | 23 | .playfulHover { 24 | transition: all 0.4s cubic-bezier(0.15, 0.75, 0.45, 0.95) 0s; 25 | &:hover { 26 | animation: 10s ease 0s infinite alternate none running rainbow; 27 | } 28 | &:focus { 29 | animation: 10s ease 0s infinite alternate none running rainbow; 30 | } 31 | } 32 | 33 | // Rainbow text 34 | @keyframes rainbow { 35 | 0% { 36 | color: rgb(240, 77, 255); 37 | } 38 | 25% { 39 | color: rgb(255, 77, 77); 40 | } 41 | 50% { 42 | color: rgb(255, 197, 77); 43 | } 44 | 75% { 45 | color: rgb(106, 237, 118); 46 | } 47 | 100% { 48 | color: rgb(0, 153, 255); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "portfolio-site", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build --webpack", 8 | "start": "next start", 9 | "lint": "eslint .", 10 | "format": "prettier --write .", 11 | "prettier": "prettier --check ." 12 | }, 13 | "dependencies": { 14 | "@gsap/react": "^2.1.2", 15 | "@mdx-js/loader": "^3.1.1", 16 | "@mdx-js/react": "^3.1.1", 17 | "@next/mdx": "^16.0.7", 18 | "@vercel/analytics": "^1.6.1", 19 | "eslint": "^9.39.1", 20 | "eslint-config-next": "^16.0.7", 21 | "framer-motion": "^12.23.25", 22 | "gsap": "^3.13.0", 23 | "lenis": "^1.3.15", 24 | "next": "^16.0.7", 25 | "react": "^19.2.1", 26 | "react-dom": "^19.2.1", 27 | "react-icons": "^5.5.0", 28 | "sass": "^1.94.2", 29 | "sharp": "^0.34.5" 30 | }, 31 | "devDependencies": { 32 | "@shikijs/rehype": "^3.19.0", 33 | "@types/mdx": "^2.0.13", 34 | "@types/node": "^24.10.1", 35 | "@types/react": "^19.2.7", 36 | "prettier": "^3.7.4", 37 | "shiki": "^3.19.0", 38 | "typescript": "^5.9.3" 39 | }, 40 | "packageManager": "pnpm@10.24.0" 41 | } 42 | -------------------------------------------------------------------------------- /components/header.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { FaGithub, FaLinkedin } from "react-icons/fa"; 3 | 4 | import styles from "../styles/header.module.scss"; 5 | import commonStyles from "../styles/common.module.scss"; 6 | 7 | type HeaderProps = { 8 | logoLink: string; 9 | }; 10 | 11 | export default function Header({ logoLink }: HeaderProps) { 12 | return ( 13 |
14 | 44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /styles/header.module.scss: -------------------------------------------------------------------------------- 1 | @import "./variables.module.scss"; 2 | 3 | .nav { 4 | display: flex; 5 | align-items: center; 6 | height: 10vh; 7 | width: 90%; 8 | max-width: 1536px; 9 | margin: 0 auto; 10 | font-family: $font; 11 | color: $white; 12 | font-weight: 700; 13 | z-index: 2; 14 | @media (min-width: 768px) { 15 | width: 75%; 16 | } 17 | .spaceBetween { 18 | width: 100%; 19 | display: flex; 20 | justify-content: space-between; 21 | align-items: center; 22 | } 23 | .logo { 24 | color: $white; 25 | cursor: pointer; 26 | font-size: 1.4rem; 27 | text-decoration: none; 28 | user-select: none; 29 | } 30 | .navList { 31 | margin: 0; 32 | padding: 0; 33 | list-style-type: none; 34 | display: flex; 35 | justify-content: center; 36 | align-items: center; 37 | gap: 1rem; 38 | li { 39 | display: flex; 40 | justify-content: center; 41 | align-items: center; 42 | padding: 0 8px; 43 | @media (min-width: 768px) { 44 | padding: 0 16px; 45 | } 46 | &:last-child { 47 | padding: 0 0 0 8px; 48 | @media (min-width: 768px) { 49 | padding: 0 16px; 50 | } 51 | } 52 | 53 | a { 54 | display: flex; 55 | justify-content: center; 56 | align-items: center; 57 | gap: 0.5rem; 58 | font-family: $font; 59 | color: $white; 60 | text-decoration: none; 61 | font-weight: 700; 62 | font-size: 0.875rem; 63 | text-underline-offset: 0.25rem; 64 | @media (min-width: 768px) { 65 | font-size: 1rem; 66 | } 67 | &:hover { 68 | text-decoration: underline; 69 | } 70 | &:focus { 71 | text-decoration: underline; 72 | } 73 | 74 | svg { 75 | font-size: 1.75rem; 76 | } 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Commands 6 | 7 | ```bash 8 | pnpm dev # Start development server at localhost:3000 9 | pnpm build # Production build 10 | pnpm lint # Run ESLint 11 | ``` 12 | 13 | ## Architecture 14 | 15 | This is a Next.js 16 portfolio site using the App Router with React 19. It uses pnpm as the package manager. 16 | 17 | ### Key Technologies 18 | 19 | - **Styling**: SCSS modules (`.module.scss` files in `styles/`) 20 | - **Animations**: GSAP with ScrollTrigger for intro and scroll-based animations 21 | - **Smooth Scrolling**: Lenis library wrapping the app 22 | - **Blog**: MDX files in `app/blog/post/[slug]/page.mdx` with Shiki syntax highlighting 23 | 24 | ### Project Structure 25 | 26 | - `app/` - App Router pages and layouts 27 | - `page.tsx` - Homepage with hero, about, blog preview, and projects sections 28 | - `blog/post/` - MDX blog posts with shared layout 29 | - `components/` - React components 30 | - `intro-overlay.tsx` - Initial ball animation overlay (client component) 31 | - `smooth-scroll.tsx` - Lenis smooth scroll wrapper (client component) 32 | - `utils/` 33 | - `project-data.ts` - Portfolio project definitions with images 34 | - `hooks/use-ball-animation.tsx` - GSAP intro animation hook with responsive breakpoints and reduced motion support 35 | - `styles/` - SCSS modules and global styles 36 | 37 | ### Animation System 38 | 39 | The homepage uses a GSAP-powered intro animation (`use-ball-animation.tsx`) that: 40 | 41 | - Bounces a ball element, then scales it to reveal content 42 | - Uses `gsap.matchMedia()` for responsive behavior (768px breakpoint) 43 | - Respects `prefers-reduced-motion` 44 | - ScrollTrigger animations for projects and footer 45 | - Skip animation with `?back=true` query param (used when navigating back from blog posts) 46 | 47 | ### MDX Configuration 48 | 49 | MDX is configured in `next.config.mjs` with: 50 | 51 | - Shiki rehype plugin for syntax highlighting (one-dark-pro theme) 52 | - Custom components in `mdx-components.tsx` for links, images, and code blocks 53 | -------------------------------------------------------------------------------- /app/blog/post/just-read-user-friendly/page.mdx: -------------------------------------------------------------------------------- 1 | import UserFriendlyBookCover from "../../../../images/user-friendly.webp"; 2 | 3 | export const metadata = { 4 | title: "I Just Read User Friendly by Cliff Kuang and Robert Fabricant", 5 | author: "Alexander Grattan", 6 | }; 7 | 8 | # I Just Read User Friendly by Cliff Kuang and Robert Fabricant 9 | 10 | And I loved it. This [book](https://www.goodreads.com/en/book/show/41940285) is no essay, but a compilation of well crafted stories that together explain the pervasiveness of user friendliness as an ideal in our technology-rich world. 11 | 12 |
13 | The book cover of User Friendly: How the Hidden Rules of Design Are Changing the Way We Live, Work, and Play by Cliff Kuang and Robert Fabricant 17 |
18 | The cover of User Friendly: How the Hidden Rules of Design Are Changing the 19 | Way We Live, Work, and Play by Cliff Kuang and Robert Fabricant 20 |
21 |
22 | 23 | Kuang and Fabricant begin at an electrifying and tense point in history by first focusing on the crisis at [Three Mile Island](https://en.wikipedia.org/wiki/Three_Mile_Island_accident) where an accident nearly led to a nuclear meltdown at a US nuclear power plant. Due to poor system feedback and a confusing layout for controls, workers were unable to accurately determine the system's state and failed to unearth the real issue behind the plant's problems. The authors later reveal this event to be the impetus for some UX figures to dedicate their career to solving usability issues. One such figure is Don Norman, who wrote another one of my favorite books: _The Design of Everday Things_. 24 | 25 | Rather than focusing purely on the present and recent past, I appreciated Kuang and Fabricant's insistance on presenting the history of "user friendliness" in totality, explaining aspects of Three Mile Island I had no knowledge of, and telling stories like of Henry Dreyfuss. He was a pioneering industrial designer during the Great Depression who dedicated his life to improving experiences using household items like washing machines, peanut butter jars, and thermostats, long before user experience became a field. 26 | 27 | In my opinion, this is an easy, automatic recommendation to anyone who works in tech, especially those that contribute directly to crafting the experience of end users. 28 | -------------------------------------------------------------------------------- /app/blog/post/no-remote-early-in-my-career/page.mdx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "Why I Don't Want to Work Remotely Early in My Career", 3 | author: "Alexander Grattan", 4 | }; 5 | 6 | # Why I Don't Want to Work Remotely Early in My Career 7 | 8 | As of recently, there's been a big push for return-to-office policies among companies (even [Zoom](https://www.washingtonpost.com/technology/2023/10/05/zoom-return-to-office-hybrid-remote-work/) of all people!), and I'm generally not opposed to it as a young person. I appreciate the flexibility of being remote my job as a software developer affords me, but the experience of being in the office among my coworkers and hashing out ideas beats any similar action I would instead have in a remote fashion. Beyond communicating more efficiently (I peep my head over my monitor to talk to my project manager), I feel more connected with my peers and overall less lonely. 9 | 10 | I am extremely grateful that I am in a position in which I can afford to rent and live alone in an apartment in a major US city. Though, with the passing of my mother to cancer this past year, living alone can feel empty, sometimes oppressively disheartening. I've fallen into bad habits following her death, which only became exacerbated when I was alone. Only in recent months have I learned to move away from these bad habits through reading and writing. 11 | 12 | One digital publication that I read and have been a big fan of is [The Pudding](https://pudding.cool/), a digital publication that focuses on creating interactive data visualizations and visual essays, for some time now. One piece of theirs that struck me this past year was ["24 hours in an invisible epidemic" by Alvin Chang](https://pudding.cool/2023/09/invisible-epidemic/). As outlined in the essay, there is evidence of a "invisible epidemic" of loneliness occuring in the USA. Statiscially, we have been spending less time with friends and less time with family, with an influx in time spent alone. 13 | 14 | Reading this article inspired me to seek more time with my friends and family while showing them as much love and appreciation for our relationship, and it's a goal I continue to take with me into 2024. 15 | 16 | There might come a time when I will need to be remote full-time, such as when kids enter the picture. As a young person, at this current stage of my life, however, I am beyond grateful for the relationships I've made thus far in my life and I want to cherish the time I can spend in-person with others as much as possible. 17 | -------------------------------------------------------------------------------- /components/project-listing.tsx: -------------------------------------------------------------------------------- 1 | import { getImageProps } from "next/image"; 2 | 3 | import styles from "../styles/project-listing.module.scss"; 4 | import commonStyles from "../styles/common.module.scss"; 5 | 6 | import { ProjectType } from "../utils/project-data"; 7 | 8 | type ProjectListingProps = { 9 | project: ProjectType; 10 | }; 11 | 12 | export default function ProjectListing({ project }: ProjectListingProps) { 13 | const { slug, image, mobileImage, name, description, link, code, type } = 14 | project; 15 | 16 | const { 17 | props: { srcSet: mobileSrc }, 18 | } = getImageProps({ alt: name, src: mobileImage ?? "" }); 19 | const { 20 | props: { src: imageSrc, ...rest }, 21 | } = getImageProps({ alt: name, src: image }); 22 | 23 | return ( 24 |
25 |
26 | 30 | 31 | {mobileSrc && ( 32 | 33 | )} 34 | {name} 40 | 41 | 42 |
43 | 47 |

{name}

48 |
49 | {description.split("\n").map((str, index) => ( 50 |

{str}

51 | ))} 52 |

{type}

53 |
54 | {link && ( 55 | 60 | Open Site 61 | 62 | )} 63 | {code && ( 64 | 69 | View Code 70 | 71 | )} 72 |
73 |
74 |
75 |
76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /styles/post.module.scss: -------------------------------------------------------------------------------- 1 | @import "./variables.module.scss"; 2 | 3 | .postContainer { 4 | color: $white; 5 | font-family: $font; 6 | min-height: 100dvh; 7 | .postMain { 8 | width: 90%; 9 | margin: 4rem auto 8rem; 10 | @media (min-width: 768px) { 11 | width: 75%; 12 | } 13 | 14 | .textContent { 15 | display: flex; 16 | justify-content: flex-start; 17 | align-items: flex-start; 18 | flex-direction: column; 19 | gap: 0.5rem; 20 | overflow-x: clip; 21 | 22 | h1 { 23 | font-size: 2rem; 24 | font-weight: bold; 25 | margin: 2rem 0; 26 | @media (min-width: 1280px) { 27 | font-size: 3rem; 28 | } 29 | } 30 | 31 | h2 { 32 | font-size: 1.5rem; 33 | font-weight: bold; 34 | margin-top: 2rem; 35 | margin-bottom: 0.5rem; 36 | @media (min-width: 1280px) { 37 | font-size: 2rem; 38 | } 39 | } 40 | 41 | p { 42 | max-width: 65ch; 43 | line-height: 1.625; 44 | font-size: 1rem; 45 | margin: 1rem 0; 46 | width: 100%; 47 | @media (min-width: 768px) { 48 | font-size: 1.25rem; 49 | } 50 | } 51 | 52 | figcaption { 53 | p { 54 | font-size: 0.75rem; 55 | font-style: italic; 56 | @media (min-width: 768px) { 57 | font-size: 1rem; 58 | } 59 | } 60 | } 61 | 62 | img { 63 | max-width: 65ch; 64 | width: 100%; 65 | height: auto; 66 | background-size: cover; 67 | background-repeat: no-repeat; 68 | vertical-align: middle; 69 | } 70 | 71 | a { 72 | color: silver; 73 | } 74 | 75 | em { 76 | font-style: italic; 77 | } 78 | 79 | ul { 80 | list-style-type: disc; 81 | padding-left: 1rem; 82 | line-height: 1.625; 83 | } 84 | 85 | code { 86 | font-family: monospace; 87 | font-size: 0.75rem; 88 | line-height: 1.25; 89 | @media (min-width: 768px) { 90 | font-size: 0.875rem; 91 | } 92 | } 93 | 94 | pre { 95 | code { 96 | span { 97 | font-size: 0.75rem; 98 | @media (min-width: 768px) { 99 | font-size: 0.875rem; 100 | } 101 | } 102 | } 103 | } 104 | } 105 | } 106 | } 107 | 108 | .codeContainer { 109 | background-color: rgb(40, 44, 52); 110 | overflow-x: auto; 111 | border-radius: 8px; 112 | padding: 1rem; 113 | max-width: 65ch; 114 | width: 100%; 115 | } 116 | -------------------------------------------------------------------------------- /styles/_reset.scss: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, 7 | body, 8 | div, 9 | span, 10 | applet, 11 | object, 12 | iframe, 13 | h1, 14 | h2, 15 | h3, 16 | h4, 17 | h5, 18 | h6, 19 | p, 20 | blockquote, 21 | pre, 22 | a, 23 | abbr, 24 | acronym, 25 | address, 26 | big, 27 | cite, 28 | code, 29 | del, 30 | dfn, 31 | em, 32 | img, 33 | ins, 34 | kbd, 35 | q, 36 | s, 37 | samp, 38 | small, 39 | strike, 40 | strong, 41 | sub, 42 | sup, 43 | tt, 44 | var, 45 | b, 46 | u, 47 | i, 48 | center, 49 | dl, 50 | dt, 51 | dd, 52 | ol, 53 | ul, 54 | li, 55 | fieldset, 56 | form, 57 | label, 58 | legend, 59 | table, 60 | caption, 61 | tbody, 62 | tfoot, 63 | thead, 64 | tr, 65 | th, 66 | td, 67 | article, 68 | aside, 69 | canvas, 70 | details, 71 | embed, 72 | figure, 73 | figcaption, 74 | footer, 75 | header, 76 | hgroup, 77 | menu, 78 | nav, 79 | output, 80 | ruby, 81 | section, 82 | summary, 83 | time, 84 | mark, 85 | audio, 86 | video { 87 | margin: 0; 88 | padding: 0; 89 | border: 0; 90 | font-size: 100%; 91 | font: inherit; 92 | vertical-align: baseline; 93 | } 94 | /* HTML5 display-role reset for older browsers */ 95 | article, 96 | aside, 97 | details, 98 | figcaption, 99 | figure, 100 | footer, 101 | header, 102 | hgroup, 103 | menu, 104 | nav, 105 | section { 106 | display: block; 107 | } 108 | body { 109 | line-height: 1; 110 | } 111 | ol, 112 | ul { 113 | list-style: none; 114 | } 115 | blockquote, 116 | q { 117 | quotes: none; 118 | } 119 | blockquote:before, 120 | blockquote:after, 121 | q:before, 122 | q:after { 123 | content: ""; 124 | content: none; 125 | } 126 | table { 127 | border-collapse: collapse; 128 | border-spacing: 0; 129 | } 130 | 131 | /* 132 | Josh W Comeau's CSS Reset 133 | https://www.joshwcomeau.com/css/custom-css-reset/ 134 | */ 135 | 136 | /* 137 | 1. Use a more-intuitive box-sizing model. 138 | */ 139 | *, 140 | *::before, 141 | *::after { 142 | box-sizing: border-box; 143 | } 144 | /* 145 | 2. Remove default margin 146 | */ 147 | * { 148 | margin: 0; 149 | } 150 | /* 151 | Typographic tweaks! 152 | 3. Add accessible line-height 153 | 4. Improve text rendering 154 | */ 155 | body { 156 | // line-height: 1.5; 157 | -webkit-font-smoothing: antialiased; 158 | } 159 | /* 160 | 5. Improve media defaults 161 | */ 162 | img, 163 | picture, 164 | video, 165 | canvas, 166 | svg { 167 | display: block; 168 | max-width: 100%; 169 | } 170 | /* 171 | 6. Remove built-in form typography styles 172 | */ 173 | input, 174 | button, 175 | textarea, 176 | select { 177 | font: inherit; 178 | } 179 | /* 180 | 7. Avoid text overflows 181 | */ 182 | p, 183 | h1, 184 | h2, 185 | h3, 186 | h4, 187 | h5, 188 | h6 { 189 | overflow-wrap: break-word; 190 | } 191 | /* 192 | 8. Create a root stacking context 193 | */ 194 | #root, 195 | #__next { 196 | isolation: isolate; 197 | } 198 | -------------------------------------------------------------------------------- /app/blog/post/falling-out-of-love-with-twin-macro/page.mdx: -------------------------------------------------------------------------------- 1 | import TwinMacroCode from "../../../../images/twin-macro-code.png"; 2 | 3 | export const metadata = { 4 | title: "Falling Out of Love with Twin Macro", 5 | author: "Alexander Grattan", 6 | }; 7 | 8 | # Falling Out of Love with Twin Macro 9 | 10 | I love Tailwind, and I also love CSS-in-JS. [Twin Macro](https://github.com/ben-rogerson/twin.macro), an open-source library that combines the two, was a match made in heaven for my day-to-day work for a long time. However, the keyword in the last sentence is "was," let's look to see why that is. 11 | 12 | The growing reason I'm starting to appreciate Web Components and what motivates me to want to try them out is the fact that no extensive build-step is required, they just work. 13 | 14 | On the other end of the spectrum though, a tool such as Twin Macro requires the usage of Babel, an alteration to the Webpack config, and typescript declaration files to wrangle together a usable experience. Once you get it up and running it feels great to use, especially alongside its [fantasic VSCode extension](https://github.com/lightyen/vscode-Tailwind-twin). 15 | 16 | ## Twin Macro is great (in theory) 17 | 18 | Many of the complaints developers have about vanilla Tailwind and its "ugly" HTML, are solved by Twin Macro's flexibility and various organization techniques. 19 | 20 | With Twin Macro, we can create a wrapper div like this with all of the benefits of the VSCode extension's syntax highlighting: 21 | 22 | Code in VSCode of a Twin Macro styled div element 26 | 27 | For components and design systems, we can easily create maps for various attributes such as button sizes: 28 | 29 | ```ts 30 | export const buttonSizes = { 31 | xs: tw`min-w-[77px] py-0.5 px-[18px] text-xs`, 32 | sm: tw`min-w-[89px] py-1 px-5 text-sm`, 33 | md: tw`min-w-[113px] py-2 px-8 text-sm`, 34 | lg: tw`min-w-[163px] py-3.5 px-[50px] text-lg`, 35 | }; 36 | ``` 37 | 38 | We can also do cool things within our defined class names that regular Tailwind cannot such as creating variant groups with parentheses: 39 | 40 | ```tsx 41 | const interactionStyles = () => ( 42 |
43 | ); 44 | ``` 45 | 46 | However, as most frameworks such as Next.js move away from Babel and focus their efforts on removing client-side javascript (Twin Macro requires the usage of Next.js client components), it becomes harder and harder to upgrade or start a new project that uses Twin Macro since it is so tightly woven into the build process. Also, If a future Next.js version were to remove its backward compatibility with Babel, a project with Twin Macro would be unable to upgrade to that version. 47 | 48 | ## It shall be missed 49 | 50 | Railway wrote [an amazing article](https://blog.railway.app/p/twin-macro-tailwind-migration) that I would highly suggest reading on why they've migrated their codebase from Twin Macro to vanilla Tailwind, including improvements to build times and cold starts. Though I'll miss some of the features and flexibility of Twin Macro, the reliability and support behind vanilla Tailwind is reason enough for me to also make the switch. 51 | -------------------------------------------------------------------------------- /styles/project-listing.module.scss: -------------------------------------------------------------------------------- 1 | @import "./variables.module.scss"; 2 | 3 | .projectListing { 4 | position: relative; 5 | width: 100%; 6 | .projectItemContainer { 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | flex-direction: column; 11 | gap: 2rem; 12 | @media (min-width: 768px) { 13 | flex-direction: row; 14 | } 15 | .projectInfo { 16 | font-family: $font; 17 | text-align: left; 18 | color: $white; 19 | @media (min-width: 768px) { 20 | max-width: 300px; 21 | } 22 | .toolsList { 23 | list-style-type: disc; 24 | columns: 2; 25 | padding-left: 1.5rem; 26 | line-height: 20px; 27 | margin-top: 0.5rem; 28 | margin-bottom: 1rem; 29 | font-family: $font; 30 | @media (min-width: 768px) { 31 | margin-bottom: 1.5rem; 32 | } 33 | li { 34 | font-size: 0.875rem; 35 | @media (max-height: 750px) { 36 | font-size: 0.875rem; 37 | } 38 | } 39 | } 40 | a { 41 | display: block; 42 | color: $white; 43 | text-decoration: none; 44 | h2 { 45 | cursor: pointer; 46 | font-size: 1.5rem; 47 | font-weight: bold; 48 | margin: 0; 49 | @media (min-width: 768px) { 50 | font-size: 2rem; 51 | } 52 | @media (max-height: 800px) { 53 | font-size: 1.6rem; 54 | } 55 | } 56 | } 57 | h3 { 58 | font-size: 1rem; 59 | margin-top: 1.5rem; 60 | font-weight: bold; 61 | @media (min-width: 768px) { 62 | font-size: 1.25rem; 63 | } 64 | } 65 | p { 66 | margin-top: 1rem; 67 | line-height: 20px; 68 | font-size: 0.875rem; 69 | font-weight: 300; 70 | @media (min-width: 768px) { 71 | font-size: 1rem; 72 | line-height: 24px; 73 | } 74 | @media (max-height: 800px) { 75 | font-size: 0.875rem; 76 | line-height: 20px; 77 | } 78 | 79 | &:last-of-type { 80 | margin-bottom: 1.5rem; 81 | text-decoration: underline; 82 | text-underline-offset: 0.25rem; 83 | } 84 | } 85 | .projectReadMore { 86 | display: flex; 87 | align-items: center; 88 | background: none; 89 | outline: none; 90 | border: none; 91 | color: white; 92 | font-family: $font; 93 | font-weight: bold; 94 | margin: 0; 95 | padding: 0; 96 | cursor: pointer; 97 | svg { 98 | transition: transform 0.3s ease; 99 | display: block; 100 | margin-top: 0.1rem; 101 | margin-left: 0.25rem; 102 | } 103 | &:hover { 104 | svg { 105 | transform: translateX(2px); 106 | } 107 | } 108 | } 109 | .projectBtns { 110 | display: flex; 111 | gap: 1rem; 112 | justify-content: center; 113 | @media (min-width: 768px) { 114 | justify-content: start; 115 | } 116 | .projectBtn { 117 | display: flex; 118 | justify-content: center; 119 | align-items: center; 120 | text-decoration: none; 121 | padding: 0; 122 | width: 100%; 123 | height: 50px; 124 | border-radius: 15px; 125 | border: none; 126 | outline: none; 127 | color: $black; 128 | background: snow; 129 | font-weight: 700; 130 | font-family: $font; 131 | font-size: 0.875rem; 132 | transition: 150ms transform ease; 133 | will-change: transform; 134 | @media (min-width: 768px) { 135 | font-size: 1rem; 136 | } 137 | &:hover, 138 | &:focus { 139 | transform: scale(1.1); 140 | } 141 | &:active { 142 | transform: scale(0.9); 143 | } 144 | } 145 | } 146 | } 147 | a { 148 | display: block; 149 | width: 100%; 150 | .projectImage { 151 | display: block; 152 | aspect-ratio: 16 / 9; 153 | width: 100%; 154 | height: 100%; 155 | object-fit: cover; 156 | object-position: center; 157 | border-radius: 20px; 158 | transition: box-shadow 0.3s ease; 159 | box-shadow: 160 | 0 2.8px 2.2px rgba(211, 211, 211, 0.02), 161 | 0 6.7px 5.3px rgba(211, 211, 211, 0.028), 162 | 0 12.5px 10px rgba(211, 211, 211, 0.035), 163 | 0 22.3px 17.9px rgba(211, 211, 211, 0.042), 164 | 0 41.8px 33.4px rgba(211, 211, 211, 0.05), 165 | 0 100px 80px rgba(211, 211, 211, 0.07); 166 | &:hover, 167 | &:focus { 168 | box-shadow: 169 | 0 0 0 4px $black, 170 | 0 0 0 8px white, 171 | 0 2.8px 2.2px rgba(256, 256, 256, 0.02), 172 | 0 6.7px 5.3px rgba(256, 256, 256, 0.028), 173 | 0 12.5px 10px rgba(256, 256, 256, 0.035), 174 | 0 22.3px 17.9px rgba(256, 256, 256, 0.042), 175 | 0 41.8px 33.4px rgba(256, 256, 256, 0.05), 176 | 0 100px 80px rgba(256, 256, 256, 0.07); 177 | } 178 | } 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import Image from "next/image"; 3 | import { Suspense } from "react"; 4 | import { FaGithub, FaLinkedin } from "react-icons/fa"; 5 | 6 | import styles from "../styles/home.module.scss"; 7 | import commonStyles from "../styles/common.module.scss"; 8 | import introOverlayStyles from "../styles/intro-overlay.module.scss"; 9 | 10 | import Header from "../components/header"; 11 | import IntroOverlay from "../components/intro-overlay"; 12 | import { projectsList } from "../utils/project-data"; 13 | import ProjectListing from "../components/project-listing"; 14 | import AlexanderGrattan from "../images/alexander-grattan.jpg"; 15 | 16 | const blogPosts: { title: string; slug: string; date: string }[] = [ 17 | { 18 | title: "I Just Read User Friendly by Cliff Kuang and Robert Fabricant", 19 | slug: "just-read-user-friendly", 20 | date: " June 8, 2024", 21 | }, 22 | { 23 | title: "Please Do an Accessibility Audit of Your Website", 24 | slug: "please-do-an-accessibility-audit", 25 | date: " April 27, 2024", 26 | }, 27 | { 28 | title: "Why I Don't Want to Work Remotely Early in My Career", 29 | slug: "no-remote-early-in-my-career", 30 | date: " January 3, 2024", 31 | }, 32 | { 33 | title: "Falling Out of Love with Twin Macro", 34 | slug: "falling-out-of-love-with-twin-macro", 35 | date: "December 29, 2023", 36 | }, 37 | ]; 38 | 39 | export default function Homepage() { 40 | return ( 41 |
42 |
} 44 | > 45 | 46 | 47 |
48 |
49 |
50 |
51 |
52 |

Alexander Grattan

53 |

54 | I create 55 | playful {" "} 56 | experiences. 57 |

58 |
59 | Portrait of Alexander Grattan 65 |
66 |
67 |

68 | Alexander Grattan / Software Developer 69 |

70 |
71 |
72 |

About Me

73 |

74 | Hi there! My name is Alexander Grattan and I'm a software 75 | developer at Gray Swan AI, an AI safety and security company 76 | located in Pittsburgh, PA.
77 |
My current software development skills primarily focus 78 | around web development, but I also enjoy experimenting with app 79 | development, creative coding, data visualization, Human-Computer 80 | Interaction (HCI), and UX. 81 |

82 |
83 |
87 |

My Blog

88 |
    89 | {blogPosts.map((post, i) => ( 90 |
  • 91 | 92 |

    {post.title}

    93 |

    {post.date}

    94 | 95 |
  • 96 | ))} 97 |
98 |
99 |
100 |
101 |

My Projects

102 |
103 | 104 |
105 | {projectsList.map((project) => ( 106 | 107 | ))} 108 |
109 |
110 |
111 | 138 |
139 |
140 | ); 141 | } 142 | -------------------------------------------------------------------------------- /utils/hooks/use-ball-animation.tsx: -------------------------------------------------------------------------------- 1 | import { gsap } from "gsap"; 2 | import { useGSAP } from "@gsap/react"; 3 | import { ScrollTrigger } from "gsap/dist/ScrollTrigger"; 4 | 5 | type UseBallAnimationProps = { 6 | enabled: boolean; 7 | onComplete: () => void; 8 | }; 9 | 10 | export const useBallAnimation = ({ 11 | enabled, 12 | onComplete, 13 | }: UseBallAnimationProps) => { 14 | useGSAP(() => { 15 | const mm = gsap.matchMedia(); 16 | const breakPoint = 768; 17 | 18 | mm.add( 19 | { 20 | isDesktop: `(min-width: ${breakPoint}px)`, 21 | isMobile: `(max-width: ${breakPoint - 1}px)`, 22 | reduceMotion: "(prefers-reduced-motion: reduce)", 23 | }, 24 | (context) => { 25 | if (context.conditions) { 26 | const { isDesktop, reduceMotion } = context.conditions; 27 | 28 | gsap.registerPlugin(ScrollTrigger); 29 | const tl = gsap.timeline(); 30 | const projects: Element[] = gsap.utils.toArray(".project"); 31 | 32 | const homeAnimation = () => { 33 | if (enabled) { 34 | tl.to("#ball", { 35 | duration: reduceMotion ? 0 : 2, 36 | y: "100vh", 37 | ease: "bounce.out", 38 | }) 39 | .to("#ball", { 40 | duration: reduceMotion ? 0 : 1, 41 | delay: 0.2, 42 | scale: isDesktop ? 25 : 30, 43 | ease: "power3.out", 44 | onComplete: onComplete, 45 | }) 46 | .from("#afterAnimation", { 47 | duration: reduceMotion ? 0 : 0.8, 48 | opacity: 0, 49 | ease: "power3.out", 50 | }) 51 | .from("#title", { 52 | duration: reduceMotion ? 0 : 0.5, 53 | y: 100, 54 | delay: 0.2, 55 | opacity: 0, 56 | ease: "power3.out", 57 | }) 58 | .from("#portraitContainer", { 59 | duration: reduceMotion ? 0 : 0.5, 60 | y: 100, 61 | opacity: 0, 62 | ease: "power3.out", 63 | }) 64 | .from("#jobTitle", { 65 | duration: reduceMotion ? 0 : 0.5, 66 | y: 100, 67 | opacity: 0, 68 | ease: "power3.out", 69 | }) 70 | .from("#aboutContainer", { 71 | duration: reduceMotion ? 0 : 0.5, 72 | y: 100, 73 | opacity: 0, 74 | ease: "power3.out", 75 | }) 76 | .from("#blogPreviewContainer", { 77 | duration: reduceMotion ? 0 : 0.5, 78 | y: 100, 79 | opacity: 0, 80 | ease: "power3.out", 81 | }); 82 | } else { 83 | onComplete(); 84 | } 85 | 86 | if (!reduceMotion) { 87 | if (isDesktop) { 88 | projects.forEach((project) => { 89 | const tlProject = gsap.timeline({ 90 | scrollTrigger: { 91 | trigger: project, 92 | start: "top bottom", 93 | end: "center center", 94 | scrub: 1, 95 | }, 96 | }); 97 | const projectImage = project.querySelector("img"); 98 | const projectInfo = project.querySelector("#projectInfo"); 99 | 100 | tlProject 101 | .from(projectImage, { 102 | x: -300, 103 | opacity: 0, 104 | }) 105 | .from(projectInfo, { 106 | x: 300, 107 | opacity: 0, 108 | }); 109 | }); 110 | } else { 111 | projects.forEach((project) => { 112 | const tlProject = gsap.timeline({ 113 | scrollTrigger: { 114 | trigger: project, 115 | start: "top center", 116 | end: "center center", 117 | scrub: 1, 118 | }, 119 | }); 120 | const projectImage = project.querySelector("img"); 121 | const projectInfo = project.querySelector("#projectInfo"); 122 | 123 | tlProject 124 | .from(projectImage, { 125 | y: 100, 126 | opacity: 0, 127 | }) 128 | .from(projectInfo, { 129 | y: 100, 130 | opacity: 0, 131 | }); 132 | }); 133 | } 134 | 135 | const tlFooter = gsap.timeline({ 136 | scrollTrigger: { 137 | trigger: "footer", 138 | start: "top center", 139 | end: "top top", 140 | scrub: 1, 141 | }, 142 | }); 143 | 144 | tlFooter 145 | .from("footer h2", { 146 | y: 100, 147 | opacity: 0, 148 | duration: 0.6, 149 | }) 150 | .from("footer #footerLinks", { 151 | y: 100, 152 | opacity: 0, 153 | duration: 0.6, 154 | }); 155 | } 156 | }; 157 | 158 | homeAnimation(); 159 | } 160 | }, 161 | ); 162 | 163 | return () => mm.revert(); 164 | }, []); 165 | }; 166 | -------------------------------------------------------------------------------- /app/blog/post/please-do-an-accessibility-audit/page.mdx: -------------------------------------------------------------------------------- 1 | import DunningKruger from "../../../../images/dunning-kruger-accessibility.png"; 2 | import WAVEEvaluationTool from "../../../../images/WAVE-evaluation-tool.png"; 3 | 4 | export const metadata = { 5 | title: "Please Do an Accessibility Audit of Your Website", 6 | author: "Alexander Grattan", 7 | }; 8 | 9 | # Please Do an Accessibility Audit of Your Website 10 | 11 | For one of my current projects, the client hired an outside team of associates dedicated to accessibility to come in and review one of their web applications. They found _a lot_ of issues (270 to be exact). 12 | 13 | Seeing this and working through these tasks made me realize the breadth and depth of the world of web accessbility. I was having fun freewheeling in the height of my accessibility Dunning Kruger phase, blind to the pit in which I would fall head-first into. 14 | 15 |
16 | A line graph of the Dunning-Kruger effect in regards to web accessibility. Alexander's Confidence is the title of the y axis and Alexander's Competence is the title of the x axis. There is an arrow pointing to a trough within the graph with text attached to it that reads 'Here I am,' representing Alexander's lack confidence regarding accessibility following his discoveries into how deep the topic goes. 20 |
21 | Writing this article from the trough of accessibility disillusionment. 22 |
23 |
24 | 25 | If you're like me and experiencing the existential crisis looking at the WCAG and available [aria-\* attributes on HTML elements](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes), don't be afraid. I'll walk through three actions you can take to prevent the simplest of accessibiltity errors. 26 | 27 | ## 1. Check Your Images For Correct Alt Text 28 | 29 | This goes without saying, but ensuring your site's images have correct alt text should be of utmost importance. Without alt text for an image, screen readers fail to describe the image's properties to your users, there is no fallback for when your image fails to load, and search engines can't properly index your site and its content. 30 | 31 | Adding alt text is as simple as adding an extra string attribute to your `img` elements, but this can sometimes be challenging when the website's content is controlled by an external party unfamiliar with accessibility standards and screen readers. As developers, the onus is on us to teach and guide others outside of our field to understand the requirement of alt text on the web. If we don't do it, no one else will. 32 | 33 | Crafting alt text is a surprisingly intricate process that depends heavily on the image's content. A good start is to simply imagine a friend in another room asking, "Hey, whatcha looking at?" and how you would attempt to describe the image in words to them. To learn more about the workflow you should take, refer to [World Wide Web Consortium's (W3C) alt decision tree](https://www.w3.org/WAI/tutorials/images/decision-tree/) and [Harvard University's post on describing images for screen readers](https://accessibility.huit.harvard.edu/describe-content-images). 34 | 35 | ## 2. Run Your Operating System's Screen Reader 36 | 37 | Speaking of screen readers, it helps to run one yourself! Why not take the guesswork out of the equation and see exactly how a user who might use a screen reader would interact with your website. Do all of your links read out properly? Is the site's content encountered in a logical order? Do your focus styles for interactive elements appear properly and are accessible by the screen reader? All of these questions and more can be answered by simply turning on your operating system's screen reader. 38 | 39 | Below is a list that matches your operating system to the screen reader you will want to use as well as some articles associated with getting started: 40 | 41 | - MacOS - [VoiceOver](https://support.apple.com/guide/voiceover/welcome/mac) 42 | - [Getting Started Testing with VoiceOver from Harvard](https://accessibility.huit.harvard.edu/voiceover) 43 | - [The University of Melbourne's Guide to Testing Web Pages with VoiceOver](https://www.unimelb.edu.au/accessibility/tools/testing-web-pages-with-voiceover) 44 | - Windows - [Narrator](https://support.microsoft.com/en-us/windows/complete-guide-to-narrator-e4397a0d-ef4f-b386-d8ae-c172f109bdb1) 45 | - [Windows Narrator For Use in Accessibility Testing from Accessibility.com](https://www.accessibility.com/blog/windows-narrator-for-use-in-accessibility-testing) 46 | - Linux - [Orca](https://help.gnome.org/users/orca/stable) 47 | - [Getting Started with Orca from The A11y Project](https://www.a11yproject.com/posts/getting-started-with-orca) 48 | 49 | ## 3. Use the WAVE Browser Extension 50 | 51 | After going through your site with a screen reader, an easy way to find any other lingering accessibility errors is to install and run the [WAVE Evaluation Tool browser extension](https://wave.webaim.org) by the team at [WebAIM](https://webaim.org). This extension returns helpful results such as where your site has color contrast issues, if your site has proper links, and if it includes correct HTML semantics. There exists an extension for the Chrome, Firefox, and Edge web browsers (sorry Safari). 52 | 53 |
54 | The WAVE Evaluation tool browser extension running on the article you are currently reading as it was being WritableStreamDefaultController. Woah! 58 |
59 | Me running the browser extension on the article you're currently reading, 60 | woah! 61 |
62 |
63 | 64 | ## My Favorite Resources 65 | 66 | Interested in checking out other sites and delving more into the topic of accessibility? Check out these awesome organizations and people: 67 | 68 | - [The A11y Project](https://www.a11yproject.com) 69 | - [Adrian Roselli](https://adrianroselli.com) 70 | - [Sarah L. Fossheim](https://fossheim.io) 71 | - [Sara Soueidan](https://www.sarasoueidan.com) 72 | - ["What are accessibility overlays?" by Heydon Pickering](https://youtu.be/uNM8FR3yzWg?si=onhOxBwdXnWrgJus) 73 | -------------------------------------------------------------------------------- /styles/project.module.scss: -------------------------------------------------------------------------------- 1 | @import "./variables.module.scss"; 2 | 3 | .projectContainer { 4 | min-height: 100dvh; 5 | width: 100%; 6 | margin: 0 auto; 7 | overflow: hidden; 8 | .projectMain { 9 | width: 75%; 10 | display: flex; 11 | flex-direction: column; 12 | justify-content: center; 13 | align-items: center; 14 | color: $white; 15 | font-family: $font; 16 | margin: 8rem auto 6rem auto; 17 | .mainImage { 18 | width: 250px; 19 | height: 140px; 20 | border-radius: 20px; 21 | object-fit: cover; 22 | object-position: center; 23 | margin: 0 0 1rem 0; 24 | 25 | @media (min-width: 768px) { 26 | width: 350px; 27 | height: 200px; 28 | } 29 | @media (min-width: 1024px) { 30 | width: 500px; 31 | height: 280px; 32 | margin: 0; 33 | } 34 | @media (min-width: 1280px) { 35 | width: 75%; 36 | max-width: 700px; 37 | height: 450px; 38 | } 39 | } 40 | .textContent { 41 | display: grid; 42 | place-items: center; 43 | 44 | h1 { 45 | color: white; 46 | font-weight: bold; 47 | margin-bottom: 1rem; 48 | text-align: center; 49 | font-size: 2.5rem; 50 | @media (min-width: 768px) { 51 | font-size: 4rem; 52 | } 53 | @media (min-width: 1280px) { 54 | font-size: 6rem; 55 | } 56 | } 57 | p { 58 | line-height: 40px; 59 | font-size: 1rem; 60 | margin: 1rem 0; 61 | width: 100%; 62 | max-width: 600px; 63 | @media (min-width: 768px) { 64 | font-size: 1.375rem; 65 | } 66 | } 67 | } 68 | .comparisonContainer { 69 | width: 100%; 70 | text-align: center; 71 | display: flex; 72 | justify-content: center; 73 | margin: 1rem; 74 | gap: 1rem; 75 | align-items: center; 76 | flex-wrap: wrap; 77 | @media (min-width: 768px) { 78 | gap: 2rem; 79 | } 80 | .imageCompare { 81 | padding: 0; 82 | @media (min-width: 768px) { 83 | padding: 1rem; 84 | } 85 | img { 86 | width: 100%; 87 | height: min-content; 88 | border-radius: 20px; 89 | @media (min-width: 768px) { 90 | width: 75%; 91 | } 92 | } 93 | 94 | h2 { 95 | margin-top: 1rem; 96 | font-weight: bold; 97 | font-size: 1.25rem; 98 | } 99 | } 100 | } 101 | .pageNavigation { 102 | button { 103 | position: fixed; 104 | bottom: 5%; 105 | cursor: pointer; 106 | margin: 0 1rem; 107 | width: 100px; 108 | height: 50px; 109 | border-radius: 15px; 110 | border: none; 111 | outline: none; 112 | background: snow; 113 | font-weight: 700; 114 | font-family: $font; 115 | font-size: 0.875rem; 116 | display: flex; 117 | justify-content: center; 118 | align-items: center; 119 | transition: 150ms transform ease; 120 | will-change: transform; 121 | @media (min-width: 768px) { 122 | top: 50%; 123 | bottom: unset; 124 | width: 125px; 125 | font-size: 1rem; 126 | } 127 | &:hover { 128 | transform: scale(1.1); 129 | } 130 | &:focus { 131 | transform: scale(1.1); 132 | } 133 | &:active { 134 | transform: scale(0.9); 135 | } 136 | } 137 | .previousBtn { 138 | left: 5%; 139 | 140 | &:disabled { 141 | filter: brightness(0.5); 142 | } 143 | .prevArrow { 144 | margin-right: 0.375rem; 145 | @media (min-width: 768px) { 146 | margin-right: 0.5rem; 147 | } 148 | } 149 | } 150 | .nextBtn { 151 | right: 5%; 152 | 153 | &:disabled { 154 | filter: brightness(0.5); 155 | } 156 | 157 | .nextArrow { 158 | margin-left: 0.375rem; 159 | @media (min-width: 768px) { 160 | margin-left: 0.5rem; 161 | } 162 | } 163 | } 164 | } 165 | .bottomLinks { 166 | width: 100%; 167 | max-width: 600px; 168 | .projectLinks { 169 | width: 100%; 170 | display: flex; 171 | gap: 1rem; 172 | justify-content: center; 173 | @media (min-width: 768px) { 174 | justify-content: start; 175 | } 176 | .projectBtn { 177 | display: flex; 178 | justify-content: center; 179 | align-items: center; 180 | text-decoration: none; 181 | color: $black; 182 | padding: 0; 183 | cursor: pointer; 184 | width: 100%; 185 | height: 50px; 186 | border-radius: 15px; 187 | border: none; 188 | outline: none; 189 | background: snow; 190 | font-weight: 700; 191 | font-family: $font; 192 | font-size: 0.875rem; 193 | transition: 150ms transform ease; 194 | will-change: transform; 195 | @media (min-width: 768px) { 196 | font-size: 1rem; 197 | } 198 | &:hover { 199 | transform: scale(1.05); 200 | } 201 | &:focus { 202 | transform: scale(1.05); 203 | } 204 | &:active { 205 | transform: scale(0.9); 206 | } 207 | } 208 | } 209 | 210 | .projectBack { 211 | text-align: center; 212 | color: $white; 213 | text-decoration: none; 214 | font-family: $font; 215 | margin: 2rem auto; 216 | cursor: pointer; 217 | display: flex; 218 | justify-content: center; 219 | align-items: center; 220 | svg { 221 | margin-right: 4px; 222 | transition: transform 0.3s ease; 223 | } 224 | &:hover { 225 | svg { 226 | transform: translateX(-2px); 227 | } 228 | } 229 | &:focus { 230 | svg { 231 | transform: translateX(-2px); 232 | } 233 | } 234 | } 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /styles/home.module.scss: -------------------------------------------------------------------------------- 1 | @import "./variables.module.scss"; 2 | 3 | .homeContainer { 4 | min-height: 100dvh; 5 | width: 100%; 6 | margin: 0 auto; 7 | overflow: hidden; 8 | color: white; 9 | font-family: $font; 10 | .hero { 11 | position: relative; 12 | display: flex; 13 | flex-direction: column; 14 | justify-content: center; 15 | align-items: flex-start; 16 | width: 90%; 17 | max-width: 1536px; 18 | margin: 3rem auto; 19 | @media (min-width: 768px) { 20 | width: 75%; 21 | margin: 8rem auto; 22 | } 23 | @media (max-height: 800px) { 24 | margin: 3rem auto; 25 | } 26 | .cta { 27 | width: 100%; 28 | display: flex; 29 | justify-content: space-between; 30 | align-items: flex-start; 31 | flex-direction: column-reverse; 32 | gap: 4rem; 33 | margin-bottom: 1.25rem; 34 | @media (min-width: 640px) { 35 | flex-direction: row; 36 | align-items: center; 37 | gap: 2rem; 38 | } 39 | .title { 40 | font-size: 2rem; 41 | font-weight: bold; 42 | margin-right: 1rem; 43 | @media (min-width: 640px) { 44 | font-size: 3rem; 45 | justify-content: center; 46 | } 47 | @media (min-width: 768px) { 48 | font-size: 4rem; 49 | } 50 | @media (min-width: 1024px) { 51 | font-size: 5rem; 52 | } 53 | @media (min-width: 1280px) { 54 | font-size: 6rem; 55 | margin-right: 2rem; 56 | } 57 | @media (min-width: 1536px) { 58 | font-size: 7.5rem; 59 | margin-right: 3rem; 60 | } 61 | } 62 | .portraitContainer { 63 | width: 100%; 64 | max-width: 200px; 65 | @media (min-width: 640px) { 66 | max-width: 200px; 67 | } 68 | @media (min-width: 1280px) { 69 | max-width: 300px; 70 | } 71 | @media (min-width: 1536px) { 72 | max-width: 400px; 73 | } 74 | .portrait { 75 | position: relative; 76 | width: 100%; 77 | object-fit: contain; 78 | height: 100%; 79 | border-radius: 2rem; 80 | box-shadow: 1.5rem 1.5rem white; 81 | filter: grayscale(100%) contrast(110%); 82 | clip-path: circle(100%); 83 | transition-duration: 0.3s; 84 | transition-timing-function: ease; 85 | transition-property: box-shadow, clip-path, filter; 86 | } 87 | 88 | @media (prefers-reduced-motion: no-preference) { 89 | &:hover .portrait { 90 | box-shadow: 0px 0px white; 91 | clip-path: circle(50%); 92 | filter: none; 93 | animation: bounceImage 600ms cubic-bezier(0.33, 1, 0.68, 1) infinite 94 | alternate; 95 | } 96 | } 97 | } 98 | } 99 | 100 | .jobTitle { 101 | font-size: 0.9rem; 102 | font-weight: 400; 103 | line-height: 1.5; 104 | @media (min-width: 768px) { 105 | font-size: 1.2rem; 106 | } 107 | @media (min-width: 1536px) { 108 | font-size: 1.4rem; 109 | } 110 | } 111 | } 112 | .aboutContainer { 113 | position: relative; 114 | width: 90%; 115 | margin: 4rem auto; 116 | max-width: 1536px; 117 | z-index: 10; 118 | @media (min-width: 768px) { 119 | width: 75%; 120 | margin: 8rem auto; 121 | } 122 | 123 | h2 { 124 | font-size: 1.5rem; 125 | font-weight: bold; 126 | margin-bottom: 1.5rem; 127 | 128 | @media (min-width: 768px) { 129 | font-size: 3rem; 130 | margin-bottom: 2.5rem; 131 | } 132 | } 133 | 134 | p { 135 | font-size: 1rem; 136 | line-height: 2; 137 | max-width: 65ch; 138 | 139 | @media (min-width: 768px) { 140 | font-size: 1.2rem; 141 | } 142 | } 143 | } 144 | .blogPreviewContainer { 145 | position: relative; 146 | width: 90%; 147 | margin: 4rem auto; 148 | max-width: 1536px; 149 | z-index: 10; 150 | @media (min-width: 768px) { 151 | width: 75%; 152 | margin: 8rem auto; 153 | } 154 | 155 | h2 { 156 | font-size: 1.5rem; 157 | font-weight: bold; 158 | margin-bottom: 1.5rem; 159 | 160 | @media (min-width: 768px) { 161 | font-size: 3rem; 162 | margin-bottom: 2.5rem; 163 | } 164 | } 165 | 166 | ul { 167 | display: flex; 168 | flex-direction: column; 169 | justify-content: flex-start; 170 | align-items: flex-start; 171 | gap: 1rem; 172 | margin-bottom: 2rem; 173 | 174 | li { 175 | display: block; 176 | 177 | a { 178 | color: white; 179 | display: block; 180 | text-decoration: none; 181 | border: 2px solid white; 182 | border-radius: 1rem; 183 | padding: 1rem; 184 | transition-property: background-color, color; 185 | transition-duration: 0.3s; 186 | transition-timing-function: ease; 187 | 188 | &:hover, 189 | &:focus { 190 | background-color: white; 191 | color: $black; 192 | } 193 | 194 | &:hover h3, 195 | &:focus h3 { 196 | text-decoration: underline; 197 | } 198 | 199 | h3 { 200 | font-size: 1rem; 201 | margin-bottom: 0.5rem; 202 | font-weight: bold; 203 | line-height: 1.2; 204 | 205 | @media (min-width: 768px) { 206 | font-size: 1.2rem; 207 | } 208 | } 209 | 210 | p { 211 | font-size: 0.875rem; 212 | 213 | @media (min-width: 768px) { 214 | font-size: 1rem; 215 | } 216 | } 217 | } 218 | } 219 | } 220 | .viewAllContainer { 221 | a { 222 | color: white; 223 | text-decoration: none; 224 | } 225 | } 226 | } 227 | .projectContainer { 228 | position: relative; 229 | 230 | .projectTitleContainer { 231 | width: 90%; 232 | max-width: 1536px; 233 | margin: 0 auto; 234 | color: $white; 235 | font-family: $font; 236 | @media (min-width: 768px) { 237 | width: 75%; 238 | } 239 | } 240 | 241 | h2 { 242 | font-size: 1.5rem; 243 | font-weight: bold; 244 | margin-bottom: 2.5rem; 245 | 246 | @media (min-width: 768px) { 247 | font-size: 3rem; 248 | } 249 | } 250 | 251 | .projectListingsContainer { 252 | display: grid; 253 | gap: 8rem; 254 | width: 90%; 255 | max-width: 1536px; 256 | margin: 0 auto; 257 | @media (min-width: 768px) { 258 | width: 75%; 259 | } 260 | } 261 | } 262 | 263 | footer { 264 | min-height: 100dvh; 265 | display: flex; 266 | justify-content: center; 267 | align-items: center; 268 | flex-direction: column; 269 | font-family: $font; 270 | h2 { 271 | color: $white; 272 | font-size: 2.5rem; 273 | font-weight: bold; 274 | margin-bottom: 2rem; 275 | @media (min-width: 768px) { 276 | font-size: 4rem; 277 | } 278 | } 279 | .footerLinks { 280 | display: flex; 281 | justify-content: center; 282 | align-items: center; 283 | margin: 0; 284 | padding: 0; 285 | list-style-type: none; 286 | li { 287 | padding: 0 16px; 288 | @media (min-width: 768px) { 289 | padding: 0 32px; 290 | } 291 | a { 292 | display: flex; 293 | justify-content: center; 294 | align-items: center; 295 | gap: 0.5rem; 296 | font-family: $font; 297 | color: $white; 298 | text-decoration: none; 299 | font-weight: 700; 300 | font-size: 1rem; 301 | text-underline-offset: 0.25rem; 302 | @media (min-width: 768px) { 303 | font-size: 1.5rem; 304 | } 305 | &:hover { 306 | text-decoration: underline; 307 | } 308 | &:focus { 309 | text-decoration: underline; 310 | } 311 | 312 | svg { 313 | font-size: 1.5rem; 314 | @media (min-width: 768px) { 315 | font-size: 2rem; 316 | } 317 | } 318 | } 319 | } 320 | } 321 | } 322 | } 323 | 324 | @keyframes bounceImage { 325 | from { 326 | transform: translateY(0px) rotateX(20deg); 327 | } 328 | 329 | to { 330 | transform: translateY(-50px) rotateX(0deg); 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /utils/project-data.ts: -------------------------------------------------------------------------------- 1 | import { StaticImageData } from "next/image"; 2 | 3 | import ArtificialUnintelligence from "../images/Artificial_Unintelligence.png"; 4 | import Alcohol101Plus from "../images/alcohol101plus-og.jpg"; 5 | import ResponsibilityWorks from "../images/rw-og.png"; 6 | import HaydenAI from "../images/hayden-ai-og.png"; 7 | import SafeDrive from "../images/safe-opengraph.jpg"; 8 | import VesperAI from "../images/vesper-og.png"; 9 | import VirtualBar from "../images/virtual-bar.png"; 10 | import FoodPhantoms from "../images/Food_Phantoms.png"; 11 | import BigBurgh from "../images/big-burgh.png"; 12 | import SecretPittsburgh from "../images/secret_pittsburgh.png"; 13 | import SecretPittsburghMobile from "../images/secret_pittsburgh_small.png"; 14 | import PollockIsShit from "../images/Pollock_Is_Shit_Screenshot.png"; 15 | import PollockIsShitMobile from "../images/Pollock_is_Shit_OG.png"; 16 | import PittCSC from "../images/Pitt_CSC_Screenshot.png"; 17 | import PittCSCMobile from "../images/Pitt_CSC_Screenshot_Small.png"; 18 | import PittCSCFigma from "../images/Pitt_CSC_Figma_Screenshot.png"; 19 | import PittCSCOld from "../images/Original_CSC_Site_Screenshot.png"; 20 | import GradeMyAid from "../images/GradeMyAid.png"; 21 | import GradeMyAidMobile from "../images/GradeMyAid_Small.png"; 22 | import GradeMyAidFigma from "../images/Grademyaid_Figma_Screenshot.png"; 23 | import Grosseries from "../images/grosseries.png"; 24 | import BellyButton from "../images/Belly_Button_Design.jpg"; 25 | import BellyButtonMobile from "../images/Belly_Button_Design_Small.jpg"; 26 | import AllegoryOfTheCave from "../images/Allegory_Cave_Screenshot.jpg"; 27 | import AllegoryOfTheCaveMobile from "../images/Allegory_Cave_Screenshot_Small.jpg"; 28 | import VirtualSafari from "../images/Virtual_Safari_Screenshot.png"; 29 | import VirtualSafariMobile from "../images/Virtual_Safari_Screenshot_Small.png"; 30 | 31 | export type ProjectType = { 32 | name: string; 33 | description: string; 34 | longDescription?: string; 35 | image: StaticImageData; 36 | mobileImage?: StaticImageData; 37 | figma?: StaticImageData; 38 | old?: StaticImageData; 39 | code?: string; 40 | link?: string; 41 | slug: string; 42 | type: string; 43 | }; 44 | 45 | export const projectsList: ProjectType[] = [ 46 | { 47 | name: "Artificial Unintelligence", 48 | description: 49 | "Online multiplayer game where players compete against each other to create the funniest AI-generated images.", 50 | image: ArtificialUnintelligence, 51 | link: "https://www.artificialunintelligence.gg", 52 | code: "https://github.com/agrattan0820/artificial-unintelligence", 53 | slug: "artificial-unintelligence", 54 | type: "Personal Project", 55 | }, 56 | { 57 | name: "Alcohol101+", 58 | description: 59 | "An innovative, interactive program which helps students make safe and responsible decisions about alcohol.", 60 | image: Alcohol101Plus, 61 | link: "https://www.alcohol101.plus", 62 | slug: "alcohol101-plus", 63 | type: "with Actual Size for Responsibility.org", 64 | }, 65 | { 66 | name: "Hayden AI Marketing Website", 67 | description: 68 | "Hayden AI creates cutting-edge AI for smarter cities and this website highlights its various platforms and technologies.", 69 | image: HaydenAI, 70 | link: "https://www.hayden.ai", 71 | slug: "hayden-ai", 72 | type: "with Actual Size for Hayden AI", 73 | }, 74 | { 75 | name: "Responsibility Works", 76 | description: 77 | "An online alcohol responsibility course that assists employers and employees in training and maintaining safe and responsible workplaces.", 78 | image: ResponsibilityWorks, 79 | link: "https://responsibilityworks.com", 80 | slug: "responsibility-works", 81 | type: "with Actual Size for Responsibility.org", 82 | }, 83 | { 84 | name: "SAFE Marketing Website", 85 | description: 86 | "SAFE drives change by educating on the importance of ignition interlocks, a technology that encourages a responsible life after DUI.", 87 | image: SafeDrive, 88 | link: "https://www.safedrive.org", 89 | slug: "safe-drive", 90 | type: "with Actual Size for SAFE", 91 | }, 92 | { 93 | name: "Virtual Bar App", 94 | description: 95 | "A iOS, Android, and web app that allows users to gain a better understanding of how different factors affect their blood-alcohol concentration – or BAC.", 96 | image: VirtualBar, 97 | link: "https://apps.apple.com/us/app/virtual-bar/id1055457361", 98 | slug: "virtual-bar", 99 | type: "with Actual Size for Responsibility.org", 100 | }, 101 | { 102 | name: "Vesper AI Marketing Website", 103 | description: 104 | "Vesper AI, a subsiduary of Hayden AI, turns crowdsourced data into operational intelligence that saves lives, at Actual Size I created this website to tell this story.", 105 | image: VesperAI, 106 | link: "https://www.vespersolutions.ai", 107 | slug: "vesper-ai", 108 | type: "with Actual Size for Hayden AI", 109 | }, 110 | { 111 | name: "Food Phantoms", 112 | description: 113 | "Web app to find if a restaurant on Doordash or Uber Eats is a ghost kitchen or a virtual kitchen where the food served originates from an unexpected establishment.", 114 | image: FoodPhantoms, 115 | link: "https://food-phantoms.deno.dev/", 116 | code: "https://github.com/agrattan0820/food-phantoms-frontend", 117 | slug: "food-phantoms", 118 | type: "Personal Project", 119 | }, 120 | { 121 | name: "Big Burgh Redesign", 122 | description: 123 | "A new design and construction of the Big Burgh app (not an official redesign) used to help Pittsburgh natives find resources such as food and shelter.", 124 | image: BigBurgh, 125 | code: "https://github.com/agrattan0820/big-burgh", 126 | slug: "big-burgh", 127 | type: "Personal Project", 128 | }, 129 | { 130 | name: "Secret Pittsburgh", 131 | description: 132 | "Website for the Secret Pittsburgh class at the University of Pittsburgh (ENGLIT 1412) dedicated to uncovering hidden spaces and unusual places in the city of Pittsburgh.", 133 | image: SecretPittsburgh, 134 | mobileImage: SecretPittsburghMobile, 135 | link: "https://secretpittsburgh.org/", 136 | code: "https://github.com/agrattan0820/secret-pittsburgh", 137 | slug: "secret-pittsburgh", 138 | type: "Personal Project", 139 | }, 140 | { 141 | name: "Pitt CSC", 142 | description: 143 | "Redesigned and developed the website for the Pitt Computer Science Club that helps advertise its efforts to support computer science and technology-related initiatives.", 144 | longDescription: 145 | "I was appointed as a club officer for the Pitt Computer Science Club, both to support initiatives and to redesign the club website.\nBesides upgrading the technology from an outdated Jekyll site to a modern Gatsby site, I wanted the design to represent the student spirit of Pitt Computer Science Club.\nI iterated with different designs, gaining feedback from members on how the club should be advertised.\nSome new things I experimented with for this project were svg animations and SEO analysis using Google Search Console, Google Analytics, and Hotjar.", 146 | image: PittCSC, 147 | mobileImage: PittCSCMobile, 148 | figma: PittCSCFigma, 149 | old: PittCSCOld, 150 | link: "https://pittcsc.org/", 151 | slug: "pitt-csc", 152 | code: "https://github.com/pittcsc/pittcsc-website", 153 | type: "Personal Project", 154 | }, 155 | { 156 | name: "Grademyaid", 157 | description: 158 | "Application that allows students to grade their college financial aid packages using data from the US Department of Education.\nThis was built for Pitt CSC Hacks, a competition sponsored by the University of Pittsburgh's Computer Science Club.", 159 | image: GradeMyAid, 160 | mobileImage: GradeMyAidMobile, 161 | longDescription: 162 | "This was built for Pitt CSC Hacks, a competition sponsored by the University of Pittsburgh's Computer Science Club and was my first time participating in a coding competition. Going into the experience I was quite nervous.\nI stuck to my guns and went with an idea that I had the past summer which was a website that helps students analyze their financial aid package based on information about the school, similar to how Niche.com grades colleges and universities.\nI ended up finding a partner who didn't know much about web development, but was willing to work with me in developing this idea.\nThis was my first foray into handling large amounts of data and trying to contextualize it for a user. If I were to fix one thing about the site, it would be effectively distinguishing the grades between each other so that a user's result is crystal clear.", 163 | figma: GradeMyAidFigma, 164 | link: "https://grademyaid.netlify.app/", 165 | slug: "grademyaid", 166 | code: "https://github.com/agrattan0820/grade-my-aid", 167 | type: "Personal Project", 168 | }, 169 | { 170 | name: "Grosseries", 171 | description: 172 | "A mobile app that helps individuals to track expiration dates and the inventory of their food, a group project for the University of Pittsburgh's CS1635 created with the app development framework, Flutter.", 173 | image: Grosseries, 174 | code: "https://github.com/agrattan0820/cs1635-flutter-project", 175 | slug: "grosseries", 176 | type: "Personal Project", 177 | }, 178 | { 179 | name: "Pollock Is Sh!t", 180 | description: 181 | "Web app that manipulates image data to create a Pollock-like painting (because literally anyone could've been Pollock).", 182 | image: PollockIsShit, 183 | mobileImage: PollockIsShitMobile, 184 | link: "https://pollockisshit.netlify.app/", 185 | code: "https://github.com/agrattan0820/Pollock-is-Poop", 186 | slug: "pollock-is-sh!t", 187 | type: "Personal Project", 188 | }, 189 | { 190 | name: "Belly Button Chrome Extension", 191 | description: 192 | "Browser extension that helps developers and accessibility engineers inspect a website's buttons and determine if they follow HTML standards and Web Content Accessibility Guidelines (WCAG).", 193 | image: BellyButton, 194 | mobileImage: BellyButtonMobile, 195 | link: "http://getbellybutton.com/", 196 | code: "https://github.com/agrattan0820/belly-button", 197 | slug: "belly-button", 198 | type: "Personal Project", 199 | }, 200 | { 201 | name: "Virtual Safari", 202 | description: 203 | "A recreation of Timon and Pumbaa's Virtual Safari, a choose-your-own adventure from The Lion King DVD.\nI edited the choices into individual clips using the video editor, DaVinci Resolve, and used JavaScript to develop the site interaction.", 204 | image: VirtualSafari, 205 | mobileImage: VirtualSafariMobile, 206 | link: "https://virtualsafari.netlify.app/", 207 | slug: "virtual-safari", 208 | code: "https://github.com/agrattan0820/Virtual-Safari", 209 | type: "Personal Project", 210 | }, 211 | ]; 212 | --------------------------------------------------------------------------------