1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2026-06-05 19:15:30 -04:00

2026 Redesign (#2531)

This commit is contained in:
2026-01-27 22:53:59 -05:00
committed by GitHub
parent d72e587401
commit 2ece5c79fa
27 changed files with 1887 additions and 2012 deletions
+30 -26
View File
@@ -11,35 +11,39 @@ export const metadata = createMetadata({
const Page = () => {
return (
<div className="w-full md:mx-auto md:w-2/3">
<>
<PageTitle canonical="/contact">Contact</PageTitle>
<p className="my-5 text-[0.925rem] leading-relaxed md:text-base">
Fill out this quick form and I&rsquo;ll get back to you as soon as I can! You can also{" "}
<Link href="mailto:jake@jarv.is">email me directly</Link> or send me a direct message on{" "}
<Link href="https://bsky.app/profile/jarv.is" className="text-nowrap">
🦋 Bluesky
</Link>{" "}
or{" "}
<Link href="https://fediverse.jarv.is/@jake" className="text-nowrap">
🦣 Mastodon
</Link>
.
</p>
<p className="my-5 text-[0.925rem] leading-relaxed md:text-base">
You can grab my public key here:{" "}
<Link
href="https://jrvs.io/pgp"
title="3BC6 E577 6BF3 79D3 6F67 1480 2B0C 9CF2 51E6 9A39"
className="bg-muted relative rounded-sm px-[0.3rem] py-[0.2rem] font-mono text-sm font-medium tracking-wider [word-spacing:-0.25em]"
>
2B0C 9CF2 51E6 9A39
</Link>
.
</p>
<div className="w-full md:mx-auto md:w-2/3">
<div className="prose prose-sm prose-neutral dark:prose-invert max-w-none">
<p>
Fill out this quick form and I&rsquo;ll get back to you as soon as I can! You can also{" "}
<Link href="mailto:jake@jarv.is">email me directly</Link> or send me a direct message on{" "}
<Link href="https://bsky.app/profile/jarv.is" className="text-nowrap">
Bluesky
</Link>{" "}
or{" "}
<Link href="https://fediverse.jarv.is/@jake" className="text-nowrap">
Mastodon
</Link>
.
</p>
<p>
You can grab my public key here:{" "}
<Link
href="https://jrvs.io/pgp"
title="3BC6 E577 6BF3 79D3 6F67 1480 2B0C 9CF2 51E6 9A39"
className="bg-muted relative rounded-sm px-[0.3rem] py-[0.2rem] font-mono text-sm font-medium tracking-wider [word-spacing:-0.25em]"
>
2B0C 9CF2 51E6 9A39
</Link>
.
</p>
</div>
<ContactForm />
</div>
<ContactForm />
</div>
</>
);
};
+230 -84
View File
@@ -1,66 +1,22 @@
@import "tailwindcss";
@import "tw-animate-css";
@plugin "@tailwindcss/typography";
@import "react-lite-youtube-embed/dist/LiteYouTubeEmbed.css";
@custom-variant dark (&:where([data-theme=dark] *));
:root {
--background: oklch(1.00 0 0);
--foreground: oklch(0.26 0 0);
--card: oklch(1.00 0 0);
--card-foreground: oklch(0.26 0 0);
--popover: oklch(1.00 0 0);
--popover-foreground: oklch(0.26 0 0);
--primary: oklch(0.50 0.13 245.46);
--primary-foreground: oklch(0.99 0 0);
--secondary: oklch(0.98 0 0);
--secondary-foreground: oklch(0.33 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.98 0 0);
--accent-foreground: oklch(0.33 0 0);
--highlight: oklch(0.50 0.13 245.46);
--highlight-foreground: oklch(0.99 0 0);
--destructive: oklch(0.62 0.21 25.77);
--warning: oklch(0.67 0.179 58.318);
--success: oklch(0.63 0.194 149.214);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--radius: 0.625rem;
}
[data-theme="dark"] {
--background: oklch(0.20 0 0);
--foreground: oklch(0.98 0 0);
--card: oklch(0.14 0.00 285.82);
--card-foreground: oklch(0.98 0 0);
--popover: oklch(0.14 0.00 285.82);
--popover-foreground: oklch(0.98 0 0);
--primary: oklch(0.81 0.10 251.81);
--primary-foreground: oklch(0.21 0.01 285.88);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.27 0.01 286.03);
--accent-foreground: oklch(0.98 0 0);
--highlight: oklch(0.81 0.10 251.81);
--highlight-foreground: oklch(0.21 0.01 285.88);
--destructive: oklch(0.70 0.19 22.23);
--warning: oklch(0.8 0.184 86.047);
--success: oklch(0.79 0.209 151.711);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
}
@custom-variant dark (&:where(.dark *));
@theme inline {
--font-sans: var(--font-geist-sans);
--font-sans--font-feature-settings: "rlig" 1, "calt" 0;
--font-mono: var(--font-geist-mono);
--font-mono--font-feature-settings: "liga" 0;
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
@@ -75,24 +31,123 @@
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-highlight: var(--highlight);
--color-highlight-foreground: var(--highlight-foreground);
--color-destructive: var(--destructive);
--color-warning: var(--warning);
--color-success: var(--success);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--color-surface: var(--surface);
--color-surface-foreground: var(--surface-foreground);
--color-code: var(--code);
--color-code-foreground: var(--code-foreground);
--color-code-highlight: var(--code-highlight);
--color-code-number: var(--code-number);
--color-selection: var(--selection);
--color-selection-foreground: var(--selection-foreground);
}
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.97 0.01 17);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: var(--color-blue-300);
--chart-2: var(--color-blue-500);
--chart-3: var(--color-blue-600);
--chart-4: var(--color-blue-700);
--chart-5: var(--color-blue-800);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--surface: oklch(0.98 0 0);
--surface-foreground: var(--foreground);
--code: var(--surface);
--code-foreground: var(--surface-foreground);
--code-highlight: oklch(0.96 0 0);
--code-number: oklch(0.56 0 0);
--selection: oklch(0.145 0 0);
--selection-foreground: oklch(1 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.269 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.371 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--destructive-foreground: oklch(0.58 0.22 27);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: var(--color-blue-300);
--chart-2: var(--color-blue-500);
--chart-3: var(--color-blue-600);
--chart-4: var(--color-blue-700);
--chart-5: var(--color-blue-800);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.439 0 0);
--surface: oklch(0.2 0 0);
--surface-foreground: oklch(0.708 0 0);
--code: var(--surface);
--code-foreground: var(--surface-foreground);
--code-highlight: oklch(0.27 0 0);
--code-number: oklch(0.72 0 0);
--selection: oklch(0.922 0 0);
--selection-foreground: oklch(0.205 0 0);
}
@theme {
--animate-wave: wave 5s ease 1s infinite;
--animate-heartbeat: heartbeat 10s ease 7.5s infinite;
--animate-marquee: marquee 30s linear infinite;
@keyframes wave {
@@ -118,21 +173,6 @@
}
}
@keyframes heartbeat {
0%,
4%,
8%,
100% {
transform: scale(1);
}
2% {
transform: scale(1.25);
}
6% {
transform: scale(1.2);
}
}
@keyframes marquee {
from {
transform: translateX(0);
@@ -147,9 +187,11 @@
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground selection:bg-highlight selection:text-highlight-foreground;
@apply bg-background text-foreground;
}
::selection {
@apply bg-selection text-selection-foreground;
}
::-webkit-scrollbar {
@@ -169,10 +211,114 @@
}
@layer components {
@import "react-lite-youtube-embed/dist/LiteYouTubeEmbed.css";
[data-slot="code-block"] {
@apply bg-code text-code-foreground overflow-x-auto overflow-y-hidden rounded-xl text-sm outline-none;
/* styles for wrapper around generated markdown content */
.generated {
@apply text-[0.925rem] leading-relaxed first:mt-0 last:mb-0 md:text-base [&_p]:my-5;
/* Override shiki inline styles */
& pre {
@apply m-0 rounded-xl !bg-transparent;
}
/* Dark mode token colors - override shiki inline color styles */
& span[style*="color"] {
@apply dark:![color:var(--shiki-dark)];
}
& code {
display: grid;
min-width: 100%;
white-space: pre;
padding: 0.875rem 1rem;
counter-reset: line;
}
& .line {
display: inline-block;
min-height: 1lh;
width: 100%;
padding-block: 0.125rem;
}
/* Highlighted lines */
& .line.highlighted {
position: relative;
background-color: var(--color-code-highlight);
&:after {
position: absolute;
top: 0;
left: 0;
width: 2px;
height: 100%;
content: "";
background-color: color-mix(in oklab, var(--muted-foreground) 50%, transparent);
}
}
/* Highlighted words */
& .highlighted-word {
position: relative;
background-color: var(--color-code-highlight);
border-radius: var(--radius-sm);
padding-inline: 0.3rem;
padding-block: 0.1rem;
}
}
/* Line numbers - only when data-line-numbers is set */
[data-slot="code-block"][data-line-numbers] .line::before {
font-size: var(--text-sm);
counter-increment: line;
content: counter(line);
display: inline-block;
width: 2rem;
margin-right: 1.5rem;
text-align: right;
color: var(--color-code-number);
}
[data-slot="code-block"][data-line-numbers] .line.highlighted::before {
background-color: var(--color-code-highlight);
}
}
/* View Transitions */
@layer base {
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
main {
view-transition-name: main-content;
}
::view-transition-old(main-content) {
animation: fade-out 120ms ease-out;
}
::view-transition-new(main-content) {
animation: fade-in 120ms ease-in 40ms;
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
}
+3 -8
View File
@@ -1,7 +1,6 @@
import { env } from "@/lib/env";
import { JsonLd } from "react-schemaorg";
import Providers from "@/components/providers";
import { ThemeScript } from "@/components/theme/theme-script";
import Header from "@/components/layout/header";
import Footer from "@/components/layout/footer";
import Toaster from "@/components/ui/sonner";
@@ -24,8 +23,6 @@ const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
suppressHydrationWarning
>
<head>
<ThemeScript />
<JsonLd<Person>
item={{
"@context": "https://schema.org",
@@ -65,13 +62,11 @@ const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
<body className="bg-background text-foreground font-sans antialiased">
<Providers>
<div className="mx-auto w-full max-w-4xl px-5 py-1">
<Header className="mt-4 mb-6 w-full" />
<Header />
<div className="mx-auto mt-4 w-full max-w-4xl px-5">
<main>{children}</main>
<Footer className="my-6 w-full" />
</div>
<Footer />
<Toaster position="bottom-center" hotkey={[]} />
<Analytics />
+3 -2
View File
@@ -1,3 +1,4 @@
import Button from "@/components/ui/button";
import Video from "@/components/video";
import Link from "@/components/link";
import type { Metadata } from "next";
@@ -23,9 +24,9 @@ const Page = () => {
<div className="mt-6 text-center">
<h1 className="my-2 text-2xl font-semibold md:text-3xl">Page Not Found</h1>
<p className="mt-4 mb-0 text-lg font-medium md:text-xl">
<Button className="mt-4 mb-0 text-[15px] leading-none" size="lg" asChild>
<Link href="/">Go home?</Link>
</p>
</Button>
</div>
</>
);
+10 -8
View File
@@ -61,8 +61,10 @@ const getFormattedDates = async (date: string) => {
const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
const { slug } = await params;
const frontmatter = await getFrontMatter(slug);
const commentCount = await getCommentCounts(`${POSTS_DIR}/${slug}`);
const [frontmatter, commentCount] = await Promise.all([
getFrontMatter(slug),
getCommentCounts(`${POSTS_DIR}/${slug}`),
]);
const formattedDates = await getFormattedDates(frontmatter!.date);
const { default: MDXContent } = await import(`../../../${POSTS_DIR}/${slug}/index.mdx`);
@@ -99,7 +101,7 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
href={`/${POSTS_DIR}/${frontmatter!.slug}`}
className={"text-foreground/70 flex flex-nowrap items-center gap-x-2 whitespace-nowrap hover:no-underline"}
>
<CalendarDaysIcon className="inline size-4 shrink-0" />
<CalendarDaysIcon className="inline size-4 shrink-0" aria-hidden="true" />
<time dateTime={formattedDates.dateISO} title={formattedDates.dateTitle}>
{formattedDates.dateDisplay}
</time>
@@ -107,7 +109,7 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
{frontmatter!.tags && (
<div className="flex flex-wrap items-center gap-x-2 whitespace-nowrap">
<TagIcon className="inline size-4 shrink-0" />
<TagIcon className="inline size-4 shrink-0" aria-hidden="true" />
{frontmatter!.tags.map((tag) => (
<span
key={tag}
@@ -126,7 +128,7 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
title={`Edit "${frontmatter!.title}" on GitHub`}
className={"text-foreground/70 flex flex-nowrap items-center gap-x-2 whitespace-nowrap hover:no-underline"}
>
<SquarePenIcon className="inline size-4 shrink-0" />
<SquarePenIcon className="inline size-4 shrink-0" aria-hidden="true" />
<span>Improve This Post</span>
</Link>
@@ -135,17 +137,17 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
title={`${Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(commentCount || 0)} ${commentCount === 1 ? "comment" : "comments"}`}
className="text-foreground/70 flex flex-nowrap items-center gap-x-2 whitespace-nowrap hover:no-underline"
>
<MessagesSquareIcon className="inline size-4 shrink-0" />
<MessagesSquareIcon className="inline size-4 shrink-0" aria-hidden="true" />
<span>{Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(commentCount || 0)}</span>
</Link>
<div className="flex min-w-14 flex-nowrap items-center gap-x-2 whitespace-nowrap">
<EyeIcon className="inline size-4 shrink-0" />
<EyeIcon className="inline size-4 shrink-0" aria-hidden="true" />
<ViewCounter slug={`${POSTS_DIR}/${frontmatter!.slug}`} />
</div>
</div>
<h1 className="mt-2 mb-3 text-3xl/10 font-bold md:text-4xl/12">
<h1 className="mt-4 mb-5 text-4xl font-semibold tracking-tight sm:text-3xl">
<Link
href={`/${POSTS_DIR}/${frontmatter!.slug}`}
dangerouslySetInnerHTML={{ __html: frontmatter!.htmlTitle || frontmatter!.title }}
+7 -6
View File
@@ -24,8 +24,8 @@ const PostStats = ({ views, comments, slug }: { views: number; comments: number;
<>
{views > 0 && (
<span className="bg-muted text-foreground/65 inline-flex h-5 flex-nowrap items-center gap-1 rounded-md px-1.5 align-text-top text-xs font-semibold text-nowrap shadow select-none">
<EyeIcon className="inline-block size-4 shrink-0" />
<span className="inline-block leading-none">{numberFormatter.format(views)}</span>
<EyeIcon className="inline-block size-4 shrink-0" aria-hidden="true" />
<span className="inline-block leading-none tabular-nums">{numberFormatter.format(views)}</span>
</span>
)}
@@ -37,8 +37,8 @@ const PostStats = ({ views, comments, slug }: { views: number; comments: number;
className="inline-flex hover:no-underline"
>
<span className="bg-muted text-foreground/65 inline-flex h-5 flex-nowrap items-center gap-1 rounded-md px-1.5 align-text-top text-xs font-semibold text-nowrap shadow select-none">
<MessagesSquareIcon className="inline-block size-3 shrink-0" />
<span className="inline-block leading-none">{numberFormatter.format(comments)}</span>
<MessagesSquareIcon className="inline-block size-3 shrink-0" aria-hidden="true" />
<span className="inline-block leading-none tabular-nums">{numberFormatter.format(comments)}</span>
</span>
</Link>
)}
@@ -96,8 +96,8 @@ const PostsList = async () => {
Object.entries(postsByYear).forEach(([year, posts]) => {
sections.push(
<section className="my-8 first-of-type:mt-6 last-of-type:mb-6" key={year}>
<h2 id={year} className="mt-0 mb-4 text-3xl font-bold md:text-4xl">
<section className="my-8 first-of-type:mt-0 last-of-type:mb-0" key={year}>
<h2 id={year} className="mt-0 mb-4 text-4xl font-semibold tracking-tight sm:text-3xl">
{year}
</h2>
<ul className="space-y-4">
@@ -114,6 +114,7 @@ const PostsList = async () => {
href={`/${POSTS_DIR}/${slug}`}
prefetch={false}
dangerouslySetInnerHTML={{ __html: htmlTitle || title }}
className="inline-flex items-center gap-2 text-lg font-medium underline-offset-4 hover:underline md:text-base"
/>
<PostStats slug={slug} views={views} comments={comments} />
+35 -164
View File
@@ -3,109 +3,50 @@ import { LockIcon } from "lucide-react";
const Page = () => {
return (
<>
<h1 className="mt-0 mb-2 text-3xl leading-relaxed font-medium">
<div className="prose prose-sm prose-neutral dark:prose-invert prose-headings:mt-0 prose-headings:mb-3 prose-p:my-3 prose-p:leading-[1.75] md:prose-p:leading-relaxed max-w-none">
<h1 className="text-2xl font-medium">
Hi there! I&rsquo;m Jake.{" "}
<span className="motion-safe:animate-wave ml-0.5 inline-block origin-[65%_80%] text-3xl">👋</span>
<span className="motion-safe:animate-wave ml-0.5 inline-block origin-[65%_80%] text-2xl">👋</span>
</h1>
<h2 className="my-2 text-xl leading-relaxed font-normal">
<h2 className="font-normal">
I&rsquo;m a frontend web developer based in the{" "}
<Link
href="https://www.youtube-nocookie.com/embed/rLwbzGyC6t4?hl=en&amp;fs=1&amp;showinfo=1&amp;rel=0&amp;iv_load_policy=3"
title='"Boston Accent Trailer - Late Night with Seth Meyers" on YouTube'
className="[--primary:#fb4d42] dark:[--primary:#ff5146]"
>
Boston
</Link>{" "}
area.
</h2>
<p className="my-3 text-base leading-relaxed md:text-[0.975rem]">
I specialize in using{" "}
<Link href="https://www.typescriptlang.org/" className="[--primary:#235a97] dark:[--primary:#59a8ff]">
TypeScript
</Link>
,{" "}
<Link href="https://reactjs.org/" className="[--primary:#1091b3] dark:[--primary:#6fcbe3]">
React
</Link>
, and{" "}
<Link href="https://nextjs.org/" className="[--primary:#5e7693] dark:[--primary:#a8b9c0]">
Next.js
</Link>{" "}
to make lightweight{" "}
<Link href="https://jamstack.org/glossary/jamstack/" className="[--primary:#04a699] dark:[--primary:#08bbac]">
Jamstack sites
</Link>{" "}
with dynamic and powerful{" "}
<Link href="https://nodejs.org/en/" className="[--primary:#6fbc4e] dark:[--primary:#84d95f]">
Node
</Link>{" "}
backends. But I still know my way around{" "}
<Link
href="https://www.jetbrains.com/lp/php-25/"
title="25 Years of PHP History"
className="[--primary:#8892bf] dark:[--primary:#a4afe3]"
>
less buzzwordy
</Link>{" "}
stacks (and{" "}
<Link
href="https://timkadlec.com/remembers/2020-04-21-the-cost-of-javascript-frameworks/"
title='"The Cost of Javascript Frameworks" by Tim Kadlec'
className="[--primary:#f48024] dark:[--primary:#e18431]"
>
vanilla JavaScript
</Link>
), too.
<p>
I specialize in using TypeScript, React, and Next.js to make lightweight frontends with dynamic and powerful
backends.
</p>
<p className="my-3 text-base leading-relaxed md:text-[0.975rem]">
<p>
Whenever possible, I also apply my experience in{" "}
<Link
href="https://bugcrowd.com/jakejarvis"
title="Jake Jarvis on Bugcrowd"
className="[--primary:#00b81a] dark:[--primary:#57f06d]"
>
<Link href="https://bugcrowd.com/jakejarvis" title="Jake Jarvis on Bugcrowd">
information security
</Link>
,{" "}
<Link
href="https://www.cloudflare.com/learning/serverless/what-is-serverless/"
title='"What is serverless computing?" on Cloudflare'
className="[--primary:#0098ec] dark:[--primary:#43b9fb]"
>
serverless architecture
</Link>
, and{" "}
</Link>{" "}
and{" "}
<Link
href="https://github.com/jakejarvis?tab=repositories&q=github-actions&type=&language=&sort=stargazers"
title='My repositories tagged with "github-actions" on GitHub'
className="[--primary:#ff6200] dark:[--primary:#f46c16]"
>
automation
devops
</Link>
.
</p>
<p className="my-3 text-base leading-relaxed md:text-[0.975rem]">
<p>
I fell in love with{" "}
<Link
href="/previously"
prefetch={false}
title="My Terrible, Horrible, No Good, Very Bad First Websites"
className="[--primary:#4169e1] dark:[--primary:#8ca9ff]"
>
<Link href="/previously" prefetch={false} title="My Terrible, Horrible, No Good, Very Bad First Websites">
frontend web design
</Link>{" "}
and{" "}
<Link
href="/notes/my-first-code"
prefetch={false}
title="Jake's Bulletin Board, circa 2003"
className="[--primary:#9932cc] dark:[--primary:#d588fb]"
>
<Link href="/notes/my-first-code" prefetch={false} title="Jake's Bulletin Board, circa 2003">
backend coding
</Link>{" "}
when my only source of income was{" "}
@@ -113,131 +54,61 @@ const Page = () => {
href="/birthday"
prefetch={false}
title="🎉 Cranky Birthday Boy on VHS Tape 📼"
className="[--primary:#e40088] dark:[--primary:#fd40b1]"
style={{
cursor: `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='30' style='font-size:24px'><text y='50%' transform='rotate(-70 0 0) translate(-20, 6)'>🪄</text></svg>") 5 5, auto`,
}}
className="font-normal no-underline"
>
the Tooth Fairy
</Link>
. <span className="text-muted-foreground">I&rsquo;ve improved a bit since then, I think?</span>
. <span className="text-muted-foreground">(I&rsquo;ve improved a bit since then, I think?)</span>
</p>
<p className="my-3 text-base leading-relaxed md:text-[0.975rem]">
<p>
I&rsquo;m currently building{" "}
<Link
href="https://domainstack.io"
title="Domainstack: Domain intelligence made easy"
className="font-medium [--primary:#a054d0] dark:[--primary:#dd9fff]"
>
<Link href="https://domainstack.io" title="Domainstack: Domain intelligence made easy" className="font-medium">
Domainstack
</Link>
, a beautiful all-in-one domain name intelligence tool, and{" "}
<Link
href="https://snoozle.ai"
title="Snoozle: AI-powered bedtime stories for children"
className="bg-gradient-to-r from-purple-500 to-pink-500 bg-clip-text font-medium text-transparent [--primary:#a855f7] dark:from-purple-400 dark:to-pink-400 dark:[--primary:#c084fc]"
className="font-medium"
>
Snoozle
</Link>
, an AI-powered bedtime story generator. Over the years, some of my other side projects{" "}
<Link
href="/leo"
prefetch={false}
title="Powncer segment on The Lab with Leo Laporte (G4techTV)"
className="[--primary:#ff1b1b] dark:[--primary:#f06060]"
>
have
</Link>{" "}
<Link
href="https://tuftsdaily.com/news/2012/04/06/student-designs-iphone-joeytracker-app/"
title='"Student designs iPhone JoeyTracker app" on The Tufts Daily'
className="[--primary:#f78200] dark:[--primary:#fd992a]"
>
been
</Link>{" "}
<Link
href="https://www.google.com/books/edition/The_Facebook_Effect/RRUkLhyGZVgC?hl=en&gbpv=1&dq=%22jake%20jarvis%22&pg=PA226&printsec=frontcover&bsq=%22jake%20jarvis%22"
title='"The Facebook Effect" by David Kirkpatrick (Google Books)'
className="[--primary:#f2b702] dark:[--primary:#ffcc2e]"
>
featured
</Link>{" "}
<Link
href="https://money.cnn.com/2007/06/01/technology/facebookplatform.fortune/index.htm"
title='"The new Facebook is on a roll" on CNN Money'
className="[--primary:#5ebd3e] dark:[--primary:#78df55]"
>
by
</Link>{" "}
<Link
href="https://www.wired.com/2007/04/our-web-servers/"
title='"Middio: A YouTube Scraper for Major Label Music Videos" on Wired'
className="[--primary:#009cdf] dark:[--primary:#29bfff]"
>
various
</Link>{" "}
<Link
href="https://gigaom.com/2009/10/06/fresh-faces-in-tech-10-kid-entrepreneurs-to-watch/6/"
title='"Fresh Faces in Tech: 10 Kid Entrepreneurs to Watch" on Gigaom'
className="[--primary:#3e49bb] dark:[--primary:#7b87ff]"
>
media
</Link>{" "}
<Link
href="https://adage.com/article/small-agency-diary/client-ceo-s-son/116723/"
title='"Your Next Client? The CEO&#39;s Son" on Advertising Age'
className="[--primary:#973999] dark:[--primary:#db60dd]"
>
outlets
</Link>
.
, an AI-powered bedtime story generator.
</p>
<p className="mt-3 mb-0 text-base leading-relaxed md:text-[0.975rem]">
<p className="mt-2 mb-0 text-sm leading-normal">
You can find my work on{" "}
<Link href="https://github.com/jakejarvis" rel="me" className="[--primary:#8d4eff] dark:[--primary:#a379f0]">
<Link href="https://github.com/jakejarvis" rel="me">
GitHub
</Link>{" "}
and{" "}
<Link
href="https://www.linkedin.com/in/jakejarvis/"
rel="me"
className="[--primary:#0073b1] dark:[--primary:#3b9dd2]"
>
<Link href="https://www.linkedin.com/in/jakejarvis/" rel="me">
LinkedIn
</Link>
. I&rsquo;m always available to connect over{" "}
<Link
href="/contact"
prefetch={false}
title="Send an email"
className="[--primary:#de0c0c] dark:[--primary:#ff5050]"
>
<Link href="/contact" prefetch={false} title="Send an email">
email
</Link>{" "}
<sup className="mr-0.5 text-[0.6rem]">
<sup className="">
<Link
href="https://jrvs.io/pgp"
rel="pgpkey"
title="3BC6 E577 6BF3 79D3 6F67 1480 2B0C 9CF2 51E6 9A39"
className="space-x-0.5 px-0.5 text-nowrap [--primary:var(--muted-foreground)] hover:no-underline"
title="Download my PGP key"
className="not-prose text-muted-foreground hover:text-primary space-x-1 px-0.5 text-nowrap no-underline hover:no-underline"
>
<LockIcon className="inline size-3 align-text-top" />{" "}
<code className="tracking-wider text-wrap [word-spacing:-3px]">2B0C 9CF2 51E6 9A39</code>
<LockIcon className="inline size-2.5" aria-hidden="true" />
<code className="text-[9px] leading-none tracking-wider text-wrap [word-spacing:-3px]">
2B0C 9CF2 51E6 9A39
</code>
</Link>
</sup>
,{" "}
<Link href="https://bsky.app/profile/jarv.is" rel="me" className="[--primary:#0085ff] dark:[--primary:#208bfe]">
Bluesky
</Link>
, or{" "}
<Link href="https://fediverse.jarv.is/@jake" rel="me" className="[--primary:#6d6eff] dark:[--primary:#7b87ff]">
Mastodon
</Link>{" "}
as well!
</sup>{" "}
as well.
</p>
</>
</div>
);
};
+9 -3
View File
@@ -11,6 +11,7 @@ import { GitHubIcon } from "@/components/icons";
import { cn } from "@/lib/utils";
import { createMetadata } from "@/lib/metadata";
import { getContributions, getRepos } from "./github";
import Button from "@/components/ui/button";
export const metadata = createMetadata({
title: "Projects",
@@ -122,9 +123,14 @@ const Page = async () => {
)}
<p className="mt-6 mb-0 text-center text-base font-medium">
<Link href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}`} className="hover:no-underline">
View more on <GitHubIcon className="fill-foreground/80 mx-0.5 inline size-5 align-text-top" /> GitHub.
</Link>
<Button variant="secondary" asChild>
<Link
href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}?tab=repositories&type=source&sort=stargazers`}
>
<GitHubIcon />
<span className="leading-none">Show All</span>
</Link>
</Button>
</p>
</>
);