mirror of
https://github.com/Nezumi-2711/multitenant-ecommerce.git
synced 2026-09-22 20:01:36 +00:00
feat: implement filter feature
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import type { SearchParams } from 'nuqs/server';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
|
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
|
||||||
|
|
||||||
@@ -7,25 +8,35 @@ import {
|
|||||||
ProductListSkeleton,
|
ProductListSkeleton,
|
||||||
} from '@/modules/products/ui/components/product-list';
|
} from '@/modules/products/ui/components/product-list';
|
||||||
import { ProductFilters } from '@/modules/products/ui/components/product-filters';
|
import { ProductFilters } from '@/modules/products/ui/components/product-filters';
|
||||||
|
import { loadProductFilters } from '@/modules/products/search-params';
|
||||||
|
import { ProductSort } from '@/modules/products/ui/components/product-sort';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
category: string;
|
category: string;
|
||||||
}>;
|
}>,
|
||||||
|
searchParams: Promise<SearchParams>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Page = async ({ params }: Props) => {
|
const Page = async ({ params, searchParams }: Props) => {
|
||||||
const { category } = await params;
|
const { category } = await params;
|
||||||
const queryClient = getQueryClient();
|
const queryClient = getQueryClient();
|
||||||
|
const filters = await loadProductFilters(searchParams);
|
||||||
|
|
||||||
void queryClient.prefetchQuery(
|
void queryClient.prefetchQuery(
|
||||||
trpc.products.getMany.queryOptions({
|
trpc.products.getMany.queryOptions({
|
||||||
category,
|
category,
|
||||||
|
...filters,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HydrationBoundary state={dehydrate(queryClient)}>
|
<HydrationBoundary state={dehydrate(queryClient)}>
|
||||||
<div className="px-4 lg:px-12 py-8 flex flex-col gap-4">
|
<div className="px-4 lg:px-12 py-8 flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col lg:flex-row lg:items-center gap-y-2 lg:gap-y-0 justify-between">
|
||||||
|
<p className='text-2xl font-medium'>Curated for you</p>
|
||||||
|
<ProductSort />
|
||||||
|
</div>
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-6 xl:grid-cols-8 gap-y-6 gap-x-12">
|
<div className="grid grid-cols-1 lg:grid-cols-6 xl:grid-cols-8 gap-y-6 gap-x-12">
|
||||||
<div className="lg:col-span-2 xl:col-span-2">
|
<div className="lg:col-span-2 xl:col-span-2">
|
||||||
<ProductFilters />
|
<ProductFilters />
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ export const Products: CollectionConfig = {
|
|||||||
relationTo: 'categories',
|
relationTo: 'categories',
|
||||||
hasMany: false,
|
hasMany: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'tags',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: 'tags',
|
||||||
|
hasMany: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'images',
|
name: 'images',
|
||||||
type: 'upload',
|
type: 'upload',
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { CollectionConfig } from "payload";
|
||||||
|
|
||||||
|
export const Tags: CollectionConfig = {
|
||||||
|
slug: "tags",
|
||||||
|
access: {
|
||||||
|
read: () => true,
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'name',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'product',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: 'products',
|
||||||
|
hasMany: true,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const DEFAULT_LIMIT = 8;
|
||||||
@@ -1,12 +1,20 @@
|
|||||||
import { parseAsString, useQueryStates } from 'nuqs';
|
import { useQueryStates, parseAsString, parseAsArrayOf, parseAsStringLiteral } from 'nuqs';
|
||||||
|
|
||||||
|
const sortValues = ['curated', 'trending', 'hot_and_new'];
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
sort: parseAsStringLiteral(sortValues).withDefault('curated'),
|
||||||
|
minPrice: parseAsString.withOptions({
|
||||||
|
clearOnDefault: true
|
||||||
|
}).withDefault(''),
|
||||||
|
maxPrice: parseAsString.withOptions({
|
||||||
|
clearOnDefault: true,
|
||||||
|
}).withDefault(''),
|
||||||
|
tags: parseAsArrayOf(parseAsString).withOptions({
|
||||||
|
clearOnDefault: true,
|
||||||
|
}).withDefault([])
|
||||||
|
}
|
||||||
|
|
||||||
export const useProductFilters = () => {
|
export const useProductFilters = () => {
|
||||||
return useQueryStates({
|
return useQueryStates(params);
|
||||||
minPrice: parseAsString.withOptions({
|
|
||||||
clearOnDefault: true,
|
|
||||||
}),
|
|
||||||
maxPrice: parseAsString.withOptions({
|
|
||||||
clearOnDefault: true,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { createLoader, parseAsString, parseAsArrayOf, parseAsStringLiteral } from 'nuqs/server';
|
||||||
|
|
||||||
|
export const sortValues = ['curated', 'trending', 'hot_and_new'] as const;
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
sort: parseAsStringLiteral(sortValues).withDefault('curated'),
|
||||||
|
minPrice: parseAsString.withOptions({
|
||||||
|
clearOnDefault: true
|
||||||
|
}).withDefault(''),
|
||||||
|
maxPrice: parseAsString.withOptions({
|
||||||
|
clearOnDefault: true,
|
||||||
|
}).withDefault(''),
|
||||||
|
tags: parseAsArrayOf(parseAsString).withOptions({
|
||||||
|
clearOnDefault: true,
|
||||||
|
}).withDefault([])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadProductFilters = createLoader(params);
|
||||||
@@ -1,17 +1,31 @@
|
|||||||
import type { Where } from "payload";
|
import type { Sort, Where } from "payload";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { baseProcedure, createTRPCRouter } from "@/trpc/init";
|
import { baseProcedure, createTRPCRouter } from "@/trpc/init";
|
||||||
import { Category } from "@/payload-types";
|
import { Category } from "@/payload-types";
|
||||||
|
import { sortValues } from "../search-params";
|
||||||
|
|
||||||
export const productsRouter = createTRPCRouter({
|
export const productsRouter = createTRPCRouter({
|
||||||
getMany: baseProcedure.input(z.object({
|
getMany: baseProcedure.input(z.object({
|
||||||
category: z.string().nullable().optional(),
|
category: z.string().nullable().optional(),
|
||||||
minPrice: z.string().nullable().optional(),
|
minPrice: z.string().nullable().optional(),
|
||||||
maxPrice: z.string().nullable().optional(),
|
maxPrice: z.string().nullable().optional(),
|
||||||
|
tags: z.array(z.string()).nullable().optional(),
|
||||||
|
sort: z.enum(sortValues).nullable().optional(),
|
||||||
})).query(async ({ ctx, input }) => {
|
})).query(async ({ ctx, input }) => {
|
||||||
const where: Where = {
|
const where: Where = {};
|
||||||
price: {},
|
let sort: Sort = "-createdAt";
|
||||||
|
|
||||||
|
if (input.sort === 'curated') {
|
||||||
|
sort = '-createdAt';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.sort === 'hot_and_new') {
|
||||||
|
sort = '+createdAt';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.sort === 'trending') {
|
||||||
|
sort = '-createdAt';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.minPrice && input.maxPrice) {
|
if (input.minPrice && input.maxPrice) {
|
||||||
@@ -63,13 +77,19 @@ export const productsRouter = createTRPCRouter({
|
|||||||
in: [parentCategory.slug, ...subCategoriesSlugs],
|
in: [parentCategory.slug, ...subCategoriesSlugs],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.tags && input.tags.length > 0) {
|
||||||
|
where['tags.name'] = {
|
||||||
|
in: input.tags
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await ctx.db.find({
|
const data = await ctx.db.find({
|
||||||
collection: 'products',
|
collection: 'products',
|
||||||
depth: 1, // Populated "category" & "image"
|
depth: 1, // Populated "category" & "image"
|
||||||
where,
|
where,
|
||||||
|
sort,
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { ReactNode, useState } from 'react';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
import { PriceFilter } from './price-filter';
|
import { PriceFilter } from './price-filter';
|
||||||
|
import { TagsFilter } from './tags-filter';
|
||||||
|
|
||||||
import { useProductFilters } from '../../hooks/use-product-filters';
|
import { useProductFilters } from '../../hooks/use-product-filters';
|
||||||
|
|
||||||
interface ProductFiltersProps {
|
interface ProductFiltersProps {
|
||||||
@@ -16,7 +18,6 @@ interface ProductFiltersProps {
|
|||||||
|
|
||||||
const ProductFilter = ({ title, className, children }: ProductFiltersProps) => {
|
const ProductFilter = ({ title, className, children }: ProductFiltersProps) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const Icon = isOpen ? ChevronDownIcon : ChevronRightIcon;
|
const Icon = isOpen ? ChevronDownIcon : ChevronRightIcon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -40,17 +41,41 @@ export const ProductFilters = () => {
|
|||||||
setFilters({ ...filters, [key]: value });
|
setFilters({ ...filters, [key]: value });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasAnyFilters = Object.entries(filters).some(([key, value]) => {
|
||||||
|
if(key === 'sort') return false;
|
||||||
|
|
||||||
|
if(Array.isArray(value)) {
|
||||||
|
return value.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.trim() !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return value !== null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClearFilters = () => {
|
||||||
|
setFilters({ minPrice: '', maxPrice: '', tags: [] });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border rounded-md bg-white">
|
<div className="border rounded-md bg-white">
|
||||||
<div className="p-4 border-b flex items-center justify-between">
|
<div className="p-4 border-b flex items-center justify-between">
|
||||||
<p className="font-medium">Filters</p>
|
<p className="font-medium">Filters</p>
|
||||||
|
|
||||||
<button className="underline" onClick={() => {}} type="button">
|
{hasAnyFilters && (
|
||||||
Clear
|
<button
|
||||||
</button>
|
className="underline cursor-pointer"
|
||||||
|
onClick={handleClearFilters}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ProductFilter title="Price" className="border-b-0">
|
<ProductFilter title="Price">
|
||||||
<PriceFilter
|
<PriceFilter
|
||||||
minPrice={filters.minPrice}
|
minPrice={filters.minPrice}
|
||||||
maxPrice={filters.maxPrice}
|
maxPrice={filters.maxPrice}
|
||||||
@@ -58,6 +83,12 @@ export const ProductFilters = () => {
|
|||||||
onMaxPriceChange={(value) => onChange('maxPrice', value)}
|
onMaxPriceChange={(value) => onChange('maxPrice', value)}
|
||||||
/>
|
/>
|
||||||
</ProductFilter>
|
</ProductFilter>
|
||||||
|
<ProductFilter title="Tags" className="border-b-0">
|
||||||
|
<TagsFilter
|
||||||
|
value={filters.tags}
|
||||||
|
onChange={(value) => onChange('tags', value)}
|
||||||
|
/>
|
||||||
|
</ProductFilter>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,16 +3,20 @@
|
|||||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { useTRPC } from '@/trpc/client';
|
import { useTRPC } from '@/trpc/client';
|
||||||
|
import { useProductFilters } from '../../hooks/use-product-filters';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
category: string;
|
category: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProductList = ({ category }: Props) => {
|
export const ProductList = ({ category }: Props) => {
|
||||||
|
const [filters] = useProductFilters();
|
||||||
|
|
||||||
const trpc = useTRPC();
|
const trpc = useTRPC();
|
||||||
const { data } = useSuspenseQuery(
|
const { data } = useSuspenseQuery(
|
||||||
trpc.products.getMany.queryOptions({
|
trpc.products.getMany.queryOptions({
|
||||||
category,
|
category,
|
||||||
|
...filters,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
|
import { useProductFilters } from '../../hooks/use-product-filters';
|
||||||
|
|
||||||
|
export const ProductSort = () => {
|
||||||
|
const [filters, setFilters] = useProductFilters();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className={cn(
|
||||||
|
'rounded-full bg-white hover:bg-white',
|
||||||
|
filters.sort !== 'curated' &&
|
||||||
|
'bg-transparent border-transparent hover:border-border hover:bg-transparent'
|
||||||
|
)}
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => setFilters({ sort: 'curated' })}
|
||||||
|
>
|
||||||
|
Curated
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className={cn(
|
||||||
|
'rounded-full bg-white hover:bg-white',
|
||||||
|
filters.sort !== 'trending' &&
|
||||||
|
'bg-transparent border-transparent hover:border-border hover:bg-transparent'
|
||||||
|
)}
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => setFilters({ sort: 'trending' })}
|
||||||
|
>
|
||||||
|
Trending
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className={cn(
|
||||||
|
'rounded-full bg-white hover:bg-white',
|
||||||
|
filters.sort !== 'hot_and_new' &&
|
||||||
|
'bg-transparent border-transparent hover:border-border hover:bg-transparent'
|
||||||
|
)}
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => setFilters({ sort: 'hot_and_new' })}
|
||||||
|
>
|
||||||
|
Hot & New
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { LoaderIcon } from 'lucide-react';
|
||||||
|
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { DEFAULT_LIMIT } from '@/constants';
|
||||||
|
import { useTRPC } from '@/trpc/client';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
|
||||||
|
interface TagsFilterProps {
|
||||||
|
value?: string[] | null;
|
||||||
|
onChange: (value: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TagsFilter = ({ value, onChange }: TagsFilterProps) => {
|
||||||
|
const trpc = useTRPC();
|
||||||
|
const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } =
|
||||||
|
useInfiniteQuery(
|
||||||
|
trpc.tags.getMany.infiniteQueryOptions(
|
||||||
|
{ limit: DEFAULT_LIMIT },
|
||||||
|
{
|
||||||
|
getNextPageParam: (lastPage) =>
|
||||||
|
lastPage.docs.length > 0 ? lastPage.nextPage : undefined,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClickTag = (tag: string) => {
|
||||||
|
if (value?.includes(tag)) {
|
||||||
|
onChange(value?.filter((t) => t !== tag) || []);
|
||||||
|
} else {
|
||||||
|
onChange([...(value || []), tag]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center p-4">
|
||||||
|
<LoaderIcon className="size-4 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
data?.pages.map((page) =>
|
||||||
|
page.docs.map((tag) => (
|
||||||
|
<div
|
||||||
|
key={tag.id}
|
||||||
|
className="flex items-center justify-between cursor-pointer"
|
||||||
|
onClick={() => handleClickTag(tag.name)}
|
||||||
|
>
|
||||||
|
<p className="font-medium">{tag.name}</p>
|
||||||
|
<Checkbox
|
||||||
|
checked={value?.includes(tag.name)}
|
||||||
|
onCheckedChange={() => handleClickTag(tag.name)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasNextPage && (
|
||||||
|
<button
|
||||||
|
disabled={isFetchingNextPage}
|
||||||
|
onClick={() => fetchNextPage()}
|
||||||
|
className="underline font-medium justify-start text-start disabled:opacity-50 cursor-pointer"
|
||||||
|
>
|
||||||
|
Load more...
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { DEFAULT_LIMIT } from "@/constants";
|
||||||
|
|
||||||
|
import { baseProcedure, createTRPCRouter } from "@/trpc/init";
|
||||||
|
|
||||||
|
export const tagsRouter = createTRPCRouter({
|
||||||
|
getMany: baseProcedure.input(z.object({
|
||||||
|
cursor: z.number().default(1),
|
||||||
|
limit: z.number().default(DEFAULT_LIMIT),
|
||||||
|
})).query(async ({ ctx, input }) => {
|
||||||
|
const data = await ctx.db.find({
|
||||||
|
collection: 'tags',
|
||||||
|
page: input.cursor,
|
||||||
|
limit: input.limit,
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -71,6 +71,7 @@ export interface Config {
|
|||||||
media: Media;
|
media: Media;
|
||||||
categories: Category;
|
categories: Category;
|
||||||
products: Product;
|
products: Product;
|
||||||
|
tags: Tag;
|
||||||
'payload-locked-documents': PayloadLockedDocument;
|
'payload-locked-documents': PayloadLockedDocument;
|
||||||
'payload-preferences': PayloadPreference;
|
'payload-preferences': PayloadPreference;
|
||||||
'payload-migrations': PayloadMigration;
|
'payload-migrations': PayloadMigration;
|
||||||
@@ -85,6 +86,7 @@ export interface Config {
|
|||||||
media: MediaSelect<false> | MediaSelect<true>;
|
media: MediaSelect<false> | MediaSelect<true>;
|
||||||
categories: CategoriesSelect<false> | CategoriesSelect<true>;
|
categories: CategoriesSelect<false> | CategoriesSelect<true>;
|
||||||
products: ProductsSelect<false> | ProductsSelect<true>;
|
products: ProductsSelect<false> | ProductsSelect<true>;
|
||||||
|
tags: TagsSelect<false> | TagsSelect<true>;
|
||||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||||
@@ -189,11 +191,23 @@ export interface Product {
|
|||||||
*/
|
*/
|
||||||
price: number;
|
price: number;
|
||||||
category?: (string | null) | Category;
|
category?: (string | null) | Category;
|
||||||
|
tags?: (string | Tag)[] | null;
|
||||||
images?: (string | null) | Media;
|
images?: (string | null) | Media;
|
||||||
refundPolicy?: ('30-day' | '14-day' | '7-day' | '3-day' | '1-day' | 'no-refunds') | null;
|
refundPolicy?: ('30-day' | '14-day' | '7-day' | '3-day' | '1-day' | 'no-refunds') | null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "tags".
|
||||||
|
*/
|
||||||
|
export interface Tag {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
product?: (string | Product)[] | null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "payload-locked-documents".
|
* via the `definition` "payload-locked-documents".
|
||||||
@@ -216,6 +230,10 @@ export interface PayloadLockedDocument {
|
|||||||
| ({
|
| ({
|
||||||
relationTo: 'products';
|
relationTo: 'products';
|
||||||
value: string | Product;
|
value: string | Product;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'tags';
|
||||||
|
value: string | Tag;
|
||||||
} | null);
|
} | null);
|
||||||
globalSlug?: string | null;
|
globalSlug?: string | null;
|
||||||
user: {
|
user: {
|
||||||
@@ -315,11 +333,22 @@ export interface ProductsSelect<T extends boolean = true> {
|
|||||||
description?: T;
|
description?: T;
|
||||||
price?: T;
|
price?: T;
|
||||||
category?: T;
|
category?: T;
|
||||||
|
tags?: T;
|
||||||
images?: T;
|
images?: T;
|
||||||
refundPolicy?: T;
|
refundPolicy?: T;
|
||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "tags_select".
|
||||||
|
*/
|
||||||
|
export interface TagsSelect<T extends boolean = true> {
|
||||||
|
name?: T;
|
||||||
|
product?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "payload-locked-documents_select".
|
* via the `definition` "payload-locked-documents_select".
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Users } from './collections/Users';
|
|||||||
import { Media } from './collections/Media'
|
import { Media } from './collections/Media'
|
||||||
import { Categories } from './collections/Categories'
|
import { Categories } from './collections/Categories'
|
||||||
import { Products } from './collections/Products'
|
import { Products } from './collections/Products'
|
||||||
|
import { Tags } from './collections/Tags'
|
||||||
|
|
||||||
const filename = fileURLToPath(import.meta.url)
|
const filename = fileURLToPath(import.meta.url)
|
||||||
const dirname = path.dirname(filename)
|
const dirname = path.dirname(filename)
|
||||||
@@ -23,7 +24,7 @@ export default buildConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
cookiePrefix: 'funroad',
|
cookiePrefix: 'funroad',
|
||||||
collections: [Users, Media, Categories, Products],
|
collections: [Users, Media, Categories, Products, Tags],
|
||||||
editor: lexicalEditor(),
|
editor: lexicalEditor(),
|
||||||
secret: process.env.PAYLOAD_SECRET || '',
|
secret: process.env.PAYLOAD_SECRET || '',
|
||||||
typescript: {
|
typescript: {
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { authRouter } from "@/modules/auth/server/procedures";
|
import { authRouter } from "@/modules/auth/server/procedures";
|
||||||
import { categoriesRouter } from '@/modules/categories/server/procedures';
|
import { categoriesRouter } from '@/modules/categories/server/procedures';
|
||||||
import { productsRouter } from "@/modules/products/server/procedures";
|
import { productsRouter } from "@/modules/products/server/procedures";
|
||||||
|
import { tagsRouter } from "@/modules/tags/server/procedures";
|
||||||
|
|
||||||
import { createTRPCRouter } from '../init';
|
import { createTRPCRouter } from '../init';
|
||||||
|
|
||||||
export const appRouter = createTRPCRouter({
|
export const appRouter = createTRPCRouter({
|
||||||
auth: authRouter,
|
auth: authRouter,
|
||||||
|
tags: tagsRouter,
|
||||||
products: productsRouter,
|
products: productsRouter,
|
||||||
categories: categoriesRouter,
|
categories: categoriesRouter,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user