added category infinite grid and fixed missing items at useInfiniteQuery

This commit is contained in:
2025-03-15 15:20:17 +01:00
parent f591e18847
commit 4ad0357133
15 changed files with 321 additions and 1100 deletions
+69 -17
View File
@@ -11,22 +11,50 @@ import {
articleSchema,
createArticleSchema,
} from "@/lib/validation/zod/article";
import { and, asc, count, desc, eq, gt, ilike, like, sql } from "drizzle-orm";
import {
and,
asc,
count,
desc,
eq,
gte,
ilike,
like,
lte,
sql,
} from "drizzle-orm";
import { hasPermission, Role } from "@/lib/validation/permissions";
import { generateSlug } from "@/lib/utils";
import SuperJSON from "superjson";
type ArticleCursor = Pick<Article, "slug" | "createdAt" | "title">;
const getArticleSorting = (sort: string, cursor?: ArticleCursor) => {
// Default to newest
const baseCase = {
orderBy: [desc(articles.createdAt), asc(articles.slug)],
cursor: cursor ? lte(articles.createdAt, cursor.createdAt) : undefined,
};
const getArticleSorting = (sort: string) => {
switch (sort) {
case "newest":
return desc(articles.createdAt);
return baseCase;
case "oldest":
return asc(articles.createdAt);
return {
orderBy: [asc(articles.createdAt), asc(articles.slug)],
cursor: cursor ? gte(articles.createdAt, cursor.createdAt) : undefined,
};
case "abc":
return asc(articles.title);
return {
orderBy: [asc(articles.title), asc(articles.slug)],
cursor: cursor ? gte(articles.title, cursor.title) : undefined,
};
case "cba":
return desc(articles.title);
return {
orderBy: [desc(articles.title), asc(articles.slug)],
cursor: cursor ? lte(articles.title, cursor.title) : undefined,
};
default:
return desc(articles.createdAt); // Default to newest
return baseCase;
}
};
@@ -59,15 +87,32 @@ export const articleRouter = createTRPCRouter({
.query(async ({ ctx, input }) => {
const { cursor } = input!;
const limit = input?.limit ?? 50;
const cursorArg = cursor ? gt(articles.slug, cursor) : undefined;
// Decode cursor if using it
let cursorObj: ArticleCursor | undefined;
if (cursor) {
try {
cursorObj = SuperJSON.parse(Buffer.from(cursor, "base64").toString());
} catch (e) {
// Handle invalid cursor
cursorObj = undefined;
}
}
const queryFilterArg = input?.filter?.query?.length
? ilike(articles.title, "%" + input.filter.query + "%")
: undefined;
const categoryArg = input?.filter?.category
? eq(articles.categoryId, input.filter.category)
: undefined;
const orderBy = getArticleSorting(input?.filter?.sort ?? "newest");
const sortConfig = input?.filter?.sort ?? "newest";
const { orderBy, cursor: cursorArg } = getArticleSorting(
sortConfig,
cursorObj,
);
const items = await ctx.db.query.articles.findMany({
where: and(
cursorArg,
@@ -83,15 +128,26 @@ export const articleRouter = createTRPCRouter({
createdAt: true,
},
});
let nextCursor: typeof cursor | undefined = undefined;
let nextCursor: string | undefined = undefined;
if (items.length > limit) {
const nextItem = items.pop();
nextCursor = nextItem!.slug;
console.log("Configure next cursor");
const cursorItem = items.pop();
// Create a cursor object with the relevant fields for sorting
const cursorData: ArticleCursor = {
slug: cursorItem!.slug,
createdAt: cursorItem!.createdAt,
title: cursorItem!.title,
};
// Encode the cursor as base64
nextCursor = Buffer.from(SuperJSON.stringify(cursorData)).toString(
"base64",
);
}
return {
items,
nextCursor,
previousCursor: cursor,
};
}),
getAll: publicProcedure
@@ -160,10 +216,6 @@ export const articleRouter = createTRPCRouter({
if (!isEditor) {
throw new Error("You are not allowed to update articles");
}
console.log(
"Content before save",
JSON.stringify(input.article.content),
);
return await ctx.db
.update(articles)
+115 -2
View File
@@ -6,10 +6,58 @@ import {
} from "@/server/api/trpc";
import { categories, Category } from "@/server/db/schema";
import { count, eq, like } from "drizzle-orm";
import {
and,
asc,
count,
desc,
eq,
gt,
gte,
ilike,
like,
lte,
} from "drizzle-orm";
import { hasPermission, Role } from "@/lib/validation/permissions";
import { categorySchema } from "@/lib/validation/zod/category";
import {
categoryFilterSchema,
categorySchema,
} from "@/lib/validation/zod/category";
import { generateSlug } from "@/lib/utils";
import SuperJSON from "superjson";
type CategoryCursor = Pick<Category, "slug" | "createdAt" | "name">;
const getCategorySorting = (sort: string, cursor?: CategoryCursor) => {
// Default to newest
const baseCase = {
orderBy: [desc(categories.createdAt), asc(categories.slug)],
cursor: cursor ? lte(categories.createdAt, cursor.createdAt) : undefined,
};
switch (sort) {
case "newest":
return baseCase;
case "oldest":
return {
orderBy: [asc(categories.createdAt), asc(categories.slug)],
cursor: cursor
? gte(categories.createdAt, cursor.createdAt)
: undefined,
};
case "abc":
return {
orderBy: [asc(categories.name), asc(categories.slug)],
cursor: cursor ? gte(categories.name, cursor.name) : undefined,
};
case "cba":
return {
orderBy: [desc(categories.name), asc(categories.slug)],
cursor: cursor ? lte(categories.name, cursor.name) : undefined,
};
default:
return baseCase;
}
};
export const categoryRouter = createTRPCRouter({
search: publicProcedure
@@ -42,6 +90,71 @@ export const categoryRouter = createTRPCRouter({
});
}),
getByCursor: publicProcedure
.input(
z.object({
limit: z.number().optional(),
cursor: z.string().optional(),
filter: categoryFilterSchema.optional(),
}),
)
.query(async ({ ctx, input }) => {
const { cursor } = input!;
const limit = input?.limit ?? 50;
// Decode cursor if using it
let cursorObj: CategoryCursor | undefined;
if (cursor) {
try {
cursorObj = SuperJSON.parse(Buffer.from(cursor, "base64").toString());
} catch (e) {
// Handle invalid cursor
cursorObj = undefined;
}
}
const queryFilterArg = input?.filter?.query?.length
? ilike(categories.name, "%" + input.filter.query + "%")
: undefined;
const sortConfig = input?.filter?.sort ?? "newest";
const { orderBy, cursor: cursorArg } = getCategorySorting(
sortConfig,
cursorObj,
);
const items = (await ctx.db.query.categories.findMany({
where: and(cursorArg, queryFilterArg),
limit: limit + 1,
orderBy,
columns: {
name: true,
slug: true,
createdAt: true,
},
})) as Category[];
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 = {
slug: cursorItem!.slug,
createdAt: cursorItem!.createdAt,
name: cursorItem!.name,
};
// Encode the cursor as base64
nextCursor = Buffer.from(SuperJSON.stringify(cursorData)).toString(
"base64",
);
}
return {
items,
nextCursor,
};
}),
getCount: publicProcedure.query(async ({ ctx }) => {
return (await ctx.db.select({ count: count() }).from(categories))[0]?.count;
}),
+2 -2
View File
@@ -29,7 +29,7 @@ export const articles = createTable(
.primaryKey()
.$defaultFn(() => createId())
.notNull(),
title: varchar("title", { length: 256 }),
title: varchar("title", { length: 256 }).notNull(),
slug: varchar("slug", { length: 256 }).unique().notNull(),
authorId: varchar("author_id", { length: 255 }),
content: jsonb("content").$type<JSONContent>(),
@@ -68,7 +68,7 @@ export const categories = createTable(
.primaryKey()
.$defaultFn(() => createId())
.notNull(),
name: varchar("name", { length: 256 }),
name: varchar("name", { length: 256 }).notNull(),
description: text("description"),
slug: varchar("slug", { length: 256 }).unique().notNull(),