Files
folio/app/pages/index.vue
T

634 lines
18 KiB
Vue
Raw Normal View History

<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-06-22 00:44:50 -05:00
const siteUrl = 'https://jaron.sh';
const currentUrl = useRequestURL().href;
const personId = `${siteUrl}/#person`;
2026-06-28 18:50:53 -05:00
const websiteId = `${siteUrl}/#website`;
2026-06-22 00:44:50 -05:00
const webpageId = `${siteUrl}/#homepage`;
2026-06-22 00:28:43 -05:00
useSeoMeta({
title: "Hey, I'm Jaron.",
ogTitle: "Hey, I'm Jaron.",
2026-06-22 01:01:41 -05:00
description: 'Developer. Reformed Rust enjoyer, Golang padawan.',
ogDescription: 'Developer. Reformed Rust enjoyer, Golang padawan.',
2026-06-22 00:44:50 -05:00
ogUrl: currentUrl,
2026-06-22 00:28:43 -05:00
});
const titles = [
2026-06-22 00:28:43 -05:00
"Hey, I'm Jaron.",
'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,
url: siteUrl,
name: "Hey, I'm Jaron.",
description:
'Developer. Reformed Rust enjoyer, Golang padawan.',
inLanguage: 'en-US',
isPartOf: { '@id': websiteId },
about: { '@id': personId },
mainEntity: { '@id': personId },
primaryImageOfPage: {
'@type': 'ImageObject',
url: `${siteUrl}/favicon.png`,
},
},
{
'@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: {
'@type': 'SoftwareApplication',
name: project.title,
description: project.description,
url: project.href,
author: { '@id': personId },
applicationCategory: 'DeveloperApplication',
},
})),
},
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);
};
definePageMeta({
layout: false,
});
const { $gsap } = useNuxtApp();
const pageRoot = ref<HTMLElement | null>(null);
onMounted(() => {
2026-06-22 00:28:43 -05:00
rotateTitle();
useHead({
title: pageTitle,
});
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 14:39:34 -05:00
</script>
<template>
<div ref="pageRoot">
<NuxtLayout name="main">
2026-07-29 20:12:44 -05:00
<section class="rows">
<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"
target="_blank"
>
2026-06-22 00:28:43 -05:00
<h3 class="work__title">{{ work.title }}</h3>
<p class="work__description">
{{ work.description }}
</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>
</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">
<a
2026-06-22 00:28:43 -05:00
v-for="project in projects"
:key="project.href"
:href="project.href"
target="_blank"
class="project"
>
2026-06-22 00:28:43 -05:00
<h3 class="project__title">{{ project.title }}</h3>
<p class="project__description">
2026-06-22 00:28:43 -05:00
{{ project.description }}
</p>
</a>
</div>
</section>
<section class="about">
<h5>About</h5>
<div class="section__content">
2026-06-14 17:29:58 -05:00
<p>I build things to solve problems from the ground up.</p>
<p>
I left school and my job at Chick-fil-A in 2022 to write
code full time. It's definitely one of the best
2026-06-14 17:29:58 -05:00
decisions I've ever made. I've learned a lot since then
and
2026-06-18 21:37:32 -05:00
<em>still</em> much to learn and build.
</p>
<p>
2026-06-14 17:29:58 -05:00
If you want to build something cool or connect hit me up
— we'll grab a coffee or beer in you're in Austin! I
still dont know enough people to form a band yet ;)
</p>
</div>
</section>
2026-06-14 17:29:58 -05:00
<!-- <section class="photos">
<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"
>
<div class="photo__image">
2026-06-22 00:28:43 -05:00
<img :src="photo.src" :alt="photo.alt" />
</div>
<div class="photo__overlay">
2026-06-22 00:28:43 -05:00
<p>{{ photo.location }}</p>
<p>{{ photo.year }}</p>
</div>
</div>
</div>
</div>
2026-06-14 17:29:58 -05:00
</section> -->
</NuxtLayout>
</div>
2026-04-29 14:39:34 -05:00
</template>
<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
}
/* 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.12em);
-webkit-mask:
linear-gradient(#000, #000) left center /
calc(100% - var(--flare-length) * 0.45) 1px 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) 1px 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 4px 4px,
var(--dot-color) var(--dot-size),
transparent calc(var(--dot-size) + 1%)
);
background-position: 4px 4px;
background-size: 12px 12px;
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: 350px;
}
@media screen and (max-width: 750px) {
.work__image img {
max-height: 300px;
}
}
.work__image figcaption {
font-size: 0.6rem;
line-height: 1;
font-family: var(--font-plain);
background: var(--bg-secondary);
border-radius: 2px;
padding: 3px 4px;
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 {
padding-top: 0;
}
2026-06-22 00:28:43 -05:00
.project:last-of-type,
.work:last-of-type {
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: 4px;
font-style: italic;
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 {
content: '↗';
position: relative;
display: inline-block;
margin-left: 0.4em;
font-size: 0.75em;
opacity: 0;
vertical-align: center;
transform: translate(-6px, 6px);
transition:
opacity 0.25s ease,
transform 0.25s ease;
}
2026-06-22 00:28:43 -05:00
.project:hover .project__title::after,
.work:hover .work__title::after {
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, 1fr);
}
.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(2px);
color: var(--color-white-500);
2026-04-29 14:39:34 -05:00
z-index: 2;
transition: opacity 0.2s 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;
font-size: small;
2026-04-29 14:39:34 -05:00
}
.about p:last-child {
margin: 0;
}
2026-04-29 14:39:34 -05:00
@media (max-width: 768px) {
.photo-grid {
grid-template-columns: 1fr;
}
}
</style>