diff --git a/src/app/(app)/(home)/[category]/[subcategory]/page.tsx b/src/app/(app)/(home)/[category]/[subcategory]/page.tsx index d9b637f..6f29e43 100644 --- a/src/app/(app)/(home)/[category]/[subcategory]/page.tsx +++ b/src/app/(app)/(home)/[category]/[subcategory]/page.tsx @@ -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
Subcategory Page: {subcategory} in Category: {category}
; + return ( + + }> + + + + ); }; export default Page; diff --git a/src/app/(app)/(home)/[category]/page.tsx b/src/app/(app)/(home)/[category]/page.tsx index ce6694a..2c62289 100644 --- a/src/app/(app)/(home)/[category]/page.tsx +++ b/src/app/(app)/(home)/[category]/page.tsx @@ -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
Category Page: {category}
; + return ( + + }> + + + + ); }; export default Page; diff --git a/src/collections/Categories.ts b/src/collections/Categories.ts index 92a8839..5b4159b 100644 --- a/src/collections/Categories.ts +++ b/src/collections/Categories.ts @@ -2,6 +2,9 @@ import type { CollectionConfig } from "payload"; export const Categories: CollectionConfig = { slug: "categories", + admin: { + useAsTitle: 'name', + }, fields: [ { name: 'name', diff --git a/src/collections/Products.ts b/src/collections/Products.ts new file mode 100644 index 0000000..239a7a2 --- /dev/null +++ b/src/collections/Products.ts @@ -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', + } + ] +} \ No newline at end of file diff --git a/src/modules/categories/server/procedures.ts b/src/modules/categories/server/procedures.ts index f378f2a..9cf3944 100644 --- a/src/modules/categories/server/procedures.ts +++ b/src/modules/categories/server/procedures.ts @@ -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, diff --git a/src/modules/products/server/procedures.ts b/src/modules/products/server/procedures.ts new file mode 100644 index 0000000..9408c20 --- /dev/null +++ b/src/modules/products/server/procedures.ts @@ -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; + }) +}) \ No newline at end of file diff --git a/src/modules/products/type.ts b/src/modules/products/type.ts new file mode 100644 index 0000000..e79f6de --- /dev/null +++ b/src/modules/products/type.ts @@ -0,0 +1,5 @@ +import { inferRouterOutputs } from "@trpc/server"; + +import { AppRouter } from "@/trpc/routers/_app"; + +export type ProductsGetManyOutput = inferRouterOutputs["products"]["getMany"]; \ No newline at end of file diff --git a/src/modules/products/ui/components/product-list.tsx b/src/modules/products/ui/components/product-list.tsx new file mode 100644 index 0000000..5db4a9a --- /dev/null +++ b/src/modules/products/ui/components/product-list.tsx @@ -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
{JSON.stringify(data)}
; +}; + +export const ProductListSkeleton = () => { + return
Loading...
; +}; diff --git a/src/payload-types.ts b/src/payload-types.ts index 7885ecb..0784f2c 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -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 | UsersSelect; media: MediaSelect | MediaSelect; categories: CategoriesSelect | CategoriesSelect; + products: ProductsSelect | ProductsSelect; 'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect; 'payload-preferences': PayloadPreferencesSelect | PayloadPreferencesSelect; 'payload-migrations': PayloadMigrationsSelect | PayloadMigrationsSelect; @@ -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 { updatedAt?: T; createdAt?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "products_select". + */ +export interface ProductsSelect { + 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". diff --git a/src/payload.config.ts b/src/payload.config.ts index ccf3adf..26a05bc 100644 --- a/src/payload.config.ts +++ b/src/payload.config.ts @@ -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: { diff --git a/src/trpc/routers/_app.ts b/src/trpc/routers/_app.ts index f7ea56b..0432115 100644 --- a/src/trpc/routers/_app.ts +++ b/src/trpc/routers/_app.ts @@ -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