feat: add product collections

This commit is contained in:
2025-07-20 17:22:47 +07:00
parent acbcd1884e
commit f8070e1dd4
11 changed files with 226 additions and 7 deletions
@@ -1,14 +1,34 @@
import { Suspense } from 'react';
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
import { getQueryClient, trpc } from '@/trpc/server';
import {
ProductList,
ProductListSkeleton,
} from '@/modules/products/ui/components/product-list';
interface Props {
params: Promise<{
category: string;
subcategory: string;
}>;
}
const Page = async ({ params }: Props) => {
const { category, subcategory } = await params;
const { subcategory } = await params;
const queryClient = getQueryClient();
void queryClient.prefetchQuery(
trpc.products.getMany.queryOptions({
category: subcategory,
})
);
return <div>Subcategory Page: {subcategory} in Category: {category}</div>;
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<Suspense fallback={<ProductListSkeleton />}>
<ProductList category={subcategory} />
</Suspense>
</HydrationBoundary>
);
};
export default Page;
+22 -1
View File
@@ -1,3 +1,12 @@
import { Suspense } from 'react';
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
import { getQueryClient, trpc } from '@/trpc/server';
import {
ProductList,
ProductListSkeleton,
} from '@/modules/products/ui/components/product-list';
interface Props {
params: Promise<{
category: string;
@@ -6,8 +15,20 @@ interface Props {
const Page = async ({ params }: Props) => {
const { category } = await params;
const queryClient = getQueryClient();
void queryClient.prefetchQuery(
trpc.products.getMany.queryOptions({
category,
})
);
return <div>Category Page: {category}</div>;
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<Suspense fallback={<ProductListSkeleton />}>
<ProductList category={category} />
</Suspense>
</HydrationBoundary>
);
};
export default Page;
+3
View File
@@ -2,6 +2,9 @@ import type { CollectionConfig } from "payload";
export const Categories: CollectionConfig = {
slug: "categories",
admin: {
useAsTitle: 'name',
},
fields: [
{
name: 'name',
+48
View File
@@ -0,0 +1,48 @@
import { CollectionConfig } from "payload";
export const Products: CollectionConfig = {
slug: "products",
fields: [
{
name: 'name',
type: 'text',
required: true,
},
{
name: 'description',
type: 'text',
},
{
name: 'price',
type: 'number',
required: true,
admin: {
description: 'Price in USD',
}
},
{
name: 'category',
type: 'relationship',
relationTo: 'categories',
hasMany: false,
},
{
name: 'images',
type: 'upload',
relationTo: 'media',
},
{
name: 'refundPolicy',
type: 'select',
options: [
'30-day',
'14-day',
'7-day',
'3-day',
'1-day',
'no-refunds',
],
defaultValue: '30-day',
}
]
}
+2 -1
View File
@@ -6,7 +6,8 @@ export const categoriesRouter = createTRPCRouter({
const data = await ctx.db.find({
collection: 'categories',
depth: 1,
depth: 1, // Populated subcategories, subcategories.[0] will be a type of "Category"
pagination: false,
where: {
parent: {
exists: false,
+58
View File
@@ -0,0 +1,58 @@
import type { Where } from "payload";
import { baseProcedure, createTRPCRouter } from "@/trpc/init";
import { z } from "zod";
import { Category } from "@/payload-types";
export const productsRouter = createTRPCRouter({
getMany: baseProcedure.input(z.object({
category: z.string().nullable().optional(),
})).query(async ({ ctx, input }) => {
const where: Where = {}
if (input.category) {
const categoriesData = await ctx.db.find({
collection: 'categories',
limit: 1,
depth: 1, // Populated subcategories, subcategories.[0] will be a type of "Category"
pagination: false,
where: {
slug: {
equals: input.category
}
}
})
const formattedData = categoriesData.docs.map((category) => ({
...category,
subcategories: (category.subcategories?.docs ?? []).map((doc) => ({
// Because of "depth: 1" we are confident "doc" will be a tpe of "Category"
...(doc as Category),
subcategories: undefined,
})),
}));
const subCategoriesSlugs = [];
const parentCategory = formattedData[0];
if (parentCategory) {
subCategoriesSlugs.push(
...parentCategory.subcategories.map((subcategory) => subcategory.slug)
);
}
if (parentCategory) {
where['category.slug'] = {
in: [parentCategory.slug, ...subCategoriesSlugs],
}
}
}
const data = await ctx.db.find({
collection: 'products',
depth: 1, // Populated "category" & "image"
where,
});
return data;
})
})
+5
View File
@@ -0,0 +1,5 @@
import { inferRouterOutputs } from "@trpc/server";
import { AppRouter } from "@/trpc/routers/_app";
export type ProductsGetManyOutput = inferRouterOutputs<AppRouter>["products"]["getMany"];
@@ -0,0 +1,23 @@
'use client';
import { useSuspenseQuery } from '@tanstack/react-query';
import { useTRPC } from '@/trpc/client';
interface Props {
category: string;
}
export const ProductList = ({ category }: Props) => {
const trpc = useTRPC();
const { data } = useSuspenseQuery(trpc.products.getMany.queryOptions({
category
}));
return <div>{JSON.stringify(data)}</div>;
};
export const ProductListSkeleton = () => {
return <div>Loading...</div>;
};
+38
View File
@@ -70,6 +70,7 @@ export interface Config {
users: User;
media: Media;
categories: Category;
products: Product;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
@@ -83,6 +84,7 @@ export interface Config {
users: UsersSelect<false> | UsersSelect<true>;
media: MediaSelect<false> | MediaSelect<true>;
categories: CategoriesSelect<false> | CategoriesSelect<true>;
products: ProductsSelect<false> | ProductsSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
@@ -174,6 +176,24 @@ export interface Category {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "products".
*/
export interface Product {
id: string;
name: string;
description?: string | null;
/**
* Price in USD
*/
price: number;
category?: (string | null) | Category;
images?: (string | null) | Media;
refundPolicy?: ('30-day' | '14-day' | '7-day' | '3-day' | '1-day' | 'no-refunds') | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents".
@@ -192,6 +212,10 @@ export interface PayloadLockedDocument {
| ({
relationTo: 'categories';
value: string | Category;
} | null)
| ({
relationTo: 'products';
value: string | Product;
} | null);
globalSlug?: string | null;
user: {
@@ -282,6 +306,20 @@ export interface CategoriesSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "products_select".
*/
export interface ProductsSelect<T extends boolean = true> {
name?: T;
description?: T;
price?: T;
category?: T;
images?: T;
refundPolicy?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".
+2 -1
View File
@@ -10,6 +10,7 @@ import sharp from 'sharp'
import { Users } from './collections/Users';
import { Media } from './collections/Media'
import { Categories } from './collections/Categories'
import { Products } from './collections/Products'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
@@ -22,7 +23,7 @@ export default buildConfig({
},
},
cookiePrefix: 'funroad',
collections: [Users, Media, Categories],
collections: [Users, Media, Categories, Products],
editor: lexicalEditor(),
secret: process.env.PAYLOAD_SECRET || '',
typescript: {
+2 -1
View File
@@ -1,11 +1,12 @@
import { authRouter } from "@/modules/auth/server/procedures";
import { categoriesRouter } from '@/modules/categories/server/procedures';
import { productsRouter } from "@/modules/products/server/procedures";
import { createTRPCRouter } from '../init';
export const appRouter = createTRPCRouter({
auth: authRouter,
products: productsRouter,
categories: categoriesRouter,
});
// export type definition of API