2026-04-29 14:39:34 -05:00
< script setup lang = "ts" >
2026-07-30 09:21:34 -05:00
import {
DEFAULT _DESCRIPTION ,
DEFAULT _IMAGE ,
DEFAULT _TITLE ,
SITE _URL ,
personId ,
personSchema ,
websiteId ,
} from '~/utils/site' ;
2026-04-29 14:39:34 -05:00
2026-07-30 09:21:34 -05:00
const webpageId = ` ${ SITE _URL } /#homepage ` ;
2026-06-22 00:44:50 -05:00
2026-06-22 00:28:43 -05:00
useSeoMeta ( {
2026-07-30 09:21:34 -05:00
title : DEFAULT _TITLE ,
ogTitle : DEFAULT _TITLE ,
description : DEFAULT _DESCRIPTION ,
ogDescription : DEFAULT _DESCRIPTION ,
ogUrl : SITE _URL ,
ogType : 'profile' ,
ogImage : DEFAULT _IMAGE ,
ogImageAlt : DEFAULT _TITLE ,
twitterCard : 'summary' ,
twitterTitle : DEFAULT _TITLE ,
twitterDescription : DEFAULT _DESCRIPTION ,
twitterImage : DEFAULT _IMAGE ,
} ) ;
useHead ( {
link : [ { rel : 'canonical' , href : SITE _URL } ] ,
2026-06-22 00:28:43 -05:00
} ) ;
const selectedWorks = [
{
title : 'Edlink' ,
description :
"I built many things I'm proud of at Edlink. Notably, I designed and shipped Edlink’ s internal billing platform for ~150 clients, a flat file ingestion engine that enabled arbitrary data mapping, client-embedded UI widgets, user onboarding flows, and a secure sandbox for running untrusted code with a live data-preview service." ,
link : 'https://ed.link' ,
images : [
{
src : '/work/edlink/embedded-onboarding.gif' ,
alt : 'Embedded Onboarding' ,
} ,
{
src : '/work/edlink/authentication-widget.gif' ,
alt : 'Auth Widget' ,
} ,
{
src : '/work/edlink/integration-sharing-rules-widget.png' ,
alt : 'Data Sharing Editor Widget' ,
} ,
{
src : '/work/edlink/billing-overview.png' ,
alt : 'Billing Overview' ,
} ,
{
src : '/work/edlink/invoice-sheet.png' ,
alt : 'Invoice View' ,
} ,
{
src : '/work/edlink/add-payment-method.png' ,
alt : 'Add Payment Method' ,
} ,
{
src : '/work/edlink/onboarding-select-provider.png' ,
alt : 'Provider Selection' ,
} ,
{
src : '/work/edlink/onboarding-edit-source.png' ,
alt : 'Source Creation' ,
} ,
{
src : '/work/edlink/onboarding-start.png' ,
alt : 'Onboarding Start' ,
} ,
{
src : '/work/edlink/custom-code-editor.png' ,
alt : 'Custom Code Editor' ,
} ,
{
src : '/work/edlink/edit-licensing-rules.png' ,
alt : 'Licensing Editor' ,
} ,
{
src : '/work/edlink/materialization-preview.png' ,
alt : 'Pending Data Change Preview' ,
} ,
] ,
} ,
] ;
const projects = [
{
title : 'Unicord' ,
href : 'https://unicord.hat.fish/' ,
description : 'Discord bot SDK inspired by webserver architecture.' ,
} ,
{
title : 'Mystwright' ,
href : 'https://mystwright.com/' ,
description :
'An AI-driven mystery detective game. Generate, investigate, solve.' ,
} ,
{
title : 'Rune Buddy' ,
href : 'https://github.com/jaronpate/rift-buddy' ,
description :
'League of Legends runepage manager. Save & organise your pages for free.' ,
} ,
] ;
2026-06-22 00:44:50 -05:00
useHead ( {
script : [
{
key : 'json-ld-homepage' ,
type : 'application/ld+json' ,
innerHTML : JSON . stringify ( {
'@context' : 'https://schema.org' ,
2026-06-28 18:50:53 -05:00
'@graph' : [
{
'@type' : 'ProfilePage' ,
'@id' : webpageId ,
2026-07-30 09:21:34 -05:00
url : SITE _URL ,
name : DEFAULT _TITLE ,
description : DEFAULT _DESCRIPTION ,
2026-06-28 18:50:53 -05:00
inLanguage : 'en-US' ,
isPartOf : { '@id' : websiteId } ,
about : { '@id' : personId } ,
2026-07-30 09:21:34 -05:00
// Google requires mainEntity to be an inline Person with name —
// a bare @id reference fails Rich Results validation.
mainEntity : personSchema ( ) ,
2026-06-28 18:50:53 -05:00
primaryImageOfPage : {
'@type' : 'ImageObject' ,
2026-07-30 09:21:34 -05:00
url : DEFAULT _IMAGE ,
2026-06-28 18:50:53 -05:00
} ,
} ,
{
'@type' : 'ItemList' ,
'@id' : ` ${ webpageId } #selected-work ` ,
name : 'Selected work' ,
itemListElement : selectedWorks . map ( ( work , index ) => ( {
'@type' : 'ListItem' ,
position : index + 1 ,
item : {
'@type' : 'CreativeWork' ,
name : work . title ,
description : work . description ,
url : work . link ,
creator : { '@id' : personId } ,
} ,
} ) ) ,
} ,
{
'@type' : 'ItemList' ,
'@id' : ` ${ webpageId } #projects ` ,
name : 'Projects' ,
itemListElement : projects . map ( ( project , index ) => ( {
'@type' : 'ListItem' ,
position : index + 1 ,
item : {
2026-07-30 09:21:34 -05:00
'@type' : 'CreativeWork' ,
2026-06-28 18:50:53 -05:00
name : project . title ,
description : project . description ,
url : project . href ,
author : { '@id' : personId } ,
} ,
} ) ) ,
} ,
2026-06-22 00:44:50 -05:00
] ,
} ) ,
} ,
] ,
} ) ;
2026-04-29 23:13:10 -05:00
definePageMeta ( {
2026-06-12 20:22:56 -05:00
layout : false ,
} ) ;
2026-04-29 14:39:34 -05:00
< / script >
< template >
2026-08-22 11:59:33 -05:00
< NuxtLayout name = "main" >
< article >
< h3 > Selected Work < / h3 >
< template v-for = "work in selectedWorks" :key="work.title" >
< p >
< a :href = "work.link" target = "_blank" rel = "noopener" > { {
work . title
} } < / a >
— { { work . description } }
< / p >
< div class = "work__images" >
< figure
v-for = "image in work.images"
:key = "image.src"
class = "work__image"
2026-06-12 20:22:56 -05:00
>
2026-08-22 11:59:33 -05:00
< img :src = "image.src" :alt = "image.alt" / >
< / figure >
2026-06-22 00:28:43 -05:00
< / div >
2026-08-22 11:59:33 -05:00
< / template >
< h3 > Projects < / h3 >
< ul >
< li v-for = "project in projects" :key="project.href" >
2026-08-20 19:37:10 -05:00
< p >
2026-08-22 11:59:33 -05:00
< a :href = "project.href" target = "_blank" rel = "noopener" > { {
project . title
} } < / a >
— { { project . description } }
2026-04-29 23:13:10 -05:00
< / p >
2026-08-22 11:59:33 -05:00
< / li >
< / ul >
< h3 > About < / h3 >
< p >
I left school , my job , and moved to Austin in 2022 to write code
full time . That was definitely one of the best and most life
changing decisions I 've ever made. I' ve learned a lot since then
and < em > still < / em > have much more to learn .
< / p >
< p >
These days I build things because I find joy in making something
someone else really enjoys using . I get excited about reaching a
level of polish that users don 't even notice.
</p>
<p>
In a time where code is cheap I pride myself in taste and
execution. The best sticking point — in my limited opinion so far
— is the way using your product <em>feels</em>. So I want
everything I make to simply <em>feel</em> the best to use.
</p>
<p>
If you want to build something cool or connect
<a href="mailto:yo@jp.wtf">hit me up</a> — we' ll grab a coffee or
beer if you ' re in Austin ! I still dont know enough people to form
a band yet ; )
< / p >
< / article >
< / NuxtLayout >
2026-04-29 14:39:34 -05:00
< / template >
2026-05-02 21:28:14 -05:00
< style scoped >
2026-06-22 00:28:43 -05:00
. work _ _images {
2026-08-22 11:59:33 -05:00
-- dot - color : # 666 ;
2026-06-22 00:28:43 -05:00
display : grid ;
grid - auto - flow : column ;
grid - auto - columns : max - content ;
gap : var ( -- gap ) ;
overflow - x : auto ;
overflow - y : hidden ;
2026-08-22 11:59:33 -05:00
margin : 1.25 rem 0 1.75 rem ;
padding - bottom : 0.35 rem ;
2026-06-22 00:28:43 -05:00
}
. work _ _image {
-- dot - size : 6 % ;
margin : 0 ;
padding : var ( -- gap ) ;
flex - shrink : 0 ;
border - radius : var ( -- radius ) ;
2026-08-22 11:59:33 -05:00
background - color : var ( -- bg - secondary ) ;
2026-06-22 00:28:43 -05:00
background - image : radial - gradient (
circle at 4 px 4 px ,
var ( -- dot - color ) var ( -- dot - size ) ,
transparent calc ( var ( -- dot - size ) + 1 % )
) ;
background - position : 4 px 4 px ;
background - size : 12 px 12 px ;
background - repeat : repeat ;
display : flex ;
align - items : center ;
justify - content : center ;
}
. work _ _image img {
2026-08-22 11:59:33 -05:00
max - width : none ;
width : auto ;
height : auto ;
2026-06-22 00:28:43 -05:00
max - height : 350 px ;
2026-08-22 11:59:33 -05:00
object - fit : cover ;
border - radius : var ( -- radius ) ;
background : none ;
2026-06-22 00:28:43 -05:00
}
@ media screen and ( max - width : 750 px ) {
. work _ _image img {
max - height : 300 px ;
}
}
2026-04-29 14:39:34 -05:00
< / style >