diff --git a/.github/workflows/link-check.yml b/.github/workflows/link-check.yml new file mode 100644 index 0000000..9c25d20 --- /dev/null +++ b/.github/workflows/link-check.yml @@ -0,0 +1,129 @@ +name: Link check + +# The portfolio points at a lot of other people's platforms. Articles get +# unpublished, videos get pulled, side-project deployments lapse. This walks +# every link and asset on a schedule and files an issue when something rots. + +on: + schedule: + # 09:00 UTC every Monday. + - cron: '0 9 * * 1' + workflow_dispatch: + pull_request: + paths: + - 'portfolio/lib/**' + - 'portfolio/app/**' + - 'portfolio/components/**' + - 'portfolio/scripts/check-links.mjs' + +permissions: + contents: read + issues: write + +jobs: + check: + runs-on: ubuntu-latest + defaults: + run: + working-directory: portfolio + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + # No install step: the checker deliberately has zero dependencies so it + # runs from a bare checkout. + - name: Check links and assets + id: check + continue-on-error: true + run: node scripts/check-links.mjs --markdown link-report.md --json link-report.json + + - name: Upload report + uses: actions/upload-artifact@v4 + with: + name: link-report + path: | + portfolio/link-report.md + portfolio/link-report.json + retention-days: 90 + + - name: Summarise on the run page + run: cat link-report.md >> "$GITHUB_STEP_SUMMARY" + + # One rolling issue rather than a new one every week. If it is already + # open we replace the body so it always shows the current state; if the + # last run fixed everything we close it. + - name: File or update the issue + if: github.event_name != 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs') + const report = fs.readFileSync('portfolio/link-report.md', 'utf8') + const data = JSON.parse(fs.readFileSync('portfolio/link-report.json', 'utf8')) + const broken = data.results.filter(r => r.status === 'broken') + const TITLE = 'Broken links on the portfolio' + + const { data: open } = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + labels: 'link-rot', + }) + const existing = open.find(i => i.title === TITLE) + + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}` + const body = `${report}\n\n---\n[Full run](${runUrl})` + + if (broken.length === 0) { + if (existing) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existing.number, + body: `All links resolve again as of ${new Date().toISOString().slice(0, 10)}. Closing.`, + }) + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existing.number, + state: 'closed', + }) + } + return + } + + if (existing) { + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existing.number, + body, + }) + } else { + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: TITLE, + labels: ['link-rot'], + body, + }) + } + + # Only the scheduled and manual runs can fail. + # + # A pull request touching one CSS file was failing because a video on + # someone else's platform had been taken down. That is real information, + # and it is already captured: the weekly run files a rolling issue and + # closes it when the links resolve again. What it is not is a reason to + # block a change, because nobody reviewing that PR can fix YouTube. + # + # The check still runs on every pull request and still writes its report + # to the run summary, so a link this branch actually broke is visible + # before merge. It just does not gate the merge. + - name: Fail if anything is broken + if: github.event_name != 'pull_request' && steps.check.outcome == 'failure' + run: exit 1 diff --git a/Claude outputs/arcade-loud-desktop.png b/Claude outputs/arcade-loud-desktop.png new file mode 100644 index 0000000..e509228 Binary files /dev/null and b/Claude outputs/arcade-loud-desktop.png differ diff --git a/Claude outputs/arcade-loud-mobile.png b/Claude outputs/arcade-loud-mobile.png new file mode 100644 index 0000000..89d43b8 Binary files /dev/null and b/Claude outputs/arcade-loud-mobile.png differ diff --git a/Claude outputs/arcade-restrained-desktop.png b/Claude outputs/arcade-restrained-desktop.png new file mode 100644 index 0000000..14a0433 Binary files /dev/null and b/Claude outputs/arcade-restrained-desktop.png differ diff --git a/Claude outputs/c-home.png b/Claude outputs/c-home.png new file mode 100644 index 0000000..a65f3ca Binary files /dev/null and b/Claude outputs/c-home.png differ diff --git a/Claude outputs/c-mobile.png b/Claude outputs/c-mobile.png new file mode 100644 index 0000000..b9eab2f Binary files /dev/null and b/Claude outputs/c-mobile.png differ diff --git a/Claude outputs/c-services.png b/Claude outputs/c-services.png new file mode 100644 index 0000000..473544f Binary files /dev/null and b/Claude outputs/c-services.png differ diff --git a/Claude outputs/d-home.png b/Claude outputs/d-home.png new file mode 100644 index 0000000..784fd20 Binary files /dev/null and b/Claude outputs/d-home.png differ diff --git a/Claude outputs/d-mobile.png b/Claude outputs/d-mobile.png new file mode 100644 index 0000000..a6789f7 Binary files /dev/null and b/Claude outputs/d-mobile.png differ diff --git a/Claude outputs/detail-full.png b/Claude outputs/detail-full.png new file mode 100644 index 0000000..df37d79 Binary files /dev/null and b/Claude outputs/detail-full.png differ diff --git a/Claude outputs/final-desktop.png b/Claude outputs/final-desktop.png new file mode 100644 index 0000000..34adff7 Binary files /dev/null and b/Claude outputs/final-desktop.png differ diff --git a/Claude outputs/final-mobile.png b/Claude outputs/final-mobile.png new file mode 100644 index 0000000..4faf82b Binary files /dev/null and b/Claude outputs/final-mobile.png differ diff --git a/Claude outputs/first-screen-mobile.png b/Claude outputs/first-screen-mobile.png new file mode 100644 index 0000000..dd13ae3 Binary files /dev/null and b/Claude outputs/first-screen-mobile.png differ diff --git a/Claude outputs/first-screen.png b/Claude outputs/first-screen.png new file mode 100644 index 0000000..5bd3058 Binary files /dev/null and b/Claude outputs/first-screen.png differ diff --git a/Claude outputs/hack-buenos-aires-shortlist.jpg b/Claude outputs/hack-buenos-aires-shortlist.jpg new file mode 100644 index 0000000..0699414 Binary files /dev/null and b/Claude outputs/hack-buenos-aires-shortlist.jpg differ diff --git a/Claude outputs/home-light2.png b/Claude outputs/home-light2.png new file mode 100644 index 0000000..4243611 Binary files /dev/null and b/Claude outputs/home-light2.png differ diff --git a/Claude outputs/home.png b/Claude outputs/home.png new file mode 100644 index 0000000..8da7838 Binary files /dev/null and b/Claude outputs/home.png differ diff --git a/Claude outputs/landing.png b/Claude outputs/landing.png new file mode 100644 index 0000000..9b103c0 Binary files /dev/null and b/Claude outputs/landing.png differ diff --git a/Claude outputs/link-check.yml b/Claude outputs/link-check.yml new file mode 100644 index 0000000..fbbd660 --- /dev/null +++ b/Claude outputs/link-check.yml @@ -0,0 +1,118 @@ +name: Link check + +# The portfolio points at a lot of other people's platforms. Articles get +# unpublished, videos get pulled, side-project deployments lapse. This walks +# every link and asset on a schedule and files an issue when something rots. + +on: + schedule: + # 09:00 UTC every Monday. + - cron: '0 9 * * 1' + workflow_dispatch: + pull_request: + paths: + - 'portfolio/lib/**' + - 'portfolio/app/**' + - 'portfolio/components/**' + - 'portfolio/scripts/check-links.mjs' + +permissions: + contents: read + issues: write + +jobs: + check: + runs-on: ubuntu-latest + defaults: + run: + working-directory: portfolio + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + # No install step: the checker deliberately has zero dependencies so it + # runs from a bare checkout. + - name: Check links and assets + id: check + continue-on-error: true + run: node scripts/check-links.mjs --markdown link-report.md --json link-report.json + + - name: Upload report + uses: actions/upload-artifact@v4 + with: + name: link-report + path: | + portfolio/link-report.md + portfolio/link-report.json + retention-days: 90 + + - name: Summarise on the run page + run: cat link-report.md >> "$GITHUB_STEP_SUMMARY" + + # One rolling issue rather than a new one every week. If it is already + # open we replace the body so it always shows the current state; if the + # last run fixed everything we close it. + - name: File or update the issue + if: github.event_name != 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs') + const report = fs.readFileSync('portfolio/link-report.md', 'utf8') + const data = JSON.parse(fs.readFileSync('portfolio/link-report.json', 'utf8')) + const broken = data.results.filter(r => r.status === 'broken') + const TITLE = 'Broken links on the portfolio' + + const { data: open } = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + labels: 'link-rot', + }) + const existing = open.find(i => i.title === TITLE) + + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}` + const body = `${report}\n\n---\n[Full run](${runUrl})` + + if (broken.length === 0) { + if (existing) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existing.number, + body: `All links resolve again as of ${new Date().toISOString().slice(0, 10)}. Closing.`, + }) + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existing.number, + state: 'closed', + }) + } + return + } + + if (existing) { + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existing.number, + body, + }) + } else { + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: TITLE, + labels: ['link-rot'], + body, + }) + } + + - name: Fail if anything is broken + if: steps.check.outcome == 'failure' + run: exit 1 diff --git a/Claude outputs/n-about.png b/Claude outputs/n-about.png new file mode 100644 index 0000000..e70eaa1 Binary files /dev/null and b/Claude outputs/n-about.png differ diff --git a/Claude outputs/n-home.png b/Claude outputs/n-home.png new file mode 100644 index 0000000..53108a3 Binary files /dev/null and b/Claude outputs/n-home.png differ diff --git a/Claude outputs/n-mobile.png b/Claude outputs/n-mobile.png new file mode 100644 index 0000000..461898f Binary files /dev/null and b/Claude outputs/n-mobile.png differ diff --git a/Claude outputs/n-services.png b/Claude outputs/n-services.png new file mode 100644 index 0000000..6724c46 Binary files /dev/null and b/Claude outputs/n-services.png differ diff --git a/Claude outputs/portfolio-home.png b/Claude outputs/portfolio-home.png new file mode 100644 index 0000000..901d4fa Binary files /dev/null and b/Claude outputs/portfolio-home.png differ diff --git a/Claude outputs/portfolio-work-list.png b/Claude outputs/portfolio-work-list.png new file mode 100644 index 0000000..ce15da1 Binary files /dev/null and b/Claude outputs/portfolio-work-list.png differ diff --git a/Claude outputs/quiet-dark-desktop.png b/Claude outputs/quiet-dark-desktop.png new file mode 100644 index 0000000..c537265 Binary files /dev/null and b/Claude outputs/quiet-dark-desktop.png differ diff --git a/Claude outputs/quiet-dark-mobile.png b/Claude outputs/quiet-dark-mobile.png new file mode 100644 index 0000000..fccd2dc Binary files /dev/null and b/Claude outputs/quiet-dark-mobile.png differ diff --git a/Claude outputs/quiet-hover.png b/Claude outputs/quiet-hover.png new file mode 100644 index 0000000..d0e0edd Binary files /dev/null and b/Claude outputs/quiet-hover.png differ diff --git a/Claude outputs/quiet-light-desktop.png b/Claude outputs/quiet-light-desktop.png new file mode 100644 index 0000000..4915684 Binary files /dev/null and b/Claude outputs/quiet-light-desktop.png differ diff --git a/Claude outputs/services-top.png b/Claude outputs/services-top.png new file mode 100644 index 0000000..e0ca7b5 Binary files /dev/null and b/Claude outputs/services-top.png differ diff --git a/Claude outputs/speaking-top.png b/Claude outputs/speaking-top.png new file mode 100644 index 0000000..b0d8810 Binary files /dev/null and b/Claude outputs/speaking-top.png differ diff --git a/Claude outputs/terminal-loud-desktop.png b/Claude outputs/terminal-loud-desktop.png new file mode 100644 index 0000000..9f09c3f Binary files /dev/null and b/Claude outputs/terminal-loud-desktop.png differ diff --git a/Claude outputs/terminal-loud-mobile.png b/Claude outputs/terminal-loud-mobile.png new file mode 100644 index 0000000..03249c5 Binary files /dev/null and b/Claude outputs/terminal-loud-mobile.png differ diff --git a/Claude outputs/terminal-restrained-desktop.png b/Claude outputs/terminal-restrained-desktop.png new file mode 100644 index 0000000..2ed20c9 Binary files /dev/null and b/Claude outputs/terminal-restrained-desktop.png differ diff --git a/Claude outputs/v4-a.png b/Claude outputs/v4-a.png new file mode 100644 index 0000000..0937ef7 Binary files /dev/null and b/Claude outputs/v4-a.png differ diff --git a/Claude outputs/v4-b.png b/Claude outputs/v4-b.png new file mode 100644 index 0000000..c655835 Binary files /dev/null and b/Claude outputs/v4-b.png differ diff --git a/Claude outputs/v4-c.png b/Claude outputs/v4-c.png new file mode 100644 index 0000000..22c969e Binary files /dev/null and b/Claude outputs/v4-c.png differ diff --git a/Claude outputs/v4-d.png b/Claude outputs/v4-d.png new file mode 100644 index 0000000..8928818 Binary files /dev/null and b/Claude outputs/v4-d.png differ diff --git a/Claude outputs/v4-mobile.png b/Claude outputs/v4-mobile.png new file mode 100644 index 0000000..c3c3c5c Binary files /dev/null and b/Claude outputs/v4-mobile.png differ diff --git a/Claude outputs/work-filtered.png b/Claude outputs/work-filtered.png new file mode 100644 index 0000000..69619ec Binary files /dev/null and b/Claude outputs/work-filtered.png differ diff --git a/Claude outputs/work-hover.png b/Claude outputs/work-hover.png new file mode 100644 index 0000000..4c7b1d3 Binary files /dev/null and b/Claude outputs/work-hover.png differ diff --git a/Claude outputs/work-mobile.png b/Claude outputs/work-mobile.png new file mode 100644 index 0000000..ebfbc8c Binary files /dev/null and b/Claude outputs/work-mobile.png differ diff --git a/Claude outputs/work-top.png b/Claude outputs/work-top.png new file mode 100644 index 0000000..6fd58d9 Binary files /dev/null and b/Claude outputs/work-top.png differ diff --git a/Claude outputs/workshop-loud-desktop.png b/Claude outputs/workshop-loud-desktop.png new file mode 100644 index 0000000..f902d43 Binary files /dev/null and b/Claude outputs/workshop-loud-desktop.png differ diff --git a/Claude outputs/workshop-restrained-desktop.png b/Claude outputs/workshop-restrained-desktop.png new file mode 100644 index 0000000..001ce7d Binary files /dev/null and b/Claude outputs/workshop-restrained-desktop.png differ diff --git a/Claude outputs/workshop-restrained-mobile.png b/Claude outputs/workshop-restrained-mobile.png new file mode 100644 index 0000000..75a9b0f Binary files /dev/null and b/Claude outputs/workshop-restrained-mobile.png differ diff --git a/Claude outputs/zine-loud-desktop.png b/Claude outputs/zine-loud-desktop.png new file mode 100644 index 0000000..ee1c419 Binary files /dev/null and b/Claude outputs/zine-loud-desktop.png differ diff --git a/Claude outputs/zine-loud-mobile.png b/Claude outputs/zine-loud-mobile.png new file mode 100644 index 0000000..b7c1a05 Binary files /dev/null and b/Claude outputs/zine-loud-mobile.png differ diff --git a/Claude outputs/zine-restrained-desktop.png b/Claude outputs/zine-restrained-desktop.png new file mode 100644 index 0000000..ce575cf Binary files /dev/null and b/Claude outputs/zine-restrained-desktop.png differ diff --git a/portfolio/.gitignore b/portfolio/.gitignore index cb2f312..85bf3db 100644 --- a/portfolio/.gitignore +++ b/portfolio/.gitignore @@ -1,38 +1,21 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc +node_modules/ +.next/ +out/ +build/ .DS_Store *.pem - -# debug npm-debug.log* yarn-debug.log* yarn-error.log* - -# local env files .env*.local - -# vercel .vercel - -# typescript *.tsbuildinfo next-env.d.ts -# In progress -app/art-bot \ No newline at end of file +# link checker output +link-report.md +link-report.json +scripts/.refs.json + +# V2 files V3 replaced. Safe to delete this folder. +_v2-removed/ diff --git a/portfolio/README.md b/portfolio/README.md index f4da3c4..50c5160 100644 --- a/portfolio/README.md +++ b/portfolio/README.md @@ -1,34 +1,84 @@ -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). +# Portfolio -## Getting Started +Jonathan (Jay) Albert's personal site. Next.js App Router, Tailwind, deployed on Vercel. -First, run the development server: +## Running it ```bash +npm install npm run dev -# or -yarn dev -# or -pnpm dev ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +## Where the content lives -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +There are no hand-written project pages. Everything is data: -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. +| File | What it holds | +| --- | --- | +| `lib/content/entries.ts` | Every piece of work: title, summary, date, cover, facets, link | +| `lib/content/writeups.ts` | Long-form write-ups for entries that have one | +| `lib/content/taxonomy.ts` | The three filter vocabularies: form, practice, field | +| `lib/site.ts` | Bio, social links, speaking formats and topics, services, roles | -## Learn More +### Adding a piece of work -To learn more about Next.js, take a look at the following resources: +Add an object to `entries.ts`. The required fields are `slug`, `title`, +`summary`, `date` (ISO `yyyy-mm`), `medium`, `practices`, `domains`, and +`cover`. Add `href` for an outbound link. Set `featured: true` to put it on the +home page. -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +If it needs a write-up on this site rather than only an outbound link, set +`detail: ''` on the entry and add a matching record to `writeups.ts`. The +route, the metadata, and the related-work section are generated from that. An +entry can name a `detail` slug before its write-up exists; the card falls back +to the outbound link until the write-up lands. -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! +### The three filters -## Deploy on Vercel +`medium` (form), `practices`, and `domains` are the only filter axes, and they +come from fixed lists in `taxonomy.ts`. Specific technologies go in `tech`, +which is displayed and searched but never becomes a filter chip. V2 had 221 +free-text skill tags across 37 projects, 130 of which appeared exactly once, +which is what this replaces. Resist adding a fourth axis. -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +Within an axis, selections are OR'd. Across axes they are AND'd. Filter state +lives in the URL, so any filtered view is a link you can send to someone: +`/work?form=talk&field=Gaming`. -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. +## Link checking + +The site links out to a lot of platforms that are not ours. `scripts/check-links.mjs` +walks every external URL, `public/` asset, and internal route referenced from +`app/`, `lib/`, and `components/`, and reports what is broken. + +```bash +npm run check:links # everything, printed to stdout +npm run check:links:report # writes link-report.md and link-report.json +node scripts/check-links.mjs --offline # assets and routes only, no network +node scripts/check-links.mjs --external-only # only http(s) URLs +``` + +YouTube links are verified through the oEmbed endpoint rather than the watch +page, because a removed or private video still returns 200 with an error page. + +Hosts that wall off bots (X, Instagram, LinkedIn, Medium, and friends) are +reported as "needs a human look" rather than broken, because the checker cannot +tell a bot wall apart from a dead page. + +`.github/workflows/link-check.yml` runs the same script every Monday and keeps +one rolling issue up to date, closing it when everything resolves again. +Scheduled workflows only run from the default branch, so this starts firing +once it is merged to `main`. + +## Theme + +Dark and light, driven by CSS custom properties in `app/globals.css`. The +visitor's system preference wins unless they use the toggle, which stores a +choice in `localStorage`. A small inline script in `app/layout.tsx` applies the +stored choice before first paint so the page does not flash the wrong theme. + +## Fonts + +Self-hosted from `@fontsource` packages via `next/font/local` (see `lib/fonts.ts`) +rather than fetched from Google. The build does not depend on reaching +fonts.googleapis.com, and no visitor's browser makes a request to Google. diff --git a/portfolio/app/about/page.tsx b/portfolio/app/about/page.tsx new file mode 100644 index 0000000..f34409c --- /dev/null +++ b/portfolio/app/about/page.tsx @@ -0,0 +1,15 @@ +import type { Metadata } from 'next' +import AboutSection from '@/components/sections/AboutSection' + +export const metadata: Metadata = { + title: 'About', + description: 'Career, what the work is actually made of, and a resume.', +} + +export default function AboutPage() { + return ( +
+ +
+ ) +} diff --git a/portfolio/app/ar-piano-lessons/page.tsx b/portfolio/app/ar-piano-lessons/page.tsx deleted file mode 100644 index 90d8f32..0000000 --- a/portfolio/app/ar-piano-lessons/page.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import "/main.css" -import ProjectCover from "../../components/project-cover"; - - -export default function Piano() { - return ( -
- - {/* This is the image card that appears at the top of every project page. */} -
- - -
-

What are Augmented Piano Lessons?

-

Augmented reality presents an opportunity to revolutionize the way we learn how to play instruments. Imagine sitting in front of a piano, not knowing how to play it, and being able to play a song by following notes projected on the instrument. Now we have the ability to take the game mechanics of Rockband and apply them to real instruments! If you are having trouble picturing augmented lessons, then take a look at RocketJump's animation from 2010.

-
- -
- -
- -

That is cool, but what does that have to do with Jonathan?

-

- During a course on project management for computer information systems, I decided to challenge myself by playing the role of developer and creating a proof of concept for augmented piano lessons. I chose the piano because it is a significant segment of the musical instrument market and the instrument I am most familiar with. To avoid expenses, I decided the proof of concept would be triggered by an image target that would be similar to a piano. Once the image is recognized, the software will play a stream of notes that a user can hit to a familiar tune. The following is a recording of the proof of concept running on my desktop using my phone as the image target. -

-
- -
- -
-
-
-
- ); - } \ No newline at end of file diff --git a/portfolio/app/blotto/page.tsx b/portfolio/app/blotto/page.tsx deleted file mode 100644 index 885d210..0000000 --- a/portfolio/app/blotto/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import "/main.css" -import Link from "next/link"; -import ProjectCover from "../../components/project-cover"; - - -export default function Blotto() { - return ( -
- - {/* This is the image card that appears at the top of every project page. */} -
- - -
-

Blotto Origins

-

After years of tinkering with the game engine Unity, I decided it was time to commit to a project that I could develop and publish within a few months. I have discussed making games with a few close friends in the past, and I finally convinced one that we were both capable of launching a game on the app store. The friend I convinced is an optometrist by trade, so I gave him a crash course on Unity over a weekend, and we decided he would be in charge of game design, while I would be in charge of development. After spending a few weeks understanding our limitations, we arrived at Blotto. -

- Blotto is British slang for drunk, and it alludes to soaking up alcohol the way blotting paper soaks up ink. Blotto is a drinking card game filled with challenges for all people over 21 years of age to enjoy. -

-
- Download the Game -
-
-
-
- ); - } \ No newline at end of file diff --git a/portfolio/app/customizable-phone-case/page.tsx b/portfolio/app/customizable-phone-case/page.tsx deleted file mode 100644 index 640fd45..0000000 --- a/portfolio/app/customizable-phone-case/page.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import "/main.css" -import ProjectCover from "../../components/project-cover"; - - -export default function Phone() { - return ( -
- - {/* This is the image card that appears at the top of every project page. */} -
- - -
-

Unlimited Styles for an Unlimited Phone

-

The iPhone case our team sought to design would place a screen on the back of an iPhone that the user could customize with an application. The user would have been able to send a static image to the screen or an animation. We also entertained the idea of building applications designed to utilize both screens, like a game of Guess Who or Battleship.

-

Preview

-

The dimensions for the inner casing were taken from Apple's website. The extension at the bottom of the case would contain the chip to control the screen on the back of the phone. The screen is protected by a transparent shell.

- -
- -
-
-
-
- ); - } \ No newline at end of file diff --git a/portfolio/app/globals.css b/portfolio/app/globals.css index d93e92b..c99e4be 100644 --- a/portfolio/app/globals.css +++ b/portfolio/app/globals.css @@ -1,27 +1,579 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import 'tailwindcss'; + +/* --------------------------------------------------------------------------- + Palette + + One accent, descended from the turquoise the site has always used but + lifted for contrast on a dark ground. Everything else is a neutral ramp. + Colours are declared as raw values here and mapped to semantic names in + the @theme block below, so a component never names a literal colour. +--------------------------------------------------------------------------- */ :root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 255, 255, 255; - --background-end-rgb: 255, 255, 255; + color-scheme: dark; + + --bg: #0a0c10; + --bg-subtle: #0e1117; + --surface: #12161f; + --surface-raised: #171c27; + --line: #232937; + --line-strong: #333b4d; + + --text: #e9edf4; + --text-muted: #9aa4b8; + --text-faint: #6b7488; + + --accent: #2fd8cd; + --accent-hover: #5fe6dd; + --accent-contrast: #04231f; + --accent-faint: #0f2b2c; + + --warn: #f0b429; + --shadow: 0 1px 2px rgb(0 0 0 / 0.4), 0 8px 24px -12px rgb(0 0 0 / 0.6); + + /* next/font sets --font-instrument and friends on , so these have to + resolve there too. Defining them on :root keeps that true. */ + --font-display: var(--font-display-face, Georgia), 'Times New Roman', serif; + --font-body: var(--font-body-face, ui-sans-serif), system-ui, sans-serif; + --font-mono: var(--font-mono-face, ui-monospace), 'SF Mono', Menlo, monospace; +} + +:root[data-theme='light'] { + color-scheme: light; + + --bg: #fbfbfa; + --bg-subtle: #f4f4f2; + --surface: #ffffff; + --surface-raised: #ffffff; + --line: #e3e3df; + --line-strong: #cdcdc7; + + --text: #16181d; + --text-muted: #5b6270; + --text-faint: #868d9b; + + --accent: #0b8f8a; + --accent-hover: #0a7a76; + --accent-contrast: #ffffff; + --accent-faint: #e4f4f2; + + --warn: #a86a05; + --shadow: 0 1px 2px rgb(20 20 20 / 0.05), 0 8px 24px -14px rgb(20 20 20 / 0.18); +} + +@theme inline { + --color-bg: var(--bg); + --color-bg-subtle: var(--bg-subtle); + --color-surface: var(--surface); + --color-surface-raised: var(--surface-raised); + --color-line: var(--line); + --color-line-strong: var(--line-strong); + + --color-text: var(--text); + --color-muted: var(--text-muted); + --color-faint: var(--text-faint); + + --color-accent: var(--accent); + --color-accent-hover: var(--accent-hover); + --color-accent-contrast: var(--accent-contrast); + --color-accent-faint: var(--accent-faint); + --color-warn: var(--warn); + + --font-display: var(--font-display); + --font-sans: var(--font-body); + --font-mono: var(--font-mono); + + /* A slightly tighter, more editorial type ramp than the Tailwind default. */ + --text-display: 4rem; + --text-display--line-height: 1.02; + --text-display--letter-spacing: -0.03em; } -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; +/* --------------------------------------------------------------------------- + Base +--------------------------------------------------------------------------- */ + +* { + border-color: var(--line); +} + +html { + scroll-behavior: smooth; + -webkit-text-size-adjust: 100%; +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; } } body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); + background: var(--bg); + color: var(--text); + font-family: var(--font-body); + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +::selection { + background: var(--accent); + color: var(--accent-contrast); +} + +/* A visible focus ring everywhere, not just where a component remembered. */ +:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 3px; + border-radius: 2px; +} + +/* Skip link: off-screen until focused. */ +.skip-link { + position: absolute; + left: -9999px; + top: 0; + z-index: 100; +} +.skip-link:focus { + left: 1rem; + top: 1rem; + padding: 0.5rem 0.875rem; + background: var(--accent); + color: var(--accent-contrast); + border-radius: 0.375rem; + font-weight: 600; +} + +/* --------------------------------------------------------------------------- + Utilities used often enough to earn a name +--------------------------------------------------------------------------- */ + +@utility label { + font-family: var(--font-mono); + font-size: 0.6875rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--text-faint); +} + +@utility rule { + border-top: 1px solid var(--line); +} + +/* Cards lift very slightly. Enough to read as interactive, not enough to + bounce the whole grid around when the pointer crosses it. */ +@utility card { + background: var(--surface); + border: 1px solid var(--line); + border-radius: 0.75rem; + transition: + border-color 0.18s ease, + transform 0.18s ease, + box-shadow 0.18s ease; +} + +@utility card-interactive { + background: var(--surface); + border: 1px solid var(--line); + border-radius: 0.75rem; + transition: + border-color 0.18s ease, + transform 0.18s ease, + box-shadow 0.18s ease; + + &:hover { + border-color: var(--line-strong); + transform: translateY(-2px); + box-shadow: var(--shadow); + } +} + +/* Long-form prose blocks on detail pages. */ +.prose-body { + color: var(--text-muted); + font-size: 1.0625rem; + line-height: 1.75; +} +.prose-body p + p { + margin-top: 1.25rem; +} +.prose-body a { + color: var(--accent); + text-decoration: underline; + text-underline-offset: 3px; + text-decoration-thickness: 1px; +} +.prose-body a:hover { + color: var(--accent-hover); +} +.prose-body strong { + color: var(--text); + font-weight: 600; +} + +/* --------------------------------------------------------------------------- + Home page primitives +--------------------------------------------------------------------------- */ + +@utility cta { + display: inline-flex; + align-items: center; + gap: 0.55rem; + min-height: 50px; + padding: 0 1.25rem; + border-radius: 0.625rem; + background: var(--accent); + color: var(--accent-contrast); + font-size: 0.97rem; + font-weight: 600; + transition: background-color 0.15s ease; + + &:hover { + background: var(--accent-hover); + } +} + +/* Results fade rather than snap when the filter changes. */ +.results { + animation: results-in 0.22s ease; +} +@keyframes results-in { + from { + opacity: 0; + transform: translateY(4px); + } +} +@media (prefers-reduced-motion: reduce) { + .results { + animation: none; + } +} + +@utility cta-ghost { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 50px; + padding: 0 1rem; + border: 1px solid var(--line); + border-radius: 0.625rem; + color: var(--text); + font-size: 0.94rem; + transition: + border-color 0.15s ease, + background-color 0.15s ease; + + &:hover { + border-color: var(--line-strong); + background: var(--surface); + } +} + +@media (max-width: 639px) { + .cta { + width: 100%; + justify-content: center; + min-height: 54px; + } +} + + + +/* + * The filters were the one control on the site speaking a different language. + * + * Two things gave them away. They were 999px pills, on a site whose every + * other rectangle is a small radius: cards 0.75rem, buttons 0.625rem, inputs + * 0.375rem. And the selected state was a solid accent fill, when accent + * appears nowhere else as a fill, only as a word ("Write-up") and one thin + * bar on About. Thirteen of them sit in a row directly above the catalogue, + * so selecting three made the filters the loudest thing on a page whose + * subject is the work underneath them. + * + * Now: the same radius family as everything else, one step below the buttons + * because a chip is the smallest control here, and a selected chip is marked + * with an accent edge and a wash rather than a flood. Accent stays an accent. + */ +@utility chip { + display: inline-flex; + align-items: center; + gap: 0.4rem; + min-height: 30px; + border: 1px solid var(--line); + border-radius: 0.5rem; + padding: 0 0.65rem; + font-size: 0.8125rem; + line-height: 1; + color: var(--text-muted); + transition: + border-color 0.14s ease, + color 0.14s ease, + background-color 0.14s ease, + opacity 0.14s ease; + + &:hover { + border-color: var(--line-strong); + color: var(--text); + } +} + +/* + * Unreachable, not gone. + * + * A chip whose count has fallen to zero used to be removed from the row. That + * kept the row honest and made it jump: pick "Talk" and the field row loses + * four of its nine chips, everything after the gap slides left, and the chip + * that was under the cursor is now a different chip. The row is a fixed set + * of options, so it should be a fixed set of positions. Ruled-out options stay + * where they are, dimmed and not clickable, which also answers the question + * the disappearing version could not: what did that selection rule out. + */ +@utility chip-out { + opacity: 0.42; + cursor: default; + + &:hover { + border-color: var(--line); + color: var(--text-muted); + } +} + +@utility chip-on { + border-color: var(--accent); + background: var(--accent-faint); + color: var(--text); + + &:hover { + border-color: var(--accent-hover); + background: var(--accent-faint); + color: var(--text); + } +} + +.chip-count { + font-family: var(--font-mono); + font-size: 0.6875rem; + font-variant-numeric: tabular-nums; + color: var(--text-faint); +} +.chip-on .chip-count { + color: var(--accent); +} + +@utility clear-btn { + display: inline-flex; + align-items: center; + min-height: 24px; + border: 1px solid var(--line); + border-radius: 0.5rem; + padding: 0 0.55rem; + font-size: 0.75rem; + line-height: 1; + color: var(--text-muted); + transition: + border-color 0.14s ease, + color 0.14s ease; + + &:hover { + border-color: var(--line-strong); + color: var(--text); + } +} +/* --------------------------------------------------------------------------- + Covers + + Every tile is generated by scripts/build-covers.py at 1200x750 on a + transparent ground, so the card paints the space around the artwork and one + tile is correct in both themes. Nothing crops: the stream banners carry + their titles hard against the left edge and a fill-crop sliced them. +--------------------------------------------------------------------------- */ + +/* Entries with no usable source image. The tile stays empty rather than + repeating the title printed directly beneath it. */ +.cover-tile { + aspect-ratio: 16 / 10; + background: var(--surface); + border-bottom: 1px solid var(--line); +} + +/* --------------------------------------------------------------------------- + Motion + + One idea: things arrive from slightly below and slightly transparent. Cards + stagger across a row on their way in. Nothing moves on its own after that, + and none of it runs for anyone who has asked for reduced motion. +--------------------------------------------------------------------------- */ + +.reveal { + opacity: 0; + transform: translateY(10px); + transition: + opacity 0.5s cubic-bezier(0.22, 1, 0.36, 1), + transform 0.5s cubic-bezier(0.22, 1, 0.36, 1); + transition-delay: var(--reveal-delay, 0ms); +} +.reveal.is-in { + opacity: 1; + transform: none; +} + +@media (prefers-reduced-motion: reduce) { + .reveal { + opacity: 1; + transform: none; + transition: none; + } +} + +/* --------------------------------------------------------------------------- + Hero + + The name is the graphic. There is no photograph and no illustration on this + site, so the one thing available to carry the top of the page at scale is + the type, and half-committing to that is what made it read as empty rather + than as restraint. +--------------------------------------------------------------------------- */ + +.hero-name { + font-family: var(--font-display); + font-size: clamp(2.5rem, 6.5vw, 4.5rem); + line-height: 1.0; + letter-spacing: -0.03em; + color: var(--text); + text-wrap: balance; +} + +/* The line and the links share a row so the width is used rather than left + trailing off to the right of a 46-character column. */ +.hero-line { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-top: 1.75rem; + padding-top: 1.5rem; + border-top: 1px solid var(--line); +} + +@media (min-width: 900px) { + .hero-line { + flex-direction: row; + align-items: baseline; + justify-content: space-between; + gap: 3rem; + } + /* ConnectRow supplies its own top margin for the stacked case. */ + .hero-line > div { + margin-top: 0; + } +} + +/* --------------------------------------------------------------------------- + Detail page rail + + The same sections in the same order on every entry, in one treatment. Two + of them link into the filtered catalogue and the rest do not, which is a + difference in behaviour rather than in kind, so it shows up as a hover + colour and nothing else. +--------------------------------------------------------------------------- */ + +.rail-section + .rail-section { + margin-top: 1.6rem; +} +.rail-item { + font-size: 0.875rem; + line-height: 1.45; + color: var(--text-muted); +} +.rail-empty { + margin-top: 0.6rem; + font-size: 0.875rem; + color: var(--text-faint); +} + +/* --------------------------------------------------------------------------- + Elsewhere + + Navigation, sitting immediately above a grid of cards. Deliberately not a + card: no border, no fill, no lift. The arrow moves on hover and the title + takes the accent, which is enough to say it is a link without competing + with the eighty-nine tiles below it. +--------------------------------------------------------------------------- */ + +.elsewhere { + display: grid; + gap: 0; + border-top: 1px solid var(--line); + border-bottom: 1px solid var(--line); +} + +@media (min-width: 640px) { + .elsewhere { + grid-template-columns: 1fr 1fr; + } + .elsewhere-item + .elsewhere-item { + border-left: 1px solid var(--line); + padding-left: 2rem; + } +} + +.elsewhere-item { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 1.4rem 0; +} +.elsewhere-item + .elsewhere-item { + border-top: 1px solid var(--line); +} +@media (min-width: 640px) { + .elsewhere-item + .elsewhere-item { + border-top: 0; + } +} + +.elsewhere-head { + display: flex; + align-items: center; + gap: 0.6rem; + color: var(--text); + transition: color 0.16s ease; +} +.elsewhere-head svg { + transition: transform 0.22s cubic-bezier(0.22, 1, 0.36, 1); +} +.elsewhere-item:hover .elsewhere-head { + color: var(--accent); +} +.elsewhere-item:hover .elsewhere-head svg { + transform: translateX(4px); +} + +.elsewhere-title { + font-family: var(--font-display); + font-size: 1.25rem; + line-height: 1.1; +} + +.elsewhere-body { + max-width: 44ch; + font-size: 0.875rem; + line-height: 1.55; + color: var(--text-muted); +} + +@media (prefers-reduced-motion: reduce) { + .elsewhere-head svg { + transition: none; + } + .elsewhere-item:hover .elsewhere-head svg { + transform: none; + } } diff --git a/portfolio/app/grace-potter/page.tsx b/portfolio/app/grace-potter/page.tsx deleted file mode 100644 index 072cc3d..0000000 --- a/portfolio/app/grace-potter/page.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import "/main.css" -import Link from "next/link"; -import ProjectCover from "../../components/project-cover"; - - -export default function Grace() { - return ( -
- - {/* This is the image card that appears at the top of every project page. */} -
- - -
-

