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:
+30
-26
@@ -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’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’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
@@ -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
@@ -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
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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
@@ -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
@@ -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’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’m a frontend web developer based in the{" "}
|
||||
<Link
|
||||
href="https://www.youtube-nocookie.com/embed/rLwbzGyC6t4?hl=en&fs=1&showinfo=1&rel=0&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’ve improved a bit since then, I think?</span>
|
||||
. <span className="text-muted-foreground">(I’ve improved a bit since then, I think?)</span>
|
||||
</p>
|
||||
|
||||
<p className="my-3 text-base leading-relaxed md:text-[0.975rem]">
|
||||
<p>
|
||||
I’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'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’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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user