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:
+1
-2
@@ -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
@@ -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,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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
@@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
@@ -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
@@ -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[]) {
|
||||
|
||||
Reference in New Issue
Block a user