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

chore: Next.js 15 → 16 (#2503)

This commit is contained in:
2025-11-22 17:11:42 -05:00
committed by GitHub
parent 19960ca2b0
commit 8a09aa918c
27 changed files with 2457 additions and 2304 deletions
+28 -24
View File
@@ -1,26 +1,36 @@
import { codeToHtml } from "shiki";
import reactToText from "react-to-text";
import { CodeIcon, TerminalIcon } from "lucide-react";
import { cacheLife } from "next/cache";
import CopyButton from "@/components/copy-button";
import { cn } from "@/lib/utils";
import reactToText from "react-to-text";
import { codeToHtml } from "shiki";
const CodeBlock = async ({
showLineNumbers = false,
showCopyButton = true,
className,
children,
...rest
}: React.ComponentProps<"pre"> & {
interface CodeBlockProps extends React.ComponentProps<"pre"> {
showLineNumbers?: boolean;
showCopyButton?: boolean;
}) => {
}
const renderHighlightedCode = async (codeString: string, lang: string) => {
"use cache";
cacheLife("max");
const html = await codeToHtml(codeString, {
lang,
themes: {
light: "github-light",
dark: "github-dark",
},
});
return html;
};
const CodeBlock = async (props: CodeBlockProps) => {
const { showLineNumbers = false, showCopyButton = true, children, className } = props;
// escape hatch if this code wasn't meant to be highlighted
if (!children || typeof children !== "object" || !("props" in children)) {
return (
<pre className={className} {...rest}>
{children}
</pre>
);
return <pre {...props}>{children}</pre>;
}
const codeProps = children.props as React.ComponentProps<"code">;
@@ -29,13 +39,7 @@ const CodeBlock = async ({
// the language set in the markdown is passed as a className
const lang = codeProps.className?.split("language-")[1] ?? "";
const codeHighlighted = await codeToHtml(codeString, {
lang,
themes: {
light: "github-light",
dark: "github-dark",
},
});
const html = await renderHighlightedCode(codeString, lang);
return (
<div className={cn("bg-muted/35 relative isolate rounded-lg border-2 font-mono shadow", className)}>
@@ -47,11 +51,11 @@ const CodeBlock = async ({
)}
data-language={lang || undefined}
data-line-numbers={showLineNumbers || undefined}
dangerouslySetInnerHTML={{ __html: codeHighlighted }}
dangerouslySetInnerHTML={{ __html: html }}
/>
{lang && (
<span className="[&_svg]:stroke-primary/90 text-foreground/75 bg-muted/40 absolute top-0 left-0 z-10 flex items-center gap-[8px] rounded-tl-md rounded-br-lg border-r-2 border-b-2 px-[10px] py-[5px] font-mono text-xs font-medium tracking-wide uppercase backdrop-blur-sm select-none [&_svg]:size-[14px] [&_svg]:shrink-0">
{["sh", "bash", "zsh"].includes(lang) ? (
{["sh", "bash", "zsh", "shell"].includes(lang) ? (
<>
<TerminalIcon />
<span>Shell</span>
+1 -1
View File
@@ -20,7 +20,7 @@ const Footer = ({ className, ...rest }: React.ComponentProps<"footer">) => {
<Link href="/previously" title="Previously on..." className="text-foreground/85 hover:no-underline">
{siteConfig.copyrightYearStart}
</Link>{" "}
{new Date().getUTCFullYear()}.
2025.
</div>
<div>
-1
View File
@@ -10,7 +10,6 @@ const Header = ({ className, ...rest }: React.ComponentProps<"header">) => {
return (
<header className={cn("flex items-center justify-between", className)} {...rest}>
<Link
dynamicOnHover
href="/"
rel="author"
aria-label={siteConfig.name}
-1
View File
@@ -31,7 +31,6 @@ const MenuItem = ({
if (href) {
return (
<Link
dynamicOnHover
href={href}
aria-label={text}
data-current={current || undefined}
+4 -22
View File
@@ -1,18 +1,7 @@
import NextLink from "next/link";
import { cn } from "@/lib/utils";
const Link = ({
href,
rel,
target,
prefetch = false,
dynamicOnHover,
className,
...rest
}: React.ComponentProps<typeof NextLink> & {
// https://github.com/vercel/next.js/pull/77866/files#diff-040f76a8f302dd3a8ec7de0867048475271f052b094cd73d2d0751b495c02f7dR30
dynamicOnHover?: boolean;
}) => {
const Link = ({ href, rel, target, className, ...rest }: React.ComponentProps<typeof NextLink>) => {
// This component auto-detects whether or not this link should open in the same window (the default for internal
// links) or a new tab (the default for external links). Defaults can be overridden with `target="_blank"`.
const isExternal = typeof href === "string" && !["/", "#"].includes(href[0]);
@@ -26,21 +15,14 @@ const Link = ({
className
),
...rest,
} as React.ComponentProps<"a">;
};
// don't waste time with next's component if it's just an external link
if (isExternal) {
return <a {...linkProps} />;
return <a {...(linkProps as unknown as React.ComponentProps<"a">)} />;
}
return (
<NextLink
{...linkProps}
prefetch={dynamicOnHover ? null : prefetch}
// @ts-expect-error
unstable_dynamicOnHover={dynamicOnHover}
/>
);
return <NextLink {...linkProps} />;
};
export default Link;
+4 -9
View File
@@ -1,6 +1,6 @@
"use client";
import { createContext, useEffect, useState } from "react";
import { createContext, useEffect } from "react";
import { useLocalStorage, useMedia } from "react-use";
export const ThemeContext = createContext<{
@@ -21,16 +21,11 @@ export const ThemeContext = createContext<{
export const ThemeProvider = ({ children }: React.PropsWithChildren) => {
// keep track of if/when the user has set their theme *on this site*
const [preferredTheme, setPreferredTheme] = useLocalStorage<string>("theme", undefined, { raw: true });
// keep track of changes to the user's OS/browser dark mode setting
const [systemTheme, setSystemTheme] = useState("");
// hook into system `prefers-dark-mode` setting
// hook into system `prefers-color-scheme` setting
// https://web.dev/prefers-color-scheme/#the-prefers-color-scheme-media-query
const isSystemDark = useMedia("(prefers-color-scheme: dark)", false);
// listen for changes in OS preference, but don't save it as a website preference to local storage
useEffect(() => {
setSystemTheme(isSystemDark ? "dark" : "light");
}, [isSystemDark]);
// Derive system theme directly from media query to avoid setState in effect
const systemTheme = isSystemDark ? "dark" : "light";
// actual DOM updates must be done in useEffect
useEffect(() => {
+41 -32
View File
@@ -1,51 +1,60 @@
import { unstable_cache as cache } from "next/cache";
import { cacheLife, cacheTag } from "next/cache";
import Image from "next/image";
import type { Tweet as TweetType } from "react-tweet/api";
import { EmbeddedTweet, TweetNotFound } from "react-tweet";
import { fetchTweet as _fetchTweet } from "react-tweet/api";
import { cn } from "@/lib/utils";
const fetchTweet = cache(_fetchTweet, undefined, {
revalidate: false, // cache indefinitely
tags: ["tweet"],
});
const fetchTweet = async (id: string) => {
"use cache";
cacheLife("max"); // cache indefinitely
cacheTag("tweet", `tweet-${id}`);
return _fetchTweet(id);
};
const TweetContent = ({ data, className }: { data: TweetType; className?: string }) => {
return (
<div
className={cn(
"my-6 min-h-30",
"*:[--tweet-body-font-size:var(--text-base)]! *:[--tweet-body-line-height:var(--leading-normal)]! *:[--tweet-container-margin:0_auto]! *:[--tweet-font-family:var(--font-sans)]! *:[--tweet-info-font-size:var(--text-sm)]! *:[--tweet-info-line-height:var(--leading-normal)]!",
className
)}
>
<EmbeddedTweet
tweet={data}
components={{
// https://react-tweet.vercel.app/twitter-theme/api-reference#custom-tweet-components
// eslint-disable-next-line jsx-a11y/alt-text
AvatarImg: (props) => <Image {...props} unoptimized />,
// eslint-disable-next-line jsx-a11y/alt-text
MediaImg: (props) => <Image {...props} fill unoptimized />,
}}
/>
</div>
);
};
const Tweet = async ({ id, className }: { id: string; className?: string }) => {
try {
const { data } = await fetchTweet(id);
let data: TweetType | undefined;
return (
<div
className={cn(
"my-6 min-h-30",
"*:[--tweet-body-font-size:var(--text-base)]! *:[--tweet-body-line-height:var(--leading-normal)]! *:[--tweet-container-margin:0_auto]! *:[--tweet-font-family:var(--font-sans)]! *:[--tweet-info-font-size:var(--text-sm)]! *:[--tweet-info-line-height:var(--leading-normal)]!",
className
)}
>
{data ? (
<EmbeddedTweet
tweet={data}
components={{
// https://react-tweet.vercel.app/twitter-theme/api-reference#custom-tweet-components
// eslint-disable-next-line jsx-a11y/alt-text
AvatarImg: (props) => <Image {...props} unoptimized />,
// eslint-disable-next-line jsx-a11y/alt-text
MediaImg: (props) => <Image {...props} fill unoptimized />,
}}
/>
) : (
<TweetNotFound />
)}
</div>
);
try {
const result = await fetchTweet(id);
data = result?.data;
} catch (error) {
console.error(error);
}
if (!data) {
return (
<div className={cn("min-h-30 *:mx-auto! *:font-sans!", className)}>
<div className={cn("my-6 min-h-30 *:mx-auto! *:font-sans!", className)}>
<TweetNotFound />
</div>
);
}
return <TweetContent data={data} className={className} />;
};
export default Tweet;
+29 -16
View File
@@ -1,26 +1,39 @@
"use client";
import { useEffect, useState } from "react";
import { env } from "@/lib/env";
import { connection } from "next/server";
import CountUp from "@/components/count-up";
import { incrementViews } from "@/lib/views";
import { incrementViews } from "@/lib/server/views";
const ViewCounter = async ({ slug }: { slug: string }) => {
// ensure this component isn't triggered by prerenders and/or preloads
await connection();
const ViewCounter = ({ slug }: { slug: string }) => {
const [views, setViews] = useState<number | null>(null);
const [error, setError] = useState(false);
try {
const hits = await incrementViews(slug);
// we have data!
return (
<span title={`${Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(hits)} ${hits === 1 ? "view" : "views"}`}>
<CountUp start={0} end={hits} delay={0} duration={1.5} />
</span>
);
} catch (error) {
console.error("[view-counter] fatal error:", error);
useEffect(() => {
// Increment views on client mount (outside of render phase)
incrementViews(slug)
.then((hits) => {
setViews(hits);
})
.catch((error) => {
console.error("[view-counter] error:", error);
setError(true);
});
}, [slug]);
if (error) {
return <span title="Error getting views! :(">?</span>;
}
if (views === null) {
return <span className="motion-safe:animate-pulse">0</span>;
}
return (
<span title={`${Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(views)} ${views === 1 ? "view" : "views"}`}>
<CountUp start={0} end={views} delay={0} duration={1.5} />
</span>
);
};
export default ViewCounter;