2026-06-12 20:22:56 -05:00
< script lang = "ts" >
// module-scoped so the reveal only plays once per visit
let sectionsRevealed = false ;
< / script >
2026-04-29 14:39:34 -05:00
< script setup lang = "ts" >
import { ref } from 'vue' ;
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
} ) ;
2026-04-29 16:32:57 -05:00
const titles = [
2026-06-22 00:28:43 -05:00
"Hey, I'm Jaron." ,
2026-04-29 16:32:57 -05:00
'Nice to meet you.' ,
'Welcome.' ,
'Willkommen.' ,
'Bienvenue.' ,
] ;
2026-04-29 14:39:34 -05:00
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-06-22 00:28:43 -05:00
const photos = [
{
src : '/photography/athens_red_scooter.jpg' ,
alt : 'Red scooter in Athens' ,
location : 'Athens' ,
year : '2026' ,
} ,
{
src : '/photography/athens_subway.jpg' ,
alt : 'Athens metro station' ,
location : 'Athens' ,
year : '2026' ,
} ,
{
src : '/photography/athens_sunset.jpg' ,
alt : 'Athens at sunset' ,
location : 'Athens' ,
year : '2026' ,
} ,
{
src : '/photography/london_porsche.jpg' ,
alt : 'Porsche shop in London' ,
location : 'London' ,
year : '2025' ,
} ,
{
src : '/photography/underground.jpg' ,
alt : 'Entrace to the London underground' ,
location : 'London' ,
year : '2026' ,
} ,
{
src : '/photography/austin_parking.jpg' ,
alt : 'Parking garage in Austin' ,
location : 'Austin' ,
year : '2024' ,
} ,
] ;
2026-04-29 14:39:34 -05:00
const pageTitle = ref ( titles [ 0 ] ) ;
const rotateTitle = ( ) => {
setTimeout ( ( ) => {
const i = titles . findIndex ( ( t ) => t === pageTitle . value ) ;
pageTitle . value = titles [ i + 1 ] ? ? titles [ 0 ] ? ? 'Oops!' ;
rotateTitle ( ) ;
} , 2000 ) ;
} ;
2026-04-29 23:13:10 -05:00
definePageMeta ( {
2026-06-12 20:22:56 -05:00
layout : false ,
} ) ;
const { $gsap } = useNuxtApp ( ) ;
const pageRoot = ref < HTMLElement | null > ( null ) ;
onMounted ( ( ) => {
2026-06-22 00:28:43 -05:00
rotateTitle ( ) ;
useHead ( {
title : pageTitle ,
} ) ;
2026-06-12 20:22:56 -05:00
if ( ! $gsap || ! pageRoot . value || sectionsRevealed ) return ;
sectionsRevealed = true ;
const reduced = window . matchMedia (
'(prefers-reduced-motion: reduce)' ,
) . matches ;
const ctx = $gsap . context ( ( ) => {
if ( reduced ) return ;
// sections in view on load are revealed by the layout's intro;
// below-the-fold sections rise in as they enter the viewport
$gsap . utils . toArray < HTMLElement > ( 'main section' ) . forEach ( ( el ) => {
const inView = el . getBoundingClientRect ( ) . top < window . innerHeight ;
if ( inView ) return ;
$gsap . from ( el , {
autoAlpha : 0 ,
y : 24 ,
duration : 0.6 ,
ease : 'power2.out' ,
scrollTrigger : {
trigger : el ,
start : 'top 92%' ,
} ,
} ) ;
} ) ;
} , pageRoot . value ) ;
onBeforeUnmount ( ( ) => ctx . revert ( ) ) ;
2026-04-29 23:13:10 -05:00
} ) ;
2026-04-29 14:39:34 -05:00
< / script >
< template >
2026-06-12 20:22:56 -05:00
< div ref = "pageRoot" >
< NuxtLayout name = "main" >
2026-07-29 20:12:44 -05:00
< section class = "rows" >
2026-06-12 20:22:56 -05:00
< h5 > Selected Work < / h5 >
< div class = "section__content" >
< a
2026-06-22 00:28:43 -05:00
v-for = "work in selectedWorks"
:key = "work.title"
class = "work"
:href = "work.link"
2026-06-12 20:22:56 -05:00
target = "_blank"
>
2026-06-22 00:28:43 -05:00
< h3 class = "work__title" > { { work . title } } < / h3 >
< p class = "work__description" >
{ { work . description } }
2026-06-12 20:22:56 -05:00
< / p >
2026-06-22 00:28:43 -05:00
< div class = "work__images" >
< figure
v-for = "image in work.images"
:key = "image.src"
class = "work__image"
>
< img :src = "image.src" :alt = "image.alt" / >
< / figure >
< / div >
2026-06-12 20:22:56 -05:00
< / a >
2026-06-22 00:28:43 -05:00
< / div >
< / section >
2026-07-29 20:12:44 -05:00
< section class = "rows" >
2026-06-22 00:28:43 -05:00
< h5 > Projects < / h5 >
< div class = "section__content" >
2026-06-12 20:22:56 -05:00
< a
2026-06-22 00:28:43 -05:00
v-for = "project in projects"
:key = "project.href"
:href = "project.href"
2026-06-12 20:22:56 -05:00
target = "_blank"
class = "project"
>
2026-06-22 00:28:43 -05:00
< h3 class = "project__title" > { { project . title } } < / h3 >
2026-06-12 20:22:56 -05:00
< p class = "project__description" >
2026-06-22 00:28:43 -05:00
{ { project . description } }
2026-06-12 20:22:56 -05:00
< / p >
< / a >
< / div >
< / section >
< section class = "about" >
< h5 > About < / h5 >
< div class = "section__content" >
< p >
2026-08-20 19:37:10 -05:00
I left school , my job , and moved 800 + miles 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 .
2026-04-29 23:13:10 -05:00
< / p >
2026-06-12 20:22:56 -05:00
< p >
2026-08-20 19:37:10 -05:00
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 utility.
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 class="basic-link" href="mailto:yo@jp.wtf">hit me up</a>
— we' ll grab a coffee or beer if you 're in Austin! I
2026-06-14 17:29:58 -05:00
still dont know enough people to form a band yet ;)
2026-04-29 23:13:10 -05:00
</p>
2026-06-12 20:22:56 -05:00
</div>
</section>
2026-06-14 17:29:58 -05:00
<!-- <section class="photos">
2026-06-12 20:22:56 -05:00
<h5>Photos</h5>
<div class="section__content">
<div class="photo-grid">
2026-06-22 00:28:43 -05:00
<div
v-for="photo in photos"
:key="photo.src"
class="photo-grid__item"
>
2026-06-12 20:22:56 -05:00
<div class="photo__image">
2026-06-22 00:28:43 -05:00
<img :src="photo.src" :alt="photo.alt" />
2026-06-12 20:22:56 -05:00
</div>
<div class="photo__overlay">
2026-06-22 00:28:43 -05:00
<p>{{ photo.location }}</p>
<p>{{ photo.year }}</p>
2026-06-12 20:22:56 -05:00
</div>
2026-05-02 21:03:02 -05:00
</div>
</div>
</div>
2026-06-14 17:29:58 -05:00
</section> -->
2026-06-12 20:22:56 -05:00
</NuxtLayout>
</div>
2026-04-29 14:39:34 -05:00
</template>
2026-05-02 21:28:14 -05:00
<style scoped>
2026-04-29 14:39:34 -05:00
section {
2026-07-29 20:12:44 -05:00
margin-top: calc(var(--gap) * 2);
2026-04-29 14:39:34 -05:00
}
section > .section__content {
2026-07-29 20:12:44 -05:00
padding: 0 calc(var(--gap) * 2);
2026-04-29 14:39:34 -05:00
}
2026-05-02 21:28:14 -05:00
/* section:not(:has(h5)) > .section__content {
padding: 2rem;
} */
2026-04-29 14:39:34 -05:00
section > h5 {
2026-07-29 20:12:44 -05:00
--flare-length: 1.125rem; /* how far the flare extends horizontally */
--flare-spread: 1.125rem; /* how far the arms open up/down */
2026-04-29 14:39:34 -05:00
font-weight: 400;
font-style: italic;
color: var(--muted);
display: flex;
align-items: center;
gap: 1rem;
2026-07-29 20:12:44 -05:00
margin: 0 0 var(--gap) 0;
line-height: 1;
}
section > h5::before {
content: ' ';
width: var(--flare-length);
height: var(--flare-spread);
flex-shrink: 0;
background-color: var(--muted);
/* Optically center */
transform: translateY(-0.12em);
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns=' http : //www.w3.org/2000/svg' viewBox='0 0 20 16' preserveAspectRatio='none' fill='none'%3E%3Cpath d='M20 8H10M10 8C5 8 2.5 4.5 1 2M10 8C5 8 2.5 11.5 1 14' stroke='black' stroke-width='1' stroke-linecap='round' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E")
center / 100 % 100 % no - repeat ;
mask : url ( "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 16' preserveAspectRatio='none' fill='none'%3E%3Cpath d='M20 8H10M10 8C5 8 2.5 4.5 1 2M10 8C5 8 2.5 11.5 1 14' stroke='black' stroke-width='1' stroke-linecap='round' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E" )
center / 100 % 100 % no - repeat ;
}
section > h5 : : after {
content : '' ;
height : var ( -- flare - spread ) ;
flex - grow : 1 ;
background - color : var ( -- muted ) ;
/* Optically center */
transform : translateY ( - 0.12 em ) ;
- webkit - mask :
linear - gradient ( # 000 , # 000 ) left center /
calc ( 100 % - var ( -- flare - length ) * 0.45 ) 1 px no - repeat ,
url ( "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 16' preserveAspectRatio='none' fill='none'%3E%3Cpath d='M0 8H10M10 8C15 8 17.5 4.5 19 2M10 8C15 8 17.5 11.5 19 14' stroke='black' stroke-width='1' stroke-linecap='round' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E" )
right center / var ( -- flare - length ) 100 % no - repeat ;
mask :
linear - gradient ( # 000 , # 000 ) left center /
calc ( 100 % - var ( -- flare - length ) * 0.45 ) 1 px no - repeat ,
url ( "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 16' preserveAspectRatio='none' fill='none'%3E%3Cpath d='M0 8H10M10 8C15 8 17.5 4.5 19 2M10 8C15 8 17.5 11.5 19 14' stroke='black' stroke-width='1' stroke-linecap='round' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E" )
right center / var ( -- flare - length ) 100 % no - repeat ;
2026-04-29 14:39:34 -05:00
}
section : last - of - type {
2026-07-29 20:12:44 -05:00
margin - bottom : calc ( var ( -- gap ) * 2 ) ;
2026-04-29 14:39:34 -05:00
}
2026-06-22 00:28:43 -05:00
. work {
}
. work _ _images {
/*display: flex;
flex-wrap: no-wrap;
align-items: center;
justify-content: center;*/
display : grid ;
grid - auto - flow : column ;
grid - auto - columns : max - content ;
gap : var ( -- gap ) ;
overflow - x : auto ;
overflow - y : hidden ;
margin - top : var ( -- gap ) ;
}
. work _ _image {
-- dot - size : 6 % ;
margin : 0 ;
/*padding: calc(var(--gap) * 2) var(--gap) var(--gap);*/
padding : var ( -- gap ) ;
flex - shrink : 0 ;
border - radius : var ( -- radius ) ;
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 ;
background - color : var ( -- bg - secondary ) ;
height : 100 % ;
display : flex ;
align - items : center ;
justify - content : center ;
flex - direction : column ;
gap : var ( -- gap ) ;
}
. work _ _image img {
border - radius : var ( -- radius ) ;
object - fit : cover ;
max - height : 350 px ;
}
@ media screen and ( max - width : 750 px ) {
. work _ _image img {
max - height : 300 px ;
}
}
. work _ _image figcaption {
font - size : 0.6 rem ;
line - height : 1 ;
font - family : var ( -- font - plain ) ;
background : var ( -- bg - secondary ) ;
border - radius : 2 px ;
padding : 3 px 4 px ;
background : var ( -- dot - color ) ;
/*border: 1px solid var(--dot-color);*/
display : flex ;
align - items : center ;
justify - content : center ;
}
2026-07-29 20:12:44 -05:00
. rows > . section _ _content {
display : flex ;
flex - direction : column ;
gap : var ( -- gap ) ;
}
2026-06-22 00:28:43 -05:00
. project ,
. work {
/*border-bottom: 1px solid var(--muted);*/
2026-07-29 20:12:44 -05:00
/*padding: var(--gap) 0;*/
2026-04-29 14:39:34 -05:00
display : block ;
text - decoration : none ;
color : inherit ;
}
2026-06-22 00:28:43 -05:00
. work {
border : 0 ;
}
. project : first - of - type ,
. work : first - of - type {
2026-04-29 16:32:57 -05:00
padding - top : 0 ;
}
2026-06-22 00:28:43 -05:00
. project : last - of - type ,
. work : last - of - type {
2026-04-29 16:32:57 -05:00
padding - bottom : 0 ;
}
2026-04-29 14:39:34 -05:00
. project : last - child {
border - bottom : none ;
}
. project : hover > . project _ _title {
text - decoration : underline ;
text - underline - offset : 4 px ;
font - style : italic ;
2026-05-01 09:54:25 -05:00
font - family : var ( -- font - hover ) ;
2026-04-29 14:39:34 -05:00
}
2026-06-22 00:28:43 -05:00
. project _ _title ,
. work _ _title {
2026-04-29 14:39:34 -05:00
margin : 0 ;
font - weight : 400 ;
2026-06-22 00:28:43 -05:00
margin - bottom : calc ( var ( -- gap ) * 0.5 ) ;
2026-04-29 14:39:34 -05:00
}
2026-06-22 00:28:43 -05:00
. project _ _title : : after ,
. work _ _title : : after {
2026-06-12 20:22:56 -05:00
content : '↗' ;
position : relative ;
display : inline - block ;
margin - left : 0.4 em ;
font - size : 0.75 em ;
opacity : 0 ;
vertical - align : center ;
transform : translate ( - 6 px , 6 px ) ;
transition :
opacity 0.25 s ease ,
transform 0.25 s ease ;
}
2026-06-22 00:28:43 -05:00
. project : hover . project _ _title : : after ,
. work : hover . work _ _title : : after {
2026-06-12 20:22:56 -05:00
opacity : 1 ;
transform : translate ( 0 , 0 ) ;
}
2026-06-22 00:28:43 -05:00
. project _ _description ,
. work _ _description {
2026-04-29 14:39:34 -05:00
margin : 0 ;
font - size : small ;
font - weight : 400 ;
font - style : italic ;
color : var ( -- muted ) ;
}
. photo - grid {
display : grid ;
grid - template - columns : repeat ( 3 , 1 fr ) ;
}
. photo - grid _ _item {
cursor : pointer ;
position : relative ;
aspect - ratio : 1 / 1 ;
}
. photo - grid _ _item : hover > . photo _ _overlay {
opacity : 1 ;
}
. photo _ _image {
width : 100 % ;
height : 100 % ;
}
. photo _ _image img {
width : 100 % ;
height : 100 % ;
object - fit : cover ;
}
. photo _ _overlay {
opacity : 0 ;
position : absolute ;
top : 0 ;
left : 0 ;
width : 100 % ;
height : 100 % ;
background - color : rgba ( 0 , 0 , 0 , 0.5 ) ;
backdrop - filter : blur ( 2 px ) ;
2026-06-12 20:22:56 -05:00
color : var ( -- color - white - 500 ) ;
2026-04-29 14:39:34 -05:00
z - index : 2 ;
transition : opacity 0.2 s ease - in - out ;
display : flex ;
align - items : center ;
justify - content : center ;
flex - direction : column ;
}
. photo _ _overlay p {
font - weight : 500 ;
margin : 0 ;
}
. photo _ _overlay p : nth - child ( 2 ) {
font - weight : 300 ;
font - style : italic ;
2026-05-02 21:03:02 -05:00
font - size : small ;
2026-04-29 14:39:34 -05:00
}
2026-05-12 19:12:41 -05:00
. about p : last - child {
2026-04-29 16:32:57 -05:00
margin : 0 ;
}
2026-04-29 14:39:34 -05:00
@ media ( max - width : 768 px ) {
. photo - grid {
grid - template - columns : 1 fr ;
}
}
< / style >