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

refactor: migrate from Biome to oxlint/oxfmt, remove contact form

- Replace Biome with oxlint + oxfmt (OXC toolchain) for linting and formatting
- Add .oxlintrc.json and .oxfmtrc.json configuration files
- Update VS Code settings and devcontainer to use oxc-vscode extension
- Remove contact form, Resend email integration, and related server action/schema
- Remove unused UI components (accordion, alert, card, tabs, toggle, etc.)
This commit is contained in:
2026-04-05 19:45:18 -04:00
parent b857ab2754
commit 5a1636baa3
114 changed files with 4901 additions and 5258 deletions
+1 -2
View File
@@ -1,6 +1,7 @@
import { type BetterAuthOptions, betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { nextCookies } from "better-auth/next-js";
import { db } from "@/lib/db";
import * as schema from "@/lib/db/schema";
@@ -13,9 +14,7 @@ export const auth = betterAuth({
plugins: [nextCookies()],
socialProviders: {
github: {
// biome-ignore lint/style/noNonNullAssertion: expected to be set in env
clientId: process.env.AUTH_GITHUB_CLIENT_ID!,
// biome-ignore lint/style/noNonNullAssertion: expected to be set in env
clientSecret: process.env.AUTH_GITHUB_CLIENT_SECRET!,
mapProfileToUser: (profile) => ({
name: profile.login,
+2 -5
View File
@@ -1,8 +1,8 @@
import { Feed, type Item as FeedItem } from "feed";
import ogImage from "@/app/opengraph-image.jpg";
import authorConfig from "@/lib/config/author";
import siteConfig from "@/lib/config/site";
import { getContent, getFrontMatter } from "@/lib/posts";
/**
@@ -52,10 +52,7 @@ export const buildFeed = async (): Promise<Feed> => {
);
// sort posts reverse chronologically in case the promises resolved out of order
posts.sort(
(post1, post2) =>
new Date(post2.date).getTime() - new Date(post1.date).getTime(),
);
posts.sort((post1, post2) => new Date(post2.date).getTime() - new Date(post1.date).getTime());
// officially add each post to the feed
posts.forEach((post) => {
+1
View File
@@ -1,6 +1,7 @@
import { attachDatabasePool } from "@vercel/functions";
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import * as schema from "@/lib/db/schema";
// Create explicit pool instance for better connection management
+2 -6
View File
@@ -1,9 +1,9 @@
import type { Metadata } from "next";
import authorConfig from "@/lib/config/author";
import siteConfig from "@/lib/config/site";
export const defaultMetadata: Metadata = {
// biome-ignore lint/style/noNonNullAssertion: expected to be set in env
metadataBase: new URL(process.env.NEXT_PUBLIC_BASE_URL!),
title: {
template: `%s ${siteConfig.name}`,
@@ -49,16 +49,12 @@ export const defaultMetadata: Metadata = {
* Helper function to deep merge a page's metadata into the default site metadata
* @see https://nextjs.org/docs/app/api-reference/functions/generate-metadata
*/
export const createMetadata = (
metadata: Metadata & { canonical: string },
): Metadata => ({
export const createMetadata = (metadata: Metadata & { canonical: string }): Metadata => ({
...defaultMetadata,
...metadata,
openGraph: {
...defaultMetadata.openGraph,
// biome-ignore lint/style/noNonNullAssertion: title is always provided by callers
title: metadata.title!,
// biome-ignore lint/style/noNonNullAssertion: description is always provided by callers
description: metadata.description!,
url: metadata.canonical,
...metadata.openGraph,
+2 -7
View File
@@ -2,10 +2,7 @@ import { cacheLife } from "next/cache";
// Load a Google Font from the Google Fonts API
// Adapted from https://github.com/brianlovin/briOS/blob/f72dc33a11194de45c80337b22be4560da62ad7e/src/lib/og-utils.tsx#L32
export async function loadGoogleFont(
font: string,
weight: number,
): Promise<ArrayBuffer> {
export async function loadGoogleFont(font: string, weight: number): Promise<ArrayBuffer> {
"use cache";
const url = `https://fonts.googleapis.com/css2?family=${font}:wght@${weight}`;
@@ -16,9 +13,7 @@ export async function loadGoogleFont(
},
});
const css = await cssResponse.text();
const resource = css.match(
/src: url\((.+)\) format\('(opentype|truetype)'\)/,
);
const resource = css.match(/src: url\((.+)\) format\('(opentype|truetype)'\)/);
if (resource) {
const fontResponse = await fetch(resource[1], {
+6 -18
View File
@@ -1,5 +1,6 @@
import fs from "node:fs/promises";
import path from "node:path";
import glob from "fast-glob";
import { decode } from "html-entities";
import { unified } from "unified";
@@ -40,9 +41,7 @@ export const getSlugs = async (): Promise<string[]> => {
});
// strip the .mdx extensions from filenames
const slugs = mdxFiles.map((fileName) =>
fileName.replace(/\/index\.mdx$/, ""),
);
const slugs = mdxFiles.map((fileName) => fileName.replace(/\/index\.mdx$/, ""));
return slugs;
};
@@ -92,10 +91,7 @@ export const getFrontMatter: {
permalink: `${process.env.NEXT_PUBLIC_BASE_URL}/${POSTS_DIR}/${slug}`,
} as FrontMatter;
} catch (error) {
console.error(
`Failed to load front matter for post with slug "${slug}":`,
error,
);
console.error(`Failed to load front matter for post with slug "${slug}":`, error);
return undefined;
}
}
@@ -110,8 +106,7 @@ export const getFrontMatter: {
// sort the results reverse chronologically and return
return posts.sort(
(post1, post2) =>
new Date(post2.date).getTime() - new Date(post1.date).getTime(),
(post1, post2) => new Date(post2.date).getTime() - new Date(post1.date).getTime(),
);
}
@@ -153,11 +148,7 @@ export const getContent = async (slug: string): Promise<string | undefined> => {
],
})
.use(rehypeStringify)
.process(
await fs.readFile(
path.join(process.cwd(), `${POSTS_DIR}/${slug}/index.mdx`),
),
);
.process(await fs.readFile(path.join(process.cwd(), `${POSTS_DIR}/${slug}/index.mdx`)));
// convert the parsed content to a string with "safe" HTML
return content
@@ -166,10 +157,7 @@ export const getContent = async (slug: string): Promise<string | undefined> => {
.replaceAll("<p></p>", "")
.trim();
} catch (error) {
console.error(
`Failed to load/parse content for post with slug "${slug}":`,
error,
);
console.error(`Failed to load/parse content for post with slug "${slug}":`, error);
return undefined;
}
};
-1
View File
@@ -5,4 +5,3 @@ export { default as rehypeSanitize } from "rehype-sanitize";
export { default as rehypeSlug } from "rehype-slug";
export { default as rehypeStringify } from "rehype-stringify";
export { default as rehypeUnwrapImages } from "rehype-unwrap-images";
export { default as rehypeWrapper } from "rehype-wrapper";
-14
View File
@@ -1,14 +0,0 @@
import { z } from "zod";
export const ContactSchema = z
.object({
name: z.string().trim().min(1, { message: "Your name is required." }),
email: z.string().email({ message: "Your email address is required." }),
message: z
.string()
.trim()
.min(15, { message: "Your message must be at least 15 characters." }),
})
.readonly();
export type ContactInput = z.infer<typeof ContactSchema>;
+17 -43
View File
@@ -1,9 +1,9 @@
"use server";
import { checkBotId } from "botid/server";
import { desc, eq, inArray, sql } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { headers } from "next/headers";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";
import * as schema from "@/lib/db/schema";
@@ -12,9 +12,7 @@ export type CommentWithUser = typeof schema.comment.$inferSelect & {
user: Pick<typeof schema.user.$inferSelect, "id" | "name" | "image">;
};
export const getComments = async (
pageSlug: string,
): Promise<CommentWithUser[]> => {
export const getComments = async (pageSlug: string): Promise<CommentWithUser[]> => {
try {
// Fetch all comments for the page with user details
const commentsWithUsers = await db
@@ -24,15 +22,16 @@ export const getComments = async (
.where(eq(schema.comment.pageSlug, pageSlug))
.orderBy(desc(schema.comment.createdAt));
return commentsWithUsers.map(({ comment, user }) => ({
...comment,
user: {
// we're namely worried about keeping the user's email private here, but nothing sensitive is stored in the db
id: user.id,
name: user.name,
image: user.image,
},
}));
return commentsWithUsers.map(({ comment, user }) =>
Object.assign(comment, {
user: {
// we're namely worried about keeping the user's email private here, but nothing sensitive is stored in the db
id: user.id,
name: user.name,
image: user.image,
},
}),
);
} catch (error) {
console.error("[server/comments] error fetching comments:", error);
// Return empty array instead of throwing during prerendering
@@ -75,9 +74,7 @@ export const getCommentCountsForSlugs = async (
.where(inArray(schema.comment.pageSlug, slugs))
.groupBy(schema.comment.pageSlug);
const map: Record<string, number> = Object.fromEntries(
slugs.map((s) => [s, 0]),
);
const map: Record<string, number> = Object.fromEntries(slugs.map((s) => [s, 0]));
for (const row of rows) {
map[row.pageSlug] = Number(row.count ?? 0);
}
@@ -91,9 +88,7 @@ export const getCommentCountsForSlugs = async (
/**
* Retrieves the numbers of comments for ALL slugs
*/
export const getAllCommentCounts = async (): Promise<
Record<string, number>
> => {
export const getAllCommentCounts = async (): Promise<Record<string, number>> => {
try {
const rows = await db
.select({
@@ -119,13 +114,6 @@ export const createComment = async (data: {
pageSlug: string;
parentId?: string;
}) => {
// BotID server-side verification
const verification = await checkBotId();
if (verification.isBot) {
console.warn("[server/comments] botid verification failed:", verification);
throw new Error("Bot check failed 🤖");
}
const session = await auth.api.getSession({
headers: await headers(),
});
@@ -147,18 +135,11 @@ export const createComment = async (data: {
revalidatePath(`/${data.pageSlug}`);
} catch (error) {
console.error("[server/comments] error creating comment:", error);
throw new Error("Failed to create comment");
throw new Error("Failed to create comment", { cause: error });
}
};
export const updateComment = async (commentId: string, content: string) => {
// BotID server-side verification
const verification = await checkBotId();
if (verification.isBot) {
console.warn("[server/comments] botid verification failed:", verification);
throw new Error("Bot check failed 🤖");
}
const session = await auth.api.getSession({
headers: await headers(),
});
@@ -200,18 +181,11 @@ export const updateComment = async (commentId: string, content: string) => {
revalidatePath(`/${comment.pageSlug}`);
} catch (error) {
console.error("[server/comments] error updating comment:", error);
throw new Error("Failed to update comment");
throw new Error("Failed to update comment", { cause: error });
}
};
export const deleteComment = async (commentId: string) => {
// BotID server-side verification
const verification = await checkBotId();
if (verification.isBot) {
console.warn("[server/comments] botid verification failed:", verification);
throw new Error("Bot check failed 🤖");
}
const session = await auth.api.getSession({
headers: await headers(),
});
@@ -247,6 +221,6 @@ export const deleteComment = async (commentId: string) => {
revalidatePath(`/${comment.pageSlug}`);
} catch (error) {
console.error("[server/comments] error deleting comment:", error);
throw new Error("Failed to delete comment");
throw new Error("Failed to delete comment", { cause: error });
}
};
-69
View File
@@ -1,69 +0,0 @@
"use server";
import { checkBotId } from "botid/server";
import { Resend } from "resend";
import siteConfig from "@/lib/config/site";
import { ContactSchema } from "@/lib/schemas/contact";
export type ContactResult = {
success: boolean;
message: string;
errors?: Record<string, string[]>;
};
export const sendContactForm = async (
formData: FormData,
): Promise<ContactResult> => {
// TODO: remove after debugging why automated spam entries are causing 500 errors
console.debug("[server/contact] received payload:", formData);
// BotID server-side verification
const verification = await checkBotId();
if (verification.isBot) {
console.warn("[server/contact] botid verification failed:", verification);
return {
success: false,
message: "Verification failed. Please try again.",
};
}
const parsed = ContactSchema.safeParse(Object.fromEntries(formData));
if (!parsed.success) {
return {
success: false,
message: "Please make sure all fields are filled in correctly.",
errors: parsed.error.flatten().fieldErrors,
};
}
try {
if (process.env.RESEND_FROM_EMAIL === "onboarding@resend.dev") {
// https://resend.com/docs/api-reference/emails/send-email
console.warn(
"[server/contact] 'RESEND_FROM_EMAIL' is not set, falling back to onboarding@resend.dev.",
);
}
const resend = new Resend(process.env.RESEND_API_KEY);
await resend.emails.send({
from: `${parsed.data.name} <${process.env.RESEND_FROM_EMAIL || "onboarding@resend.dev"}>`,
replyTo: `${parsed.data.name} <${parsed.data.email}>`,
// biome-ignore lint/style/noNonNullAssertion: expected to be set in env
to: [process.env.RESEND_TO_EMAIL!],
subject: `[${siteConfig.name}] Contact Form Submission`,
text: parsed.data.message,
});
return { success: true, message: "Thanks! You should hear from me soon." };
} catch (error) {
console.error("[server/contact] fatal error:", error);
return {
success: false,
message:
"Internal server error. Please try again later or shoot me an email.",
};
}
};
+5 -12
View File
@@ -2,6 +2,7 @@
import { eq, inArray, sql } from "drizzle-orm";
import { revalidateTag } from "next/cache";
import { db } from "@/lib/db";
import { page } from "@/lib/db/schema";
@@ -10,11 +11,7 @@ import { page } from "@/lib/db/schema";
*/
export const getViewCount = async (slug: string): Promise<number> => {
try {
const pages = await db
.select()
.from(page)
.where(eq(page.slug, slug))
.limit(1);
const pages = await db.select().from(page).where(eq(page.slug, slug)).limit(1);
return pages[0]?.views ?? 0;
} catch (error) {
console.error("[server/views] fatal error:", error);
@@ -25,14 +22,10 @@ export const getViewCount = async (slug: string): Promise<number> => {
/**
* Retrieves the numbers of views for an array of slugs, returning 0 for any that don't exist
*/
export const getViewCountsForSlugs = async (
slugs: string[],
): Promise<Record<string, number>> => {
export const getViewCountsForSlugs = async (slugs: string[]): Promise<Record<string, number>> => {
try {
const pages = await db.select().from(page).where(inArray(page.slug, slugs));
const viewMap: Record<string, number> = Object.fromEntries(
slugs.map((s) => [s, 0]),
);
const viewMap: Record<string, number> = Object.fromEntries(slugs.map((s) => [s, 0]));
for (const p of pages) {
viewMap[p.slug] = p.views;
}
@@ -83,6 +76,6 @@ export const incrementViews = async (slug: string): Promise<number> => {
return result.views;
} catch (error) {
console.error("[server/views] error incrementing views:", error);
throw new Error("Failed to increment views");
throw new Error("Failed to increment views", { cause: error });
}
};
+1 -1
View File
@@ -1,4 +1,4 @@
import { type ClassValue, clsx } from "clsx";
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {