mirror of
https://github.com/Nezumi-2711/multitenant-ecommerce.git
synced 2026-09-22 05:31:56 +00:00
feat: add product collections
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -2,6 +2,9 @@ import type { CollectionConfig } from "payload";
|
||||
|
||||
export const Categories: CollectionConfig = {
|
||||
slug: "categories",
|
||||
admin: {
|
||||
useAsTitle: 'name',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
})
|
||||
})
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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".
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user