diff --git a/bun.lock b/bun.lock index 81fbc89..31aebc6 100644 --- a/bun.lock +++ b/bun.lock @@ -35,7 +35,12 @@ "@radix-ui/react-toggle": "^1.1.6", "@radix-ui/react-toggle-group": "^1.1.7", "@radix-ui/react-tooltip": "^1.2.4", + "@tanstack/react-query": "5.72.1", + "@trpc/client": "11.0.3", + "@trpc/server": "11.0.3", + "@trpc/tanstack-react-query": "11.0.3", "class-variance-authority": "^0.7.1", + "client-only": "0.0.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", @@ -52,11 +57,13 @@ "react-hook-form": "^7.56.1", "react-resizable-panels": "^2.1.9", "recharts": "^2.15.3", + "server-only": "0.0.1", "sonner": "^2.0.3", + "superjson": "2.2.2", "tailwind-merge": "^3.2.0", "tw-animate-css": "1.2.5", "vaul": "^1.1.2", - "zod": "^3.24.3", + "zod": "3.24.2", }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -708,8 +715,18 @@ "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.4", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.4", "@tailwindcss/oxide": "4.1.4", "postcss": "^8.4.41", "tailwindcss": "4.1.4" } }, "sha512-bjV6sqycCEa+AQSt2Kr7wpGF1bOZJ5wsqnLEkqSbM/JEHxx/yhMH8wHmdkPyApF9xhHeMSwnnkDUUMMM/hYnXw=="], + "@tanstack/query-core": ["@tanstack/query-core@5.72.1", "", {}, "sha512-nOu0EEkZuJ0BZnYgeaEfo44+psq1jBO7/zp3KudixD4dvgOVerrhAhDEKsWx2N7MxB59mjO4r0ddP/VqWGPK+Q=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.72.1", "", { "dependencies": { "@tanstack/query-core": "5.72.1" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-4UEMyRx54xj144D2nDvDIMiXSG5BrqyCJrmyNoGbymNS+VWODcBDFrmRk9p2fe12UGZ4JtKPTNuW2Jg0aisUgQ=="], + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + "@trpc/client": ["@trpc/client@11.0.3", "", { "peerDependencies": { "@trpc/server": "11.0.3", "typescript": ">=5.7.2" } }, "sha512-nUhRo/Ml3LQzs2USs5pfquNqETEQOMS6o9IR30cwn32oj4NuXXSqe9UqUc3hIBALtVzcfg6mLK43hGQA9O3X/Q=="], + + "@trpc/server": ["@trpc/server@11.0.3", "", { "peerDependencies": { "typescript": ">=5.7.2" } }, "sha512-/qlaFITZ5a9OCI5mZiW94C8q9mr/wxCmb8GfP3k7VF9RuY4ql+M1UWrzS7xAj8bHekqG9PcR8qEWlOJ6NnXbQg=="], + + "@trpc/tanstack-react-query": ["@trpc/tanstack-react-query@11.0.3", "", { "peerDependencies": { "@tanstack/react-query": "^5.67.1", "@trpc/client": "11.0.3", "@trpc/server": "11.0.3", "react": ">=18.2.0", "react-dom": ">=18.2.0", "typescript": ">=5.7.2" } }, "sha512-4U3oagLmWBQ1nxbfyvQInImWHoIGx8AQcSaB+wrynFN8u+VAk2AvYNXpSolsdbmkTH1IJlofp+NlsvU4kPW7wA=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="], "@types/acorn": ["@types/acorn@4.0.6", "", { "dependencies": { "@types/estree": "*" } }, "sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ=="], @@ -946,6 +963,8 @@ "convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], + "copy-anything": ["copy-anything@3.0.5", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w=="], + "cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="], "croner": ["croner@9.0.0", "", {}, "sha512-onMB0OkDjkXunhdW9htFjEhqrD54+M94i6ackoUkjHKbRnXdyEyKRelp4nJ1kAz32+s27jP1FsebpJCVl0BsvA=="], @@ -1286,6 +1305,8 @@ "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], + "is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="], + "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], @@ -1656,6 +1677,8 @@ "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "server-only": ["server-only@0.0.1", "", {}, "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA=="], + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], @@ -1732,6 +1755,8 @@ "stylis": ["stylis@4.2.0", "", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="], + "superjson": ["superjson@2.2.2", "", { "dependencies": { "copy-anything": "^3.0.2" } }, "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], @@ -1854,7 +1879,7 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "zod": ["zod@3.24.3", "", {}, "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg=="], + "zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], diff --git a/package.json b/package.json index c42a2b0..0b3c1fc 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,12 @@ "@radix-ui/react-toggle": "^1.1.6", "@radix-ui/react-toggle-group": "^1.1.7", "@radix-ui/react-tooltip": "^1.2.4", + "@tanstack/react-query": "5.72.1", + "@trpc/client": "11.0.3", + "@trpc/server": "11.0.3", + "@trpc/tanstack-react-query": "11.0.3", "class-variance-authority": "^0.7.1", + "client-only": "0.0.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", @@ -60,11 +65,13 @@ "react-hook-form": "^7.56.1", "react-resizable-panels": "^2.1.9", "recharts": "^2.15.3", + "server-only": "0.0.1", "sonner": "^2.0.3", + "superjson": "2.2.2", "tailwind-merge": "^3.2.0", "tw-animate-css": "1.2.5", "vaul": "^1.1.2", - "zod": "^3.24.3" + "zod": "3.24.2" }, "devDependencies": { "typescript": "^5", diff --git a/src/app/(app)/(home)/layout.tsx b/src/app/(app)/(home)/layout.tsx index ce8a84a..4f7f1c6 100644 --- a/src/app/(app)/(home)/layout.tsx +++ b/src/app/(app)/(home)/layout.tsx @@ -1,46 +1,29 @@ -import { ReactNode } from 'react'; -import configPromise from '@payload-config'; -import { getPayload } from 'payload'; +import { ReactNode, Suspense } from 'react'; +import { dehydrate, HydrationBoundary } from '@tanstack/react-query'; + +import { getQueryClient, trpc } from '@/trpc/server'; import Navbar from './navbar'; import Footer from './footer'; -import SearchFilters from './search-filters'; -import { Category } from '@/payload-types'; -import { CustomCategory } from './type'; +import SearchFilters, { SearchFiltersSkeleton } from './search-filters'; interface LayoutProps { children: ReactNode; } const Layout = async ({ children }: LayoutProps) => { - const payload = await getPayload({ - config: configPromise, - }); - - const data = await payload.find({ - collection: 'categories', - depth: 1, - where: { - parent: { - exists: false, - }, - }, - sort: 'name', - }); - - const formattedData: CustomCategory[] = data.docs.map((category) => ({ - ...category, - subcategories: (category.subcategories?.docs ?? []).map((doc) => ({ - ...(doc as Category), - subcategories: undefined, - })), - })); + const queryClient = getQueryClient(); + void queryClient.prefetchQuery(trpc.categories.getMany.queryOptions()); return (
- + + }> + + +
{children}
diff --git a/src/app/(app)/(home)/search-filters/categories-sidebar.tsx b/src/app/(app)/(home)/search-filters/categories-sidebar.tsx index b914038..408d013 100644 --- a/src/app/(app)/(home)/search-filters/categories-sidebar.tsx +++ b/src/app/(app)/(home)/search-filters/categories-sidebar.tsx @@ -1,32 +1,36 @@ +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'; + import { Sheet, SheetContent, SheetHeader, SheetTitle, } from '@/components/ui/sheet'; -import { CustomCategory } from '../type'; -import { useState } from 'react'; import { ScrollArea } from '@/components/ui/scroll-area'; -import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'; -import { useRouter } from 'next/navigation'; +import { useTRPC } from '@/trpc/client'; +import { CategoriesGetManyOutput } from '@/modules/categories/type'; interface CategoriesSidebarProps { open: boolean; onOpenChange: (open: boolean) => void; - data: CustomCategory[]; } const CategoriesSidebar = ({ open, onOpenChange, - data, }: CategoriesSidebarProps) => { + const trpc = useTRPC(); + const { data } = useSuspenseQuery(trpc.categories.getMany.queryOptions()); + const router = useRouter(); const [parentCategories, setParentCategories] = useState< - CustomCategory[] | null + CategoriesGetManyOutput | null >(null); const [selectedCategory, setSelectedCategory] = - useState(null); + useState(null); // If we have parent categories, show those, otherwise show root categories const currentCategories = parentCategories ?? data ?? []; @@ -37,9 +41,9 @@ const CategoriesSidebar = ({ onOpenChange(open); }; - const handleCategoryClick = (category: CustomCategory) => { + const handleCategoryClick = (category: CategoriesGetManyOutput[1]) => { if (category.subcategories && category.subcategories.length > 0) { - setParentCategories(category.subcategories as CustomCategory[]); + setParentCategories(category.subcategories as CategoriesGetManyOutput); setSelectedCategory(category); } else { // This is a leaf category (no subcategories) diff --git a/src/app/(app)/(home)/search-filters/categories.tsx b/src/app/(app)/(home)/search-filters/categories.tsx index 10d5a9d..a2277a7 100644 --- a/src/app/(app)/(home)/search-filters/categories.tsx +++ b/src/app/(app)/(home)/search-filters/categories.tsx @@ -7,11 +7,11 @@ import { cn } from '@/lib/utils'; import CategoryDropdown from './category-dropdown'; -import { CustomCategory } from '../type'; import CategoriesSidebar from './categories-sidebar'; +import { CategoriesGetManyOutput } from '@/modules/categories/type'; interface CategoriesProps { - data: CustomCategory[]; + data: CategoriesGetManyOutput; } const Categories = ({ data }: CategoriesProps) => { @@ -69,7 +69,6 @@ const Categories = ({ data }: CategoriesProps) => { {/* Hidden div to measure all items */} diff --git a/src/app/(app)/(home)/search-filters/category-dropdown.tsx b/src/app/(app)/(home)/search-filters/category-dropdown.tsx index ffe4819..edbf25b 100644 --- a/src/app/(app)/(home)/search-filters/category-dropdown.tsx +++ b/src/app/(app)/(home)/search-filters/category-dropdown.tsx @@ -7,11 +7,11 @@ import { cn } from '@/lib/utils'; import useDropdownPosition from './use-dropdown-position'; import SubCategoryMenu from './subcategory-menu'; -import { CustomCategory } from '../type'; import Link from 'next/link'; +import { CategoriesGetManyOutput } from '@/modules/categories/type'; interface CategoryDropdownProps { - category: CustomCategory; + category: CategoriesGetManyOutput[1]; isActive?: boolean; isNavigationHovered?: boolean; } diff --git a/src/app/(app)/(home)/search-filters/index.tsx b/src/app/(app)/(home)/search-filters/index.tsx index 3b36717..7a52ba1 100644 --- a/src/app/(app)/(home)/search-filters/index.tsx +++ b/src/app/(app)/(home)/search-filters/index.tsx @@ -1,15 +1,19 @@ -import { CustomCategory } from '../type'; +'use client'; + +import { useSuspenseQuery } from '@tanstack/react-query'; + +import { useTRPC } from '@/trpc/client'; + import Categories from './categories'; import SearchInput from './search-input'; -interface SearchFiltersProps { - data: CustomCategory[]; -} +const SearchFilters = () => { + const trpc = useTRPC(); + const { data } = useSuspenseQuery(trpc.categories.getMany.queryOptions()); -const SearchFilters = ({ data }: SearchFiltersProps) => { return ( -
- +
+
@@ -18,4 +22,16 @@ const SearchFilters = ({ data }: SearchFiltersProps) => { ); }; +export const SearchFiltersSkeleton = () => { + return ( +
+ + +
+
+
+
+ ); +}; + export default SearchFilters; diff --git a/src/app/(app)/(home)/search-filters/search-input.tsx b/src/app/(app)/(home)/search-filters/search-input.tsx index 3653818..1e36188 100644 --- a/src/app/(app)/(home)/search-filters/search-input.tsx +++ b/src/app/(app)/(home)/search-filters/search-input.tsx @@ -8,21 +8,17 @@ import { Button } from '@/components/ui/button'; import CategoriesSidebar from './categories-sidebar'; -import { CustomCategory } from '../type'; - interface SearchInputProps { disabled?: boolean; - data: CustomCategory[]; } -const SearchInput = ({ disabled, data }: SearchInputProps) => { +const SearchInput = ({ disabled }: SearchInputProps) => { const [isSidebarOpen, setIsSidebarOpen] = useState(false); return (
diff --git a/src/app/(app)/(home)/search-filters/subcategory-menu.tsx b/src/app/(app)/(home)/search-filters/subcategory-menu.tsx index db19ddf..4c9cc76 100644 --- a/src/app/(app)/(home)/search-filters/subcategory-menu.tsx +++ b/src/app/(app)/(home)/search-filters/subcategory-menu.tsx @@ -1,10 +1,10 @@ -import { Category } from '@/payload-types'; import Link from 'next/link'; -import { CustomCategory } from '../type'; +import { Category } from '@/payload-types'; +import { CategoriesGetManyOutput } from '@/modules/categories/type'; interface SubCategoryMenuProps { - category: CustomCategory; + category: CategoriesGetManyOutput[1]; isOpen: boolean; position: { top: number; diff --git a/src/app/(app)/(home)/type.ts b/src/app/(app)/(home)/type.ts deleted file mode 100644 index b53d7f5..0000000 --- a/src/app/(app)/(home)/type.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Category } from "@/payload-types"; - -export type CustomCategory = Category & { - subcategories: Category[]; -} \ No newline at end of file diff --git a/src/app/(app)/api/trpc/[trpc]/route.ts b/src/app/(app)/api/trpc/[trpc]/route.ts new file mode 100644 index 0000000..3146bf1 --- /dev/null +++ b/src/app/(app)/api/trpc/[trpc]/route.ts @@ -0,0 +1,11 @@ +import { createTRPCContext } from '@/trpc/init'; +import { appRouter } from '@/trpc/routers/_app'; +import { fetchRequestHandler } from '@trpc/server/adapters/fetch'; +const handler = (req: Request) => + fetchRequestHandler({ + endpoint: '/api/trpc', + req, + router: appRouter, + createContext: createTRPCContext, + }); +export { handler as GET, handler as POST }; \ No newline at end of file diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx index c5bd35e..c9e2b49 100644 --- a/src/app/(app)/layout.tsx +++ b/src/app/(app)/layout.tsx @@ -1,28 +1,29 @@ -import type { Metadata } from "next"; -import { DM_Sans } from "next/font/google"; -import "./globals.css"; +import type { Metadata } from 'next'; +import { DM_Sans } from 'next/font/google'; + +import { TRPCReactProvider } from '@/trpc/client'; + +import './globals.css'; const dmSans = DM_Sans({ - subsets: ["latin"], + subsets: ['latin'], }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: 'Create Next App', + description: 'Generated by create next app', }; export default function RootLayout({ - children, + children, }: Readonly<{ - children: React.ReactNode; + children: React.ReactNode; }>) { - return ( - - - {children} - - - ); + return ( + + + {children} + + + ); } diff --git a/src/modules/categories/server/procedures.ts b/src/modules/categories/server/procedures.ts new file mode 100644 index 0000000..f378f2a --- /dev/null +++ b/src/modules/categories/server/procedures.ts @@ -0,0 +1,28 @@ +import { baseProcedure, createTRPCRouter } from "@/trpc/init"; +import { Category } from '@/payload-types'; + +export const categoriesRouter = createTRPCRouter({ + getMany: baseProcedure.query(async ({ ctx }) => { + + const data = await ctx.db.find({ + collection: 'categories', + depth: 1, + where: { + parent: { + exists: false, + }, + }, + sort: 'name', + }); + + const formattedData = data.docs.map((category) => ({ + ...category, + subcategories: (category.subcategories?.docs ?? []).map((doc) => ({ + ...(doc as Category), + subcategories: undefined, + })), + })); + + return formattedData; + }) +}) \ No newline at end of file diff --git a/src/modules/categories/type.ts b/src/modules/categories/type.ts new file mode 100644 index 0000000..5ee1c51 --- /dev/null +++ b/src/modules/categories/type.ts @@ -0,0 +1,5 @@ +import { inferRouterOutputs } from "@trpc/server"; + +import { AppRouter } from "@/trpc/routers/_app"; + +export type CategoriesGetManyOutput = inferRouterOutputs["categories"]["getMany"]; \ No newline at end of file diff --git a/src/trpc/client.tsx b/src/trpc/client.tsx new file mode 100644 index 0000000..9c78d34 --- /dev/null +++ b/src/trpc/client.tsx @@ -0,0 +1,61 @@ +'use client'; +// ^-- to make sure we can mount the Provider from a server component +import { useState } from 'react'; +import type { QueryClient } from '@tanstack/react-query'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { createTRPCClient, httpBatchLink } from '@trpc/client'; +import { createTRPCContext } from '@trpc/tanstack-react-query'; +import superjson from 'superjson'; + +import { makeQueryClient } from './query-client'; +import type { AppRouter } from './routers/_app'; +export const { TRPCProvider, useTRPC } = createTRPCContext(); +let browserQueryClient: QueryClient; +function getQueryClient() { + if (typeof window === 'undefined') { + // Server: always make a new query client + return makeQueryClient(); + } + // Browser: make a new query client if we don't already have one + // This is very important, so we don't re-make a new client if React + // suspends during the initial render. This may not be needed if we + // have a suspense boundary BELOW the creation of the query client + if (!browserQueryClient) browserQueryClient = makeQueryClient(); + return browserQueryClient; +} +function getUrl() { + const base = (() => { + if (typeof window !== 'undefined') return ''; + + return process.env.NEXT_PUBLIC_APP_URL; + })(); + return `${base}/api/trpc`; +} +export function TRPCReactProvider( + props: Readonly<{ + children: React.ReactNode; + }> +) { + // NOTE: Avoid useState when initializing the query client if you don't + // have a suspense boundary between this and the code that may + // suspend because React will throw away the client on the initial + // render if it suspends and there is no boundary + const queryClient = getQueryClient(); + const [trpcClient] = useState(() => + createTRPCClient({ + links: [ + httpBatchLink({ + transformer: superjson, + url: getUrl(), + }), + ], + }) + ); + return ( + + + {props.children} + + + ); +} diff --git a/src/trpc/init.ts b/src/trpc/init.ts new file mode 100644 index 0000000..de46bca --- /dev/null +++ b/src/trpc/init.ts @@ -0,0 +1,30 @@ +import { cache } from 'react'; +import { initTRPC } from '@trpc/server'; +import { getPayload } from 'payload'; +import config from '@payload-config'; +import superjson from 'superjson'; + +export const createTRPCContext = cache(async () => { + /** + * @see: https://trpc.io/docs/server/context + */ + return { userId: 'user_123' }; +}); +// Avoid exporting the entire t-object +// since it's not very descriptive. +// For instance, the use of a t variable +// is common in i18n libraries. +const t = initTRPC.create({ + /** + * @see https://trpc.io/docs/server/data-transformers + */ + transformer: superjson, +}); +// Base router and procedure helpers +export const createTRPCRouter = t.router; +export const createCallerFactory = t.createCallerFactory; +export const baseProcedure = t.procedure.use(async ({ next }) => { + const payload = await getPayload({ config }); + + return next({ ctx: { db: payload } }); +}); \ No newline at end of file diff --git a/src/trpc/query-client.ts b/src/trpc/query-client.ts new file mode 100644 index 0000000..e6448bd --- /dev/null +++ b/src/trpc/query-client.ts @@ -0,0 +1,24 @@ +import { + defaultShouldDehydrateQuery, + QueryClient, +} from '@tanstack/react-query'; +import superjson from 'superjson'; + +export function makeQueryClient() { + return new QueryClient({ + defaultOptions: { + queries: { + staleTime: 30 * 1000, + }, + dehydrate: { + serializeData: superjson.serialize, + shouldDehydrateQuery: (query) => + defaultShouldDehydrateQuery(query) || + query.state.status === 'pending', + }, + hydrate: { + deserializeData: superjson.deserialize, + }, + }, + }); +} \ No newline at end of file diff --git a/src/trpc/routers/_app.ts b/src/trpc/routers/_app.ts new file mode 100644 index 0000000..2e0ed93 --- /dev/null +++ b/src/trpc/routers/_app.ts @@ -0,0 +1,8 @@ +import { categoriesRouter } from '@/modules/categories/server/procedures'; +import { createTRPCRouter } from '../init'; + +export const appRouter = createTRPCRouter({ + categories: categoriesRouter, +}); +// export type definition of API +export type AppRouter = typeof appRouter; \ No newline at end of file diff --git a/src/trpc/server.tsx b/src/trpc/server.tsx new file mode 100644 index 0000000..33d124e --- /dev/null +++ b/src/trpc/server.tsx @@ -0,0 +1,14 @@ +import 'server-only'; // <-- ensure this file cannot be imported from the client +import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query'; +import { cache } from 'react'; +import { createTRPCContext } from './init'; +import { makeQueryClient } from './query-client'; +import { appRouter } from './routers/_app'; +// IMPORTANT: Create a stable getter for the query client that +// will return the same client during the same request. +export const getQueryClient = cache(makeQueryClient); +export const trpc = createTRPCOptionsProxy({ + ctx: createTRPCContext, + router: appRouter, + queryClient: getQueryClient, +}); \ No newline at end of file