Grace Potter

-

During the senior phase of Fullstack Academy, three other students and I were tasked with developing an e-commerce platform. One of our team members made pottery in his free time, and we decided to build a platform to sell the products he made. -

-
- View the Code -
-
-
-
- ); - } \ No newline at end of file diff --git a/portfolio/app/invisible-maze-game/page.tsx b/portfolio/app/invisible-maze-game/page.tsx deleted file mode 100644 index 9afd259..0000000 --- a/portfolio/app/invisible-maze-game/page.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import "/main.css" -import YoutubeEmbed from "../../components/youtube-embed"; -import Link from "next/link"; -import ProjectCover from "../../components/project-cover"; - - -export default function Maze() { - return ( -
- - {/* This is the image card that appears at the top of every project page. */} -
- - -
-

Discovering the Metaverse

-

A few months after establishing Mix3d, the community was interested in learning about the metaverse. Many were interested in buying land and building on platforms like Decentraland and Sandbox, and we came up with the idea of a generative maze game. Every day, a new maze would be generated, and users would pay a fee to enter the arena. A portion of their fee would be deposited into a prize pool that would sit at the end of the maze for someone to claim. -

- The Invisible Maze game was my attempt at testing Decentraland before deploying our game. On my 30th birthday, I launched a prototype for the Mix3d community, and I challenged our members to compete to solve the invisible maze and claim 0.1 ETH. -

- I posted this video announcing the launch, and I provided written instructions for anyone to get involved. I pressed the publish button and went off to dinner. Immediately after, the community and my followers on social media went into a frenzy, competing for the prize. -

-

- -

-

- Everyone involved had a lot of fun with the experience, and I learned some crucial lessons. -
- 1. Decentraland is too limited for indie developers to build worthwhile experiences. The plots are too small, and the height limitation prevented us from building challenging mazes, which is why I decided to make the maze invisible. -
- 2. The objects were not programmable, which prevented us from creating moving platforms and in-game interactions. It is more of a digital museum than a place for people to create and play games. -
- 3. The experience made me reevaluate the value of metaverses with limited land. Web2 thrived because developer tools and distribution platforms were free to use. Web3 games are struggling because indie developers do not have the capital to experiment, and even if they do, the tools they have to develop are extremely limited. -

- If you want to find out more about the invisible maze, you can view the original documents on the Mix3d notion. -

-
- View the Docs -
-
-
-
- ); - } \ No newline at end of file diff --git a/portfolio/app/layout.tsx b/portfolio/app/layout.tsx index 3bf7c5c..aa92ea3 100644 --- a/portfolio/app/layout.tsx +++ b/portfolio/app/layout.tsx @@ -1,53 +1,82 @@ -import './globals.css' -import { Inter } from 'next/font/google' -import Link from 'next/link' -import "/main.css" +import type { Metadata } from 'next' import Script from 'next/script' +import Header from '@/components/Header' +import Footer from '@/components/Footer' +import { fontVars } from '@/lib/fonts' +import { site } from '@/lib/site' +import './globals.css' -const inter = Inter({ subsets: ['latin'] }) - -export const metadata = { - title: "Jonathan Albert's Portfolio", - description: "A portfolio of all of Jonathan's past work that is not included in a resume or LinkedIn.", +export const metadata: Metadata = { + metadataBase: new URL(site.url), + title: { + default: `${site.name} — developer relations`, + template: `%s — ${site.name}`, + }, + description: site.tagline, + openGraph: { + title: site.name, + description: site.tagline, + url: site.url, + siteName: site.name, + type: 'website', + }, + twitter: { card: 'summary_large_image', creator: '@Jay_Albert_' }, } -export default function RootLayout({ - children, -}: { - children: React.ReactNode -}) { +/** + * Applied before first paint so the correct theme is already on the document + * when the CSS lands, otherwise the page flashes dark and then corrects itself. + */ +const themeInit = ` +(function () { + try { + var stored = localStorage.getItem('theme'); + var prefersLight = window.matchMedia('(prefers-color-scheme: light)').matches; + var theme = stored || (prefersLight ? 'light' : 'dark'); + if (theme === 'light') document.documentElement.setAttribute('data-theme', 'light'); + } catch (e) {} +})(); +` + +export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - - -
-
-
- Portfolio - {/* Resources - Lifestyle */} -
-
- - {children} -
+ + + - + ) -} \ No newline at end of file +} diff --git a/portfolio/app/lifestyle/page.tsx b/portfolio/app/lifestyle/page.tsx deleted file mode 100644 index 8fbe414..0000000 --- a/portfolio/app/lifestyle/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Lifestyle() { - return (

This is the lifestyle page!

); - } \ No newline at end of file diff --git a/portfolio/app/lilypad-frontend/page.tsx b/portfolio/app/lilypad-frontend/page.tsx deleted file mode 100644 index 4961f6d..0000000 --- a/portfolio/app/lilypad-frontend/page.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import Image from 'next/image' -import Link from 'next/link' -import { Typography } from '../../lib/mui' - -export default function LilypadFrontend() { - return ( -
-
-
- Screenshot of Lilypad frontend code -

Lilypad Frontend Guide

-
- -

- Created a comprehensive guide to help developers set up and customize - their own frontend for the Lilypad Network. This documentation enables - developers to create user-friendly interfaces for interacting with the - decentralized compute network. -

- -

Guide Highlights

-
    -
  • - Step-by-step instructions for setting up a Next.js development - environment -
  • -
  • - Detailed explanation of the frontend architecture and key components -
  • -
  • Examples of customizing the UI to match specific use cases
  • -
  • - Best practices for integrating with the Lilypad Network backend -
  • -
- -

Technical Implementation

-
    -
  • - Next.js Framework: Utilized for its robust - development features and excellent developer experience -
  • -
  • - TypeScript Integration: Ensures type safety and - better code maintainability -
  • -
  • - React Components: Created modular, reusable - components for common Lilypad interactions -
  • -
  • - Documentation Structure: Organized content to - progress from basic setup to advanced customization -
  • -
- -

Impact

-

- This guide significantly reduces the barrier to entry for developers - looking to build on the Lilypad Network. By providing clear, - actionable instructions and example code, it enables developers to - quickly set up their own customized frontends and start leveraging the - platform's distributed computing capabilities. -

- -
- - Technical Writing - - - Documentation - - - React - - - Next.js - - - TypeScript - - - Frontend Development - - - UI Development - - - Git - - - Developer Relations - -
- -
-
- - Read Guide - -
-
- - View Frontend Repository - -
-
-
-
- ) -} diff --git a/portfolio/app/lilypad/page.tsx b/portfolio/app/lilypad/page.tsx deleted file mode 100644 index 8df4238..0000000 --- a/portfolio/app/lilypad/page.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import Image from 'next/image' -import Link from 'next/link' -import { Typography } from '../../lib/mui' - -export default function Lilypad() { - return ( -
-
-
- Lilypad Network logo -

Lilypad Network

-
- -

- Lilypad is an innovative decentralized compute network that enables - users to run any Docker container across a distributed network of - providers. As a contributor to this project, I focused on enhancing - user experience and documentation to make the platform more accessible - to developers and businesses. -

- -

Key Contributions

-
    -
  • - Collaborated with the development team to identify and document core - use cases for the platform -
  • -
  • - Created comprehensive documentation explaining how to leverage - Lilypad's distributed compute capabilities -
  • -
  • - Helped articulate the platform's value proposition for - different user segments -
  • -
  • Contributed to improving the user onboarding experience
  • -
- -

Technical Overview

-

Lilypad Network is built on several key technologies:

-
    -
  • - Docker Integration: Allows users to run any - containerized workload on the network -
  • -
  • - Filecoin Virtual Machine: Provides the backbone for - decentralized compute resources -
  • -
  • - Bacalhau: Powers distributed computing capabilities -
  • -
  • - Go Implementation: Ensures robust and efficient - backend operations -
  • -
- -

Impact and Learning

-

- Working on Lilypad provided valuable insights into distributed - computing and blockchain technology applications. The project - demonstrates how decentralized networks can provide accessible, - scalable computing resources while maintaining security and - efficiency. -

- -
- - Technical Writing - - - Documentation - - - Distributed Computing - - - Blockchain - - - Docker - - - Go - - - Linux - - - Content Creation - - - Project Management - -
- -
-
- - Read Blog Post - -
-
-
-
- ) -} diff --git a/portfolio/app/livein/page.tsx b/portfolio/app/livein/page.tsx deleted file mode 100644 index a35e441..0000000 --- a/portfolio/app/livein/page.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import "/main.css" -import Link from "next/link"; -import ProjectCover from "../../components/project-cover"; - - -export default function Livein() { - return ( -
- - {/* This is the image card that appears at the top of every project page. */} -
- - -
-

LiveIn Vision

-

LIVEIN sought to allow real estate buyers and renters to discover available units within a building by scanning the facade of the building using their smartphones. Potential clients would then be offered augmented reality tours and the opportunity to book a viewing via a mobile application. Once a client finds a property that suits their needs, they would be able to make an offer and come to an agreement with the owner via the app, eliminating the need for a broker and reducing the cost to transact. -

-
-
- Instagram -
-
- Facebook -
-
-
-
-
- ); - } \ No newline at end of file diff --git a/portfolio/app/memorial-website/page.tsx b/portfolio/app/memorial-website/page.tsx deleted file mode 100644 index 7780a50..0000000 --- a/portfolio/app/memorial-website/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import "/main.css" -import Link from "next/link"; -import ProjectCover from "../../components/project-cover"; - - -export default function Memorial() { - return ( -
- - {/* This is the image card that appears at the top of every project page. */} -
- - -
-

Rest on the Web

-

My Uncle Eric was an amazing man who found himself in unfortunate circumstances. It is hard to find people who match his creativity and talent, which included drawing, dancing, composing music, and storytelling. He brightened every room he walked into and sought to be everyone's greatest advocate. -

- When Tio Eddie passed away, my father took care of the funeral arrangements. The funeral home offered us a package that included a website where people could go to pay their respects and share memories. My father and I thought it was a unique idea, and we decided we would make it ourselves. My father shared some of his ideas for the website, and I used Wix to create it. We created the website to reflect Tio Eddie's interests in music, his experiences, his family, and his friends. -

-
- Visit the Website -
-
-
-
- ); - } \ No newline at end of file diff --git a/portfolio/app/mix3d-clothing/page.tsx b/portfolio/app/mix3d-clothing/page.tsx deleted file mode 100644 index 8b3d29b..0000000 --- a/portfolio/app/mix3d-clothing/page.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import "/main.css" -import ProjectCover from "../../components/project-cover"; -import YoutubeEmbed from "../../components/youtube-embed"; -import Image from "next/image"; - - -export default function Clothing() { - return ( -
- - {/* This is the image card that appears at the top of every project page. */} -
- - -
-

The Start of the Phygital Movement

-

To my surprise, some of the most active members of the Mix3D community were interested in how Web3 would impact fashion. After several meetings, I came up with the idea for a phygital (physical + digital) marketplace and a pipeline for releasing collections. The release cycle is as follows: -

- 1. Mix3d designers sketch clothing and create tech packs for manufacturers. -
- 2. Mix3d 3D modelers create models out of sketches. -
- 3. Create augmented reality filters for Snapchat, Facebook, and Instagram so users can post pictures wearing their clothing before having the physical products. -

-
- -

-
- 4. Create a copy of the 3D models with fewer polygons to be used in Decentraland. -
- 5. Use Magica voxel to create a 3D version of the clothing that can be used in Sandbox. -

- A 3D model of a light pink sweater on the left of the image, and a voxel version of the sweater on the right of the image. -
-

-

- 6. Mint the original 3D model as an NFT to be used for presales. -
- 7. List the 3D model on the Mix3d marketplace and allow users to preview the clothing in augmented reality. -

-
- -
-

- Using this strategy, we would be able to raise the funds to manufacture a collection without worrying if there was a market for it. Since production would take longer than most other clothing lines, we would be able to airdrop the social network filters and models for Decentraland and Sandbox every couple of months leading up to receiving the physical product. -

-

Preview Store Proof of Concept

- -
-
-
- ); - } \ No newline at end of file diff --git a/portfolio/app/mix3d/page.tsx b/portfolio/app/mix3d/page.tsx deleted file mode 100644 index c1b5f87..0000000 --- a/portfolio/app/mix3d/page.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import "/main.css" -import Link from "next/link"; -import ProjectCover from "../../components/project-cover"; - - -export default function Mixed() { - return ( -
- - {/* This is the image card that appears at the top of every project page. */} -
- - -
-

Building the Next Iteration of the Internet

-

At the height of the 2021 bull run, my friend and I found that we were being bombarded by questions about cryptocurrency, DeFi, NFTs, and DAOs. After having the same conversations over and over, we both decided we would work together to create a community where anyone could learn about Web3, with the hopes that we would build products and services together. -

- Together, we worked on informational content, articles, Dapps, games, clothing, investment groups, and a Web3 boot camp. Everyone volunteered in hopes that we could build a self-sustaining community, but our focus shifted as we each obtained roles in Web3. The community has a little over 100 members, and many have transitioned into web3 roles at organizations like Coinbase, Protocol Labs, Celcius, and DAOPunks. -

- The community is now inactive, but you can still join our Discord, and access all of our documents. To appreciate the speed at which it grew in a few months, I encourage you to visit the page: - How we Mix3d up 2021 -

-
- Join the Community -
-
-
-
- ); - } \ No newline at end of file diff --git a/portfolio/app/mobile-vendor/page.tsx b/portfolio/app/mobile-vendor/page.tsx deleted file mode 100644 index 2457d43..0000000 --- a/portfolio/app/mobile-vendor/page.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import "/main.css" -import ProjectCover from "../../components/project-cover"; - - -export default function Vendor() { - return ( -
- - {/* This is the image card that appears at the top of every project page. */} -
- - -
-

The On-Demand Economy

-

Consumers are increasingly expecting an on-demand experience for every product and service they engage with. Amazon has set the tone for 2-day deliveries for products and is working on reducing turnaround time from days to hours. To deliver products and services within hours of a request, companies will begin to manage mobile distribution centers and mobile service providers.

-

Application Preview

-

- The following preview is the product of a week of research and development. -

-
-
- -
-

Toyota's Vision

-

Toyota is already anticipating mobile distribution centers and mobile service providers. The video below shares the full vision of a Mobile Electronic Marketplace (MEM).

-
-
- -
-
-

- As several companies work on autonomous vehicles, I see an opportunity to spring up a third-party service that can be deployed now and can integrate with future autonomous systems. -

-
-
-
- ); - } \ No newline at end of file diff --git a/portfolio/app/page.tsx b/portfolio/app/page.tsx index 63c5b48..840338d 100644 --- a/portfolio/app/page.tsx +++ b/portfolio/app/page.tsx @@ -1,185 +1,48 @@ -'use client' - -import Image from 'next/image' -import Link from 'next/link' -import { useState } from 'react' -import SkillsFilter from '../components/filters/SkillsFilter' -import ProjectCard from '../components/ProjectCard' -import { projects, getAllSkills, filterProjects } from '../lib/projectsData' - +import ConnectRow from '@/components/ConnectRow' +import Elsewhere from '@/components/Elsewhere' +import WorkSection from '@/components/sections/WorkSection' +import { site } from '@/lib/site' + +/** + * Three pages, segmented by who is asking. + * + * One page put everything in a single very long scroll; five put the content + * behind labels nobody had a reason to click. This is the middle: the home + * page carries what every visitor wants regardless of why they came, which is + * who he is, how to reach him, and the work. Anyone who wants to engage him + * goes to /services, anyone deciding whether to hire him goes to /about, and + * both are previewed here with enough substance to make the click informed. + * + * The line under the name names three practices rather than one. Developer + * relations is a thing Jay does, not the category he sits in. + * + * The hero was half a viewport of nothing: a name, one sentence and six links, + * with the work starting below the fold. Restraint is not the same as absence, + * and on the page someone reaches by tapping a business card the first screen + * has to carry something. So the name is set at the width of the page, the + * line and the links sit side by side under it rather than stacked, and the + * work itself starts immediately. + */ export default function Home() { - const [selectedSkills, setSelectedSkills] = useState([]) - const allSkills = getAllSkills() - - const handleSkillClick = (skill: string) => { - setSelectedSkills((prev) => - prev.includes(skill) ? prev.filter((s) => s !== skill) : [...prev, skill] - ) - } - - const filteredProjects = filterProjects(projects, selectedSkills) - return ( -
-

Jonathan (Jay) Albert's Portfolio

-

- Jay is a futurist, reverse-engineering the products and services of tomorrow today. Turning visions into projects, projects into milestones, and milestones into tasks, providing software development teams with roadmaps to the future. With over a decade of experience gathering requirements, rapid prototyping, managing, and coordinating cross-functional teams in both start-up and global operations. Boundless curiosity has led to work with organizations focused on AI, decentralized computing, blockchains, gaming, consulting, augmented reality, finance, politics, and language learning. Intent on bridging the gap between the future and today, confronting any challenge that gets in the way.{' '} -

-
- -

- Want to know Jay on a more personal level? View Jay's Alva Labs test results. -

- - -
-
- - Personality Profile - +
+
+

{site.name}

+
+

+ {site.standfirst} {site.location}. +

+
-
- - Logical Ability - -
-
- - - -
- - calendly - - - email - - - resume - - - linkedin - - - telegram - - - discord - - - github - - - medium - - - mirror - - - twitter - - - tiktok - - - instagram - -
- + - + {/* Above the work, not below it. Underneath eighty-seven cards these + two were past the point where anyone was still looking for a link: + the reader is scrolling the catalogue by then, not hunting for the + rest of the site. */} + -
- {filteredProjects.map((project) => ( - - ))} -
+
) } diff --git a/portfolio/app/playerchain/page.tsx b/portfolio/app/playerchain/page.tsx deleted file mode 100644 index aaf9d2a..0000000 --- a/portfolio/app/playerchain/page.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import Image from 'next/image' -import Link from 'next/link' -import { Typography } from '../../lib/mui' - -export default function PlayerChain() { - return ( -
-
-
- PlayerChain Development -

PlayerChain: A New Approach to Blockchain Gaming

-
- - {/* Rest of the content stays exactly the same */} -

Analyzed and presented on Playmint's innovative PlayerChain technology. PlayerChain represents a paradigm shift in blockchain gaming architecture, where each player operates their own blockchain that synchronizes with others, replacing the traditional global shared state model.

- -

Technical Overview

-
    -
  • Players act as nodes in the blockchain network, eliminating the need for separate consensus computers
  • -
  • Achieves significantly faster update times (50ms vs traditional 2000ms)
  • -
  • Each player maintains their own state that syncs with other players
  • -
  • Removes the requirement for gas fees in gaming transactions
  • -
- -

Development Journey

-
    -
  • Previous Challenges: Addressed issues with traditional blockchain gaming including transaction times and gas fees
  • -
  • Architecture Innovation: Rethought blockchain implementation specifically for gaming needs
  • -
  • Proof of Concept: Space Shooter game demonstrating PlayerChain's capabilities
  • -
  • Performance Gains: Dramatic improvement in response times for better gaming experience
  • -
- -

Impact on Game Development

-

PlayerChain technology represents a significant advancement in blockchain gaming, offering developers a new approach to building decentralized games without the traditional performance limitations of blockchain technology. This innovation enables more responsive gameplay while maintaining the benefits of decentralization.

- -
- Blockchain Gaming - Distributed Systems - Game Development - Technical Analysis - Architecture Design - Technical Writing - Research -
- -
- -
- -
-
- - Watch Stream - -
-
- - View Github Repository - -
-
- - Visit Playmint - -
-
-
-
- ) -} \ No newline at end of file diff --git a/portfolio/app/resources/page.tsx b/portfolio/app/resources/page.tsx deleted file mode 100644 index b433c96..0000000 --- a/portfolio/app/resources/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Resources() { - return (

This is the resources page!

); - } \ No newline at end of file diff --git a/portfolio/app/sdk-updates/page.tsx b/portfolio/app/sdk-updates/page.tsx deleted file mode 100644 index b7b299e..0000000 --- a/portfolio/app/sdk-updates/page.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import Image from 'next/image' -import Link from 'next/link' -import { Typography } from '../../lib/mui' - -export default function SDKUpdates() { - return ( -
-
-
- ChainSafe SDK Updates Review -

ChainSafe Gaming SDK Updates & Token Streaming Demo

-
- -

- Presented and demonstrated the latest updates to ChainSafe's - Web3.Unity SDK, including major improvements to blockchain integration - and a live demonstration of token streaming functionality. -

- -

Major SDK Updates

-
    -
  • - Multi-Chain Support: Added seamless switching - between multiple blockchains within a single project -
  • -
  • - Event Subscriptions: New support for subscribing to - blockchain events -
  • -
  • - Improved Developer Experience: Introduced Contract - ABI to C# generator for easier smart contract interactions -
  • -
  • - Enhanced WebGL Support: Added WalletConnect support - and improved Web3Auth workflow -
  • -
  • - Marketplace Sample: New demo showcasing NFT - marketplace functionality -
  • -
  • - MUD Integration: Initial support for building - on-chain autonomous worlds -
  • -
- -

Token Streaming Demo

-
    -
  • - Live demonstration of real-time token streaming implementation -
  • -
  • - Showcased continuous payment flows using blockchain technology -
  • -
  • Explained technical considerations and integration approaches
  • -
  • Discussed practical use cases and applications
  • -
- -

Breaking Changes & Architecture

-
    -
  • Web3Accessor renamed to Web3Unity for clarity
  • -
  • Unified wallet interface with single modal window
  • -
  • Simplified scene management for samples
  • -
  • Service adapter pattern implementation for better modularity
  • -
  • Immediate blockchain read access upon initialization
  • -
- -
- - Unity - - - Web3 - - - Blockchain Gaming - - - SDK Development - - - Live Demo - - - Technical Communication - - - Token Streaming - - - Developer Relations - -
- -
- -
- -
-
- - Watch Stream - -
-
- - View SDK Repository - -
-
-
-
- ) -} diff --git a/portfolio/app/services/page.tsx b/portfolio/app/services/page.tsx new file mode 100644 index 0000000..c86752d --- /dev/null +++ b/portfolio/app/services/page.tsx @@ -0,0 +1,26 @@ +import type { Metadata } from 'next' +import ServicesSection from '@/components/sections/ServicesSection' +import SpeakingSection from '@/components/sections/SpeakingSection' + +export const metadata: Metadata = { + title: 'Services', + description: + 'Speaking, advisory, custom development, and 3D printing. Formats, scope, and what you actually get.', +} + +/** + * One page for anyone who wants to engage Jay rather than read about him. + * + * Speaking lives here rather than on its own route because booking someone to + * run a workshop and hiring them to build a reference implementation are the + * same decision from the same kind of visitor, arriving with a budget and a + * date. + */ +export default function ServicesPage() { + return ( +
+ + +
+ ) +} diff --git a/portfolio/app/spaces/page.tsx b/portfolio/app/spaces/page.tsx deleted file mode 100644 index 082015c..0000000 --- a/portfolio/app/spaces/page.tsx +++ /dev/null @@ -1,152 +0,0 @@ -'use client' - -import Link from 'next/link' -import Image from 'next/image' - -export default function SpacesPage() { - const spaces = [ - { - title: 'AI Integration in Gaming', - date: 'February 2025', - description: - 'Explored how artificial intelligence is transforming game development and player experiences in the Web3 space.', - link: 'https://x.com/_WEB3M_/status/1889111873653793027', - image: '/images/ai-gaming.png', - imageAlt: 'Twitter Space on AI Integration in Gaming', - }, - { - title: 'Future of Web3 Gaming', - date: 'February 2025', - description: - 'Discussed the evolving landscape of Web3 gaming and the key trends shaping its future.', - link: 'https://x.com/_WEB3M_/status/1889471630860230932', - image: '/images/future-web3-gaming.png', - imageAlt: 'Twitter Space on Future of Web3 Gaming', - }, - { - title: 'Will AI Agents Start the Gaming Bull Run?', - date: 'January 2025', - description: - 'Explored the potential impact of AI agents in gaming, discussing how they could drive adoption and innovation in the blockchain gaming space.', - link: 'https://x.com/G7_DAO/status/1877739546827178250', - image: '/images/game7.jpeg', - imageAlt: 'Game7 Twitter Space on AI Agents in Gaming', - }, - { - title: 'Leaders in AI: Shaping the Next Era of Automation', - date: 'January 2025', - description: - 'Joined industry leaders to discuss the future of AI automation, sharing insights on how AI is transforming development workflows and creating new opportunities.', - link: 'https://x.com/_WEB3M_/status/1875306129628291488', - image: '/images/leaders-in-ai.png', - imageAlt: 'Twitter Space on AI Leadership and Automation', - }, - { - title: 'Building Your Dream: A Guide to Publishing Games in Web3', - date: 'October 2024', - description: - 'Shared practical insights and strategies for game developers looking to publish their games in the Web3 ecosystem, covering key considerations and best practices.', - link: 'https://x.com/_WEB3M_/status/1848050211723067686', - image: '/images/publishing.jpeg', - imageAlt: 'Twitter Space on Web3 Game Publishing', - }, - { - title: 'Rise of Web3 Gaming', - date: 'September 2024', - description: - 'Explored the growing momentum in Web3 gaming, discussing emerging trends, successful projects, and the future outlook for blockchain-based games.', - link: 'https://x.com/_WEB3M_/status/1832141772975444463', - image: '/images/rise-of-web3-gaming.jpeg', - imageAlt: 'Twitter Space on the Rise of Web3 Gaming', - }, - { - title: 'Security Best Practices in Web3', - date: 'April 2024', - description: - 'Discussed essential security considerations and best practices for Web3 developers and users, focusing on protecting assets and maintaining secure environments.', - link: 'https://x.com/TimeToTerminal/status/1775168818131722482', - image: '/images/web3-security.jpeg', - imageAlt: 'Twitter Space on Web3 Security Best Practices', - }, - { - title: 'Responsible Gaming in Web3', - date: 'April 2024', - description: - 'Discussed the importance of responsible gaming practices in Web3, exploring frameworks and strategies to ensure player well-being in blockchain gaming.', - link: 'https://x.com/SNEGbet/status/1777653520088301877', - image: '/images/responsible-gaming.jpeg', - imageAlt: 'SNEG Twitter Space on Responsible Gaming', - }, - { - title: 'Rewriting the Rules of Game Design', - date: 'March 2024', - description: - 'Explored how Web3 technology is transforming traditional game design principles, discussing new opportunities and challenges in blockchain game development.', - link: 'https://x.com/ChainSafeGaming/status/1764971150382117222', - image: '/images/rewriting-the-rules.jpeg', - imageAlt: 'ChainSafe Gaming Twitter Space on Game Design Innovation', - }, - { - title: 'Gaming Analytics and Growth', - date: 'May 2023', - description: - 'Discussed the role of analytics in gaming with Covalent, exploring how data insights drive game development and player engagement strategies.', - link: 'https://x.com/Covalent_HQ/status/1655009610623844353', - image: '/images/covalent.jpeg', - imageAlt: 'Covalent Twitter Space on Gaming Analytics', - }, - ] - - return ( -
-
-

Twitter Spaces

-

- A collection of Twitter Spaces where I discuss various aspects of Web3 - gaming, AI integration, and game development. These conversations - explore emerging trends and opportunities in the intersection of - gaming and blockchain technology. -

- -
- {spaces.map((space, index) => ( -
-
-
- {space.imageAlt} -
-
-
-

{space.title}

- - {space.date} - -
-

- {space.description} -

- - View Space → - -
-
-
- ))} -
-
-
- ) -} diff --git a/portfolio/app/streams/page.tsx b/portfolio/app/streams/page.tsx deleted file mode 100644 index 37cb242..0000000 --- a/portfolio/app/streams/page.tsx +++ /dev/null @@ -1,162 +0,0 @@ -'use client' - -import Link from 'next/link' -import Image from 'next/image' - -export default function StreamsPage() { - const streams = [ - { - title: 'Moddio Game Engine Stream', - date: 'January 2025', - description: - 'Explored how Moddio is transforming game development with AI-powered tools and innovative funding through indie.fun. Discussed their vision for the future of game creation and upcoming developments in the space.', - link: 'https://www.youtube.com/watch?v=OsfmODTBQaA', - image: '/images/Moddio.png', - imageAlt: 'Moddio Game Engine Stream', - }, - { - title: 'Discover Games & Whalepass Stream', - date: 'January 2025', - description: - 'A deep dive with Hayder from Discover Games and Whalepass, exploring their platform that bridges Web2 and Web3 gaming. Featured their developer tools including battle passes and strategies for enhancing player engagement.', - link: 'https://www.youtube.com/watch?v=eOjg6ZJvpTM', - image: '/images/discover-games.png', - imageAlt: 'Discover Games Platform Stream', - }, - { - title: 'Farworld Stream', - date: 'January 2025', - description: - 'Showcased Farworld and explored the evolution of AI in gaming. Discussed their innovative approaches to game development and the future landscape of AI-enhanced gaming experiences.', - link: 'https://www.youtube.com/watch?v=8wQHD2qj8bs', - image: '/images/farworld.png', - imageAlt: 'Farworld AI Gaming Stream', - }, - { - title: 'Playermint Stream', - date: 'December 2024', - description: - 'Explored a new approach to decentralized gaming where each player operates their own blockchain instead of relying on a global shared state.', - link: 'https://www.youtube.com/watch?v=1AgjLWaMQvw', - image: '/images/playerchain.png', - imageAlt: - 'Chainsafe stream banner featuring David Amor, Martin Maurer, and Jay Albert', - }, - { - title: 'ChainSafe Gaming SDK Update Stream', - date: 'November 2024', - description: - "Presented major updates to ChainSafe's Web3.Unity SDK and demonstrated token streaming functionality in a live stream", - link: 'https://www.youtube.com/live/rgArGYfoT8o', - image: '/images/sdk-updates.png', - imageAlt: 'ChainSafe Gaming SDK Updates stream screenshot', - }, - { - title: 'Hatchyverse Stream', - date: 'October 2024', - description: - 'An in-depth analysis of Hatchyverse, a decentralized gaming ecosystem that allows community members to build games using shared IP.', - link: 'https://www.youtube.com/watch?v=QNr3moS0B3o', - image: '/images/hatchyverse.png', - imageAlt: 'Hatchyverse platform analysis and overview', - }, - { - title: 'TruWorld Stream', - date: 'October 2024', - description: - 'Explored TruWorld, an AR geo-location game that combines NFTs with real-world exploration. Similar to Pokemon Go but with blockchain integration.', - link: 'https://www.youtube.com/watch?v=Y9EHHzF6Rzw', - image: '/images/trumen-world.png', - imageAlt: 'TruWorld stream showcase', - }, - { - title: 'CLUAIDO Stream', - date: 'September 2024', - description: - 'Explored CLUAIDO, an AI-powered detective game where players solve procedurally generated murder cases by freely interrogating AI-driven suspects.', - link: 'https://www.youtube.com/watch?v=SdJ9oo9d2Os', - image: '/images/cluaido-stream.png', - imageAlt: 'CLUAIDO game stream showcase', - }, - { - title: 'Quantum Command Stream', - date: 'September 2024', - description: - 'Streamed with Quantum Command, a shooter game that adapts its maps based on player behavior and equipment.', - link: 'https://www.youtube.com/watch?v=RMemvjcBoNE', - image: '/images/quantum-command.png', - imageAlt: 'Quantum Command game analysis stream', - }, - { - title: 'HackFS Winner Stream', - date: 'July 2024', - description: - 'Featured the winning project from HackFS, showcasing a PvP battle game that combines Monaverse maps with ChainSafe SDK.', - link: 'https://www.youtube.com/watch?v=rTQhJHbDaWM', - image: '/images/hackfs-winner.png', - imageAlt: 'HackFS Winner stream showing NFT battle game demo', - }, - { - title: 'Hyperplay & Coinracer Stream', - date: 'July 2024', - description: - 'Explored Hyperplay, a trusted game launcher for blockchain games, featuring Coinracer.', - link: 'https://www.youtube.com/watch?v=70DahC2GHXs', - image: '/images/hyperplay-stream.jpeg', - imageAlt: 'Hyperplay and Coinracer platform showcase stream', - }, - ] - - return ( -
-
-

Game Development Streams

-

- A collection of live streams focused on innovative teams building the - future of gaming. These streams showcase various projects, - technologies, and educational content for the blockchain gaming - community. -

- -
- {streams.map((stream, index) => ( -
-
-
- {stream.imageAlt} -
-
-
-

{stream.title}

- - {stream.date} - -
-

- {stream.description} -

- - Watch Stream → - -
-
-
- ))} -
-
-
- ) -} diff --git a/portfolio/app/the-fourth-branch/page.tsx b/portfolio/app/the-fourth-branch/page.tsx deleted file mode 100644 index 125aa90..0000000 --- a/portfolio/app/the-fourth-branch/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import "/main.css" -import ProjectCover from "../../components/project-cover"; - - -export default function Fourth() { - return ( -
- - {/* This is the image card that appears at the top of every project page. */} -
- - -
-

What is The Fourth Branch?

-

The Fourth Branch summarizes bills being debated in Congress, allows users to vote on the summaries, and compares their votes to those of their Congressmen.

-

Demo

-

The video below is a demo I recorded of the final product for desktops.

-
- -
-
-
-
- ); - } \ No newline at end of file diff --git a/portfolio/app/thought-card/page.tsx b/portfolio/app/thought-card/page.tsx deleted file mode 100644 index 8c51ee3..0000000 --- a/portfolio/app/thought-card/page.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import "/main.css" -import ProjectCover from "../../components/project-cover"; - - -export default function Thought() { - return ( -
- - {/* This is the image card that appears at the top of every project page. */} -
- - -
-

Augmented Business Card

-

After sharing some augmented reality ideas with a friend, he requested that I augment his business card. Over several years, he compiled quotes that he felt moved by and wanted to share the quotes with the people who had his business card. He called the idea a “Thought Card”. -

- After a couple of weeks, I was able to include the quotes using an image target, and I decided to include a virtual button that would link viewers to a store where they could purchase the book. The links can be swapped out for affiliated marketing links, allowing the card owner to profit from viewers who bought the book. -

-

Application Preview

-
- -
-
-
-
- ); - } \ No newline at end of file diff --git a/portfolio/app/tokensets/page.tsx b/portfolio/app/tokensets/page.tsx deleted file mode 100644 index d7ff3fe..0000000 --- a/portfolio/app/tokensets/page.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import "/main.css" -import Link from "next/link"; -import ProjectCover from "../../components/project-cover"; - - -export default function Tokensets() { - return ( -
- - {/* This is the image card that appears at the top of every project page. */} -
- - -
-

Exploring Web3 Finance

-

I started investing in Bitcoin and alternative coins in 2014 while working at a mutual fund. I was one of a handful of people at the company who were exposed to Bitcoin. The Associate Vice President of Investments Management Technology and I discussed offering a crypto product to clients but found that the market cap was too small, and mutual funds were too heavily regulated, so we would never get approval. Years later, I was happy to discover the emergence of DeFi. -

- While working on the Mix3d community I had several friends and family ask, what Cryptocurrencies they should invest in. Besides legal reasons, I try to avoid giving any financial advice because novice investors act on emotion and are inconsistent. I have considered launching a fund several times, and DeFi presented an opportunity to create a strategy without any organization becoming the custodian of the funds. I found Token Sets and put together a strategy on the Polygon network to minimize trading fees while having access to most tokens. The strategy is weighted by market cap and includes all of the currencies that I thought would be crucial to the development of Web3. -

- While Token Sets is an incredible tool, ultimately I was unsure of the legal implications and did not want to take the leap of being responsible for the funds of family and friends. Luckily, I did not move forward because LUNA imploded months later. -

-
- View the Strategy -
-

Institutional Web3 Investing

-

In late 2022, I was searching for my next role and applied to a firm that creates crypto indexes for ETF issuers. The firm asked me to put together an index, and I chose to create an index for smart contracts. I provided the document below, and it led me to the next round of the interview process. I was in the last rounds of interviewing for a few different companies and decided to accept an offer at another organization instead of continuing the interview process. Below is the strategy, and my rationalization for investing in a Smart Contract Index.

-
-
- -
- -
-
-
- ); - } \ No newline at end of file diff --git a/portfolio/app/traffic-jam-vr/page.tsx b/portfolio/app/traffic-jam-vr/page.tsx deleted file mode 100644 index c79d08d..0000000 --- a/portfolio/app/traffic-jam-vr/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import "/main.css" -import Link from "next/link"; -import ProjectCover from "../../components/project-cover"; - - -export default function Traffic() { - return ( -
- - {/* This is the image card that appears at the top of every project page. */} -
- - -
-

Virtual Reality Game Jam

-

I find virtual and augmented reality fascinating, and I spend endless nights considering the possibilities and implications for humanity. I want to enter the space, and I joined a meetup group for virtual reality to learn about development. The group posted an event called “VR Game Jam” and sought to bring together developers, digital artists, and audiographers to compete to make the best virtual reality game in 48 hours. The theme of the game jam was to make everyday life interesting, and our team decided to make a game out of a traffic cop's experience. Our idea was to have the user stand in the middle of an intersection and control the flow of traffic, with the goal of avoiding a collision and losing the game. Each car would have varying speeds and wait times before they chose to ignore the traffic controller and proceed on their own. -

- I initially joined the team as a developer, but two other members of our team had strong development backgrounds, so I took the opportunity to learn about the other roles. I was exposed to Unity and was tasked with placing the digital assets for the map, finding audio clips for the cars, and helping with the game design. -

-
- Download the Game -
-
-
-
- ); - } \ No newline at end of file diff --git a/portfolio/app/web3-game-workshop/page.tsx b/portfolio/app/web3-game-workshop/page.tsx deleted file mode 100644 index 761b928..0000000 --- a/portfolio/app/web3-game-workshop/page.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import Image from 'next/image' -import Link from 'next/link' -import { Typography } from '../../lib/mui' - -export default function Web3GameWorkshop() { - return ( -
-
-
- ETHDenver Workshop -

Web2 to Web3 Game Development Workshop

-
- - {/* Rest of the content stays exactly the same */} -

Led a hands-on workshop at ETHDenver 2023 demonstrating how to transform traditional web games into blockchain-enabled experiences. The session focused on maintaining engaging gameplay while leveraging blockchain features for enhanced functionality.

- -

Workshop Content

-
    -
  • Step-by-step guide on integrating Web3 features into existing games
  • -
  • Best practices for blockchain integration in gaming
  • -
  • Live coding demonstration using real game examples
  • -
  • Discussion of common challenges and solutions
  • -
- -

Technical Topics Covered

-
    -
  • Smart Contract Integration: Adding blockchain functionality to game mechanics
  • -
  • Wallet Connectivity: Implementing Web3 wallet connections
  • -
  • Asset Management: Handling in-game assets on the blockchain
  • -
  • User Experience: Maintaining smooth gameplay while adding Web3 features
  • -
- -

Workshop Objectives

-

The workshop aimed to bridge the gap between traditional game development and blockchain technology. Participants learned practical approaches to Web3 game development that could be immediately applied to their own projects, while understanding the advantages and challenges of blockchain integration in gaming.

- -
- Game Development - Web3 - Smart Contracts - Public Speaking - Technical Education - Live Coding - Workshop Facilitation - Blockchain -
- -
- -
- -
-
- - Watch Workshop - -
-
-
-
- ) -} \ No newline at end of file diff --git a/portfolio/app/werewolf/page.tsx b/portfolio/app/werewolf/page.tsx deleted file mode 100644 index a4d0aab..0000000 --- a/portfolio/app/werewolf/page.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import "/main.css" -import Link from "next/link"; -import ProjectCover from "../../components/project-cover"; - - -export default function Werewolf() { - return ( -
- - {/* This is the image card that appears at the top of every project page. */} -
- - -
-

Fullstack Academy Capstone

-

For the final project at Fullstack Academy, we were split into teams of four and given two and a half weeks to define and develop a project. Everyone in our group was a fan of tabletop games, and we decided to give new life to an old concept. The game we settled on was Werewolf. Werewolf is a social deduction game where some players are on the “bad” team and others are on the “good” team, but neither player knows the other's identity. Throughout the game, players vote to kill each other based on their suspicions, and the last team standing wins the game. Our team put a new spin on the game by allowing groups to form in online video conferences and by programmatically managing all of the events in the game. -

-

Gameplay Demo

-
- -
-

Game Rules

-

The game proceeds in alternating night and day rounds, beginning with night. -

- At the beginning of the night, only the werewolves could see. Everyone else is sleeping, so their video is stopped. While everyone sleeps, the werewolves will choose a villager to kill. When the werewolves have made their kill, they too sleep. -

- After the werewolves have made their kill, the village doctor awakens and, not knowing who has been killed, chooses a villager (who can be themselves) to heal. The villager chosen will survive the night if the werewolves choose to kill them. Once the doctor has made their choice, they go back to sleep. -

- Next, the village seer awakens. The seer can choose one villager to have their true identity revealed. If the villager chosen is a werewolf, the seer will be told so. Otherwise, the seer returns to sleep. -

- Once the seer has finished, we transition to daytime, and the villagers find out who has been killed during the night. That villager is immediately dead and out of the game. They do not reveal their identity. -

- Daytime is very simple; all the living villagers gather and decide whom to kill in hopes of ridding themselves of werewolves. As soon as a majority votes for a single villager to be killed, that villager is immediately dead and out of the game. -

- There are no restrictions on speech. Any living villager can say anything they want—truth, misdirection, nonsense, or a bare-faced lie. Dead villagers may not speak at all. If a villager senses the other villagers are beginning to turn on them and they want to protest their innocence or reveal some information (like the seer`s visions), they must do it before the votes go through. -

- Once a player is killed, night falls, and the cycle repeats. -

-

Winning:

-

The villagers win if they kill all of the werewolves. -

- The werewolves win if they kill enough villagers so that the numbers are even. (Example: Two werewolves and two villagers) -

-
- View the Code -
-
-
-
- ); - } \ No newline at end of file diff --git a/portfolio/app/wix/page.tsx b/portfolio/app/wix/page.tsx deleted file mode 100644 index 57e0d93..0000000 --- a/portfolio/app/wix/page.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import "/main.css" -import Link from "next/link"; -import ProjectCover from "../../components/project-cover"; -import Image from "next/image"; - - -export default function Wix() { - return ( -
- - {/* This is the image card that appears at the top of every project page. */} -
- - -
-

Freelance Work

-

While searching for my next career, I reached out to a few friends and family, letting everyone know that I was open to new opportunities. During that time, a friend connected me with a couple of businesses that were looking to create websites for their services. Below are the businesses I worked with and the websites I delivered using the Wix website builder. -

-
-
-
-
- A long triangle pierces two circles. One circle is inside another. -
-
-

Health Shield Solutions

-

Health Shield Solutions' unique two-step process disinfects to kill existing viruses, bacteria, and other pathogens on surfaces and then immediately re-surfaces the entire area and objects with a nano barrier protectant that provides continuous protection from re-infection for 30-90 days

-
-
-
-
-
- A black box with the letters RT in bold -
-
-

RT Advisory Team

-

RT Advisory Team is a growth and compliance consulting firm. The firm's services include consulting for patents, branding, product manufacturing, product placement, and much more.

-
-
-
-
-
-
- ); - } \ No newline at end of file diff --git a/portfolio/app/work/[slug]/page.tsx b/portfolio/app/work/[slug]/page.tsx new file mode 100644 index 0000000..f001b63 --- /dev/null +++ b/portfolio/app/work/[slug]/page.tsx @@ -0,0 +1,343 @@ +import type { Metadata } from 'next' +import Image from 'next/image' +import Link from 'next/link' +import { notFound } from 'next/navigation' +import { allEntries, coverSize, formatDate, MEDIUM_LABELS, STATUS_NOTES } from '@/lib/content' +import { writeupFor, writeups } from '@/lib/content/writeups' +import type { Block } from '@/lib/content/writeups' +import EntryCard from '@/components/EntryCard' + +export function generateStaticParams() { + return writeups.map((w) => ({ slug: w.slug })) +} + +function entryForDetail(slug: string) { + return allEntries.find((e) => e.detail === slug) +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ slug: string }> +}): Promise { + const { slug } = await params + const entry = entryForDetail(slug) + if (!entry) return {} + return { + title: entry.title, + description: entry.summary ?? entry.title, + openGraph: { title: entry.title, description: entry.summary ?? entry.title, images: entry.cover ? [entry.cover.src] : [] }, + } +} + +function BlockView({ block }: { block: Block }) { + switch (block.kind) { + case 'youtube': + return ( +
+
+