fixed build performance issues; optimized queries

This commit is contained in:
2025-03-15 20:31:38 +01:00
parent cff9dad54c
commit 324451e082
27 changed files with 289 additions and 208 deletions
+2 -2
View File
@@ -10,8 +10,8 @@ import { Button } from "@/components/ui/button";
import Link from "next/link";
export default async function Home() {
const categories = await api.category.getAll({ limit: 6 });
const articles = await api.article.getAll({ limit: 6 });
const categories = await api.category.getMany({ limit: 6 });
const articles = await api.article.getMany({ limit: 6 });
return (
<>
<Alert>
+1 -2
View File
@@ -1,8 +1,7 @@
import * as cheerio from "cheerio";
import { NextApiRequest } from "next";
import { NextResponse } from "next/server";
export async function GET(req: NextApiRequest) {
export async function GET(req: Request) {
if (req.method !== "GET") {
return NextResponse.json({ error: "Method not allowed" });
}
+2 -1
View File
@@ -25,7 +25,8 @@ import CategorySelect from "@/components/category/category-select";
import { CheckCircle, XCircle } from "lucide-react";
import PublishArticleAlertDialog from "./publish-article-alert-dialog";
import { Label } from "@/components/ui/label";
import Editor from "../editor";
import dynamic from "next/dynamic";
const Editor = dynamic(() => import("../editor"), { ssr: false });
export default ({ server_article }: { server_article: Article }) => {
const [loading, setLoading] = React.useState(false);
@@ -19,6 +19,9 @@ function InfiniteArticlesGrid() {
},
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
staleTime: 60 * 4 * 1000, // 4 minutes stale time
refetchOnMount: false, // Prevents unnecessary refetching
refetchOnWindowFocus: false, // Avoids refetch when switching tabs
},
);
// Calculate all visible items across all loaded pages
+2 -2
View File
@@ -1,6 +1,6 @@
import React from "react";
import { JSONContent } from "novel";
function RenderArticle({ content }: { content: string }) {
function RenderArticle({ content }: { content: JSONContent }) {
return "render article: in work"; //<Editor readOnly editorProviderProps={{ content: content }} />;
}
+1 -1
View File
@@ -5,7 +5,7 @@ import { Combobox, ComboboxProps } from "../combobox";
import { Icons } from "../icons";
function CategorySelect(props: Partial<ComboboxProps>) {
const { data: categories } = api.category.getAll.useQuery();
const { data: categories } = api.category.getMany.useQuery();
return (
<Combobox
{...(props as ComboboxProps)}
@@ -9,8 +9,8 @@ export const CATEGORY_GRID_CLASS =
function CategoryGrid({ categories }: { categories: Category[] }) {
return (
<menu className={CATEGORY_GRID_CLASS}>
{categories.map((category) => (
<li key={category.id}>
{categories.map((category, idx) => (
<li key={idx}>
<CategoryCard {...category} />
</li>
))}
@@ -18,6 +18,9 @@ export default function InfiniteCategoryGrid() {
},
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
staleTime: 60 * 4 * 1000, // 4 minutes stale time
refetchOnMount: false, // Prevents unnecessary refetching
refetchOnWindowFocus: false, // Avoids refetch when switching tabs
},
);
// Calculate all visible items across all loaded pages
@@ -1,20 +0,0 @@
"use client";
import ColorPicker, { type ColorPickerProps } from ".";
import { Button } from "../ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
export default function ColorPickerPopover(props: ColorPickerProps) {
return (
<Popover>
<PopoverTrigger asChild>
<Button style={{ backgroundColor: props.initialColor }} className="">
color
</Button>
</PopoverTrigger>
<PopoverContent>
<ColorPicker {...props} />
</PopoverContent>
</Popover>
);
}
@@ -1,16 +0,0 @@
.color-picker .react-colorful {
width: 100%;
height: 240px;
}
.color-picker .react-colorful__saturation {
border-radius: 4px 4px 0 0;
}
.color-picker .react-colorful__hue {
height: 40px;
border-radius: 0 0 4px 4px;
}
.color-picker .react-colorful__hue-pointer {
width: 12px;
height: inherit;
border-radius: 0;
}
-103
View File
@@ -1,103 +0,0 @@
"use client";
import "./color-picker.css";
import React from "react";
import { useState, useEffect } from "react";
import { HexColorPicker } from "react-colorful";
import { Input } from "../ui/input";
import { debounce } from "@/lib/utils";
const STORAGE_KEY = "savedColors";
const MAX_COLORS = 12;
const SELECT_DEBOUNCE = 500;
export type ColorPickerProps = {
onInput?: (color: string) => void;
initialColor?: string;
};
function ColorPicker({ onInput, initialColor }: ColorPickerProps) {
const [mounted, setMounted] = useState(false);
const [customColors, setCustomColors] = useState<string[]>([]);
const [color, setColor] = useState(initialColor ?? "#ff0000");
// Load colors from localStorage on mount
useEffect(() => {
if (initialColor?.length) setColor(initialColor);
const storageColors = localStorage.getItem(STORAGE_KEY);
const storedColors = storageColors ? JSON.parse(storageColors) : [];
if (storedColors.length) {
setCustomColors(storedColors);
if (!initialColor?.length) setColor(storedColors[0]);
setMounted(true);
}
}, []);
const persistColor = (newColor: string) => {
if (newColor.length < 2) return;
if (customColors[0] === newColor) return; // Prevent duplicate consecutive colors
const updatedColors = [
newColor,
...customColors.filter((c) => c !== newColor),
].slice(0, MAX_COLORS);
console.log(updatedColors);
setCustomColors(updatedColors);
localStorage.setItem(STORAGE_KEY, JSON.stringify(updatedColors));
};
const selectColor = (newColor: string) => {
persistColor(newColor);
onInput?.(newColor);
};
const selectColorDebounced = debounce((newColor: string) => {
selectColor(newColor);
}, SELECT_DEBOUNCE);
const handleColorChange = (newColor: string, skipDebounce = false) => {
setColor(newColor);
if (skipDebounce)
selectColor(newColor); // Delayed save
else selectColorDebounced(newColor);
};
return (
<div className="color-picker flex flex-col items-center gap-4">
{/* React Colorful Picker */}
<HexColorPicker
color={color}
onChange={handleColorChange}
className="w-full"
/>
{/* Default Input (Native Color Picker) */}
<div className="flex w-full gap-2">
<div
className="size-8 rounded-md border"
style={{ backgroundColor: color }}
/>
<Input
className="h-8"
value={color}
onInput={(e) => handleColorChange(e.currentTarget.value)}
/>
</div>
{/* Display Recent Colors */}
<div className="flex flex-wrap gap-2">
{customColors.map((col) => (
<button
key={col}
className="size-8 rounded border"
style={{ backgroundColor: col }}
onClick={() => handleColorChange(col, true)}
/>
))}
</div>
</div>
);
}
export default ColorPicker;
-1
View File
@@ -36,7 +36,6 @@ export function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
"use no memo";
const [pagination, setPagination] = React.useState<PaginationState>({
pageSize: 25,
pageIndex: 0,
+27
View File
@@ -0,0 +1,27 @@
import winston from "winston";
const customLevels = {
levels: {
error: 0,
warn: 1,
info: 2,
sql: 3,
debug: 4,
},
colors: {
error: "red",
warn: "yellow",
info: "green",
sql: "magenta",
debug: "blue",
},
};
winston.addColors(customLevels.colors);
export const logger = winston.createLogger({
levels: customLevels.levels,
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: "db_queries.log", level: "sql" }),
new winston.transports.File({ filename: "errors.log", level: "error" }),
],
});
+4
View File
@@ -1,8 +1,12 @@
"use server";
import fs from "node:fs/promises";
import { revalidatePath } from "next/cache";
import { auth } from "../auth";
import { hasPermission, Role } from "@/lib/validation/permissions";
export async function uploadFile(formData: FormData) {
const session = await auth();
if (!session || hasPermission(session.user.role, Role.EDITOR)) return false;
const file = formData.get("file") as File;
const arrayBuffer = await file.arrayBuffer();
const buffer = new Uint8Array(arrayBuffer);
-23
View File
@@ -1,29 +1,6 @@
import { z } from "zod";
import { createTRPCRouter, publicProcedure } from "../trpc";
import { desc, ilike, like } from "drizzle-orm";
import {
articles as articlesTable,
categories as categoriesTable,
lower,
} from "@/server/db/schema";
export const appRouter = createTRPCRouter({
searchContent: publicProcedure
.input(
z.object({
query: z.string().optional(),
}),
)
.query(async ({ ctx, input }) => {
const articles = await ctx.db.query.articles.findMany({
where: ilike(articlesTable.title, "%" + input.query + "%"),
});
const categories = await ctx.db.query.categories.findMany({
where: like(categoriesTable.name, "%" + input.query + "%"),
});
return { articles, categories };
}),
getSidebarMain: publicProcedure.query(async ({ ctx }) => {
const categories = await ctx.db.query.categories.findMany({
limit: 3,
+5 -5
View File
@@ -71,10 +71,10 @@ export const articleRouter = createTRPCRouter({
get: publicProcedure
.input(z.object({ slug: z.string() }))
.query(async ({ ctx, input }) => {
return await ctx.db.query.articles.findFirst({
return (await ctx.db.query.articles.findFirst({
where: eq(articles.slug, input.slug),
with: { category: true },
});
})) as Article;
}),
getByCursor: publicProcedure
.input(
@@ -131,7 +131,6 @@ export const articleRouter = createTRPCRouter({
let nextCursor: string | undefined = undefined;
if (items.length > limit) {
console.log("Configure next cursor");
const cursorItem = items.pop();
// Create a cursor object with the relevant fields for sorting
const cursorData: ArticleCursor = {
@@ -150,7 +149,7 @@ export const articleRouter = createTRPCRouter({
nextCursor,
};
}),
getAll: publicProcedure
getMany: publicProcedure
.input(
z
.object({
@@ -160,11 +159,12 @@ export const articleRouter = createTRPCRouter({
.optional(),
)
.query(async ({ ctx, input }) => {
const limit = input?.limit ?? 50;
return (await ctx.db.query.articles.findMany({
where: input?.categoryId
? eq(articles.categoryId, input.categoryId)
: undefined,
limit: input?.limit,
limit: limit,
columns: {
title: true,
slug: true,
+1 -1
View File
@@ -1,4 +1,4 @@
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
import { createTRPCRouter, publicProcedure } from "../trpc";
export const authorRouter = createTRPCRouter({
getCount: publicProcedure.query(async ({ ctx }) => {
+17 -13
View File
@@ -12,11 +12,11 @@ import {
count,
desc,
eq,
gt,
gte,
ilike,
like,
lte,
sql,
} from "drizzle-orm";
import { hasPermission, Role } from "@/lib/validation/permissions";
import {
@@ -60,13 +60,6 @@ const getCategorySorting = (sort: string, cursor?: CategoryCursor) => {
};
export const categoryRouter = createTRPCRouter({
search: publicProcedure
.input(z.object({ query: z.string() }))
.query(async ({ ctx, input }) => {
return await ctx.db.query.categories.findMany({
where: like(categories.name, "%" + input.query + "%"),
});
}),
get: publicProcedure
.input(z.object({ slug: z.string(), with: z.any() }))
.query(async ({ ctx, input }) => {
@@ -76,7 +69,7 @@ export const categoryRouter = createTRPCRouter({
})) as Category;
}),
getAll: publicProcedure
getMany: publicProcedure
.input(
z
.object({
@@ -85,9 +78,15 @@ export const categoryRouter = createTRPCRouter({
.optional(),
)
.query(async ({ ctx, input }) => {
return await ctx.db.query.categories.findMany({
limit: input?.limit,
});
const limit = input?.limit ?? 50;
return (await ctx.db.query.categories.findMany({
limit: limit,
columns: {
name: true,
slug: true,
createdAt: true,
},
})) as Category[];
}),
getByCursor: publicProcedure
@@ -135,7 +134,6 @@ export const categoryRouter = createTRPCRouter({
let nextCursor: string | undefined = undefined;
if (items.length > limit) {
console.log("Configure next cursor");
const cursorItem = items.pop();
// Create a cursor object with the relevant fields for sorting
const cursorData: CategoryCursor = {
@@ -172,6 +170,12 @@ export const categoryRouter = createTRPCRouter({
.values({ ...input.category, slug })
.returning({
slug: categories.slug,
})
.onConflictDoUpdate({
target: categories.slug,
set: {
slug: sql`${slug} || '-' || (SELECT COUNT(*) FROM ${categories} WHERE slug LIKE ${slug + "-%"})`,
},
});
}),
update: protectedProcedure
+7 -1
View File
@@ -13,7 +13,7 @@ export const usersRouter = createTRPCRouter({
.update(users)
.set(input.profile)
.where(eq(users.id, ctx.session.user.id))
.returning();
.returning({ id: users.id });
}),
getAll: protectedProcedure.query(async ({ ctx }) => {
@@ -21,6 +21,12 @@ export const usersRouter = createTRPCRouter({
if (!isAdmin) throw new Error("You are not allowed to get all users");
return await ctx.db.query.users.findMany({
orderBy: desc(users.role),
columns: {
name: true,
email: true,
role: true,
id: true,
},
});
}),
+3 -1
View File
@@ -15,6 +15,8 @@ const globalForDb = globalThis as unknown as {
const conn = globalForDb.conn ?? postgres(env.DATABASE_URL);
if (env.NODE_ENV !== "production") globalForDb.conn = conn;
export const db = drizzle(conn, { schema });
export const db = drizzle(conn, {
schema,
});
export type DBType = typeof db;
+7 -8
View File
@@ -1,7 +1,6 @@
import { createId } from "@paralleldrive/cuid2";
import { relations, SQL, sql } from "drizzle-orm";
import { relations, sql } from "drizzle-orm";
import {
AnyPgColumn,
boolean,
index,
integer,
@@ -16,10 +15,6 @@ import { User } from "next-auth";
import { type AdapterAccount } from "next-auth/adapters";
import { JSONContent } from "novel";
export function lower(value: AnyPgColumn): SQL {
return sql`lower(${value})`;
}
export const createTable = pgTableCreator((name) => `wiki-antifa_${name}`);
export const articles = createTable(
@@ -43,7 +38,9 @@ export const articles = createTable(
),
},
(example) => ({
articleTitleIndex: index("article_title_idx").on(example.title),
titleIndex: index("article_title_idx").on(example.title),
slugIndex: index("article_slug_idx").on(example.slug),
createdAtIndex: index("article_created_at_idx").on(example.createdAt),
}),
);
export type Article = typeof articles.$inferSelect & {
@@ -81,7 +78,9 @@ export const categories = createTable(
),
},
(example) => ({
categoryNameIndex: index("category_name_idx").on(example.name),
nameIndex: index("category_name_idx").on(example.name),
slugameIndex: index("category_slug_idx").on(example.slug),
createdAtIndex: index("category_created_at_idx").on(example.createdAt),
}),
);
export type Category = typeof categories.$inferSelect & {