My website—this very page you're on right now. It's got an about page with some personal info and work experience, some pages dedicated to various projects, and some art showcases.
I self-identify as a bit of a maniac when it comes to projecting. I really love learning and building things, and usually have something in the works during my non-work time. Professionally—and also personally—I thought it'd be fun to showcase some of these interests to the world, doubling as a portfolio and place to document a variety of project journeys.
Similarly to my other endeavors, this website is built with NextJS. Almost all of the content for the site is edited and published in Contentful. Contentful is a content management system1 that allows you to maintain and publish content (e.g. this post) with their platform, but consume and serve up the content in any way you see fit via their API. As an example, I've defined a ProjectPost
content type in Contentful that I use for each of these projects. The types of content that a project post can have are explicitly defined via a content model.
Once you publish a ProjectPost
on the Contentful side, you receive a payload via the API on your side that looks something like this:
fields: {
title: 'My Website',
description: 'Website that includes a short CV, coding projects, illustrations, and other things.',
demoLink: 'tannersmarshall.com',
slug: 'website',
mainImage: { metadata: [Object], sys: [Object], fields: [Object] },
body: { nodeType: 'document', data: {}, content: [Array] }
}
Contentful also has a nice React utility called documentToReactComponents
, which helps render rich text as HTML. This utility takes an options
argument that allows custom rendering of various HTML elements. For example, to render "code" nodes like this one
:
const options = {
renderMark: {
[MARKS.CODE]: (text) => (
<code className="text-accent-400 px-2 py-1 mx-1 text-[85%] rounded-md font-mono">
{text}
</code>
),
},
};
NextJS is set up such that when a user navigates to /project/[slug]
, it checks all the ProjectPost
entries for a matching slug
property and renders it if it exists. In this way new project posts get their own pages automagically!
Another fun part of the website was the root page animation2. The animation was produced with phaser
, a JavaScript-based game engine using matter-js
to get the saucy explosion effects. The head and facial features were illustrated and exported as separate png
images, and their relatives positions mapped to coordinates in the Phaser canvas.
const textures: Texture[] = [
{ key: 'brain', x: -25, y: -50 },
{ key: 'skull', x: -25, y: -50 },
{ key: 'ear-left', x: -460, y: 35 },
{ key: 'ear-right', x: 425, y: 35 },
{ key: 'earring-left', x: -450, y: 115 },
{ key: 'earring-right', x: 415, y: 115 },
{ key: 'head', x: 0, y: 0 },
{ key: 'eye-left', x: -245, y: 40 },
{ key: 'eye-right', x: 205, y: 40 },
{ key: 'eyebrow-right', x: 200, y: -150 },
{ key: 'mouth', x: 70, y: 300 },
{ key: 'nose', x: 0, y: 125 },
{ key: 'hair', x: -25, y: -300 },
];
Once in place, I set up both pointermove
and pointerdown
event listeners to trigger an explode
method that calculates random x
, y
, and angular velocities to send the unexpecting facial features off into the ether.
explode() {
if (!this.isExploded && !this.explodingCoolingDown) {
this.isExploded = true;
this.explodeCooldown();
this.parts.forEach((part) => {
const rangeV = 15;
const rangeA = 1;
const vx = randomIntFromInterval(-rangeV, rangeV);
const vy = randomIntFromInterval(-rangeV, rangeV);
part.sprite.setVelocity(vx, vy);
part.sprite.setAngularVelocity(randomIntFromInterval(-rangeA, rangeA) * 0.1);
});
}
}
As mentioned, the backend framework is NextJS with Contentful as the content manager, so there's currently no database layer.
NextJS uses good ol' fashioned React on the frontend (as well as the backend with server components). For styling, I previously used styled-components and Rebass but have recently migrated to TailwindCSS. Most UI animations are done with framer-motion or basic CSS animations.
Made with 🥒 by T. 2024