mirror of
https://github.com/Nezumi-2711/multitenant-ecommerce.git
synced 2026-09-22 13:38:34 +00:00
Merge pull request #13 from Nezumi-2711/feat/multi-tenancy
Featuer/ Implement multi tenancy
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
"@payloadcms/db-mongodb": "^3.36.0",
|
"@payloadcms/db-mongodb": "^3.36.0",
|
||||||
"@payloadcms/next": "^3.36.0",
|
"@payloadcms/next": "^3.36.0",
|
||||||
"@payloadcms/payload-cloud": "^3.36.0",
|
"@payloadcms/payload-cloud": "^3.36.0",
|
||||||
|
"@payloadcms/plugin-multi-tenant": "3.36.0",
|
||||||
"@payloadcms/richtext-lexical": "^3.36.0",
|
"@payloadcms/richtext-lexical": "^3.36.0",
|
||||||
"@radix-ui/react-accordion": "^1.2.8",
|
"@radix-ui/react-accordion": "^1.2.8",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.11",
|
"@radix-ui/react-alert-dialog": "^1.1.11",
|
||||||
@@ -462,6 +463,8 @@
|
|||||||
|
|
||||||
"@payloadcms/payload-cloud": ["@payloadcms/payload-cloud@3.36.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "^3.614.0", "@aws-sdk/client-s3": "^3.614.0", "@aws-sdk/credential-providers": "^3.614.0", "@aws-sdk/lib-storage": "^3.614.0", "@payloadcms/email-nodemailer": "3.36.0", "amazon-cognito-identity-js": "^6.1.2", "nodemailer": "6.9.16" }, "peerDependencies": { "payload": "3.36.0" } }, "sha512-3S8PVBosNdPxk5FnlphfeqCS9ECDkMK4KTaZgXdoNI184psDH9Mu+QKBmMX2sH4WXxNVqQ5r7BRLNtQQqDvHkw=="],
|
"@payloadcms/payload-cloud": ["@payloadcms/payload-cloud@3.36.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "^3.614.0", "@aws-sdk/client-s3": "^3.614.0", "@aws-sdk/credential-providers": "^3.614.0", "@aws-sdk/lib-storage": "^3.614.0", "@payloadcms/email-nodemailer": "3.36.0", "amazon-cognito-identity-js": "^6.1.2", "nodemailer": "6.9.16" }, "peerDependencies": { "payload": "3.36.0" } }, "sha512-3S8PVBosNdPxk5FnlphfeqCS9ECDkMK4KTaZgXdoNI184psDH9Mu+QKBmMX2sH4WXxNVqQ5r7BRLNtQQqDvHkw=="],
|
||||||
|
|
||||||
|
"@payloadcms/plugin-multi-tenant": ["@payloadcms/plugin-multi-tenant@3.36.0", "", { "peerDependencies": { "@payloadcms/ui": "3.36.0", "next": "^15.2.3", "payload": "3.36.0" } }, "sha512-geX5jHLG5GUF/NVhyySNg0vG9sWVfeEPSSFh1L+YL48YFxRusITKSGXIzBdgNYUcQntQFeyTrGNWhazH8H4zZQ=="],
|
||||||
|
|
||||||
"@payloadcms/richtext-lexical": ["@payloadcms/richtext-lexical@3.36.0", "", { "dependencies": { "@lexical/headless": "0.28.0", "@lexical/html": "0.28.0", "@lexical/link": "0.28.0", "@lexical/list": "0.28.0", "@lexical/mark": "0.28.0", "@lexical/react": "0.28.0", "@lexical/rich-text": "0.28.0", "@lexical/selection": "0.28.0", "@lexical/table": "0.28.0", "@lexical/utils": "0.28.0", "@payloadcms/translations": "3.36.0", "@payloadcms/ui": "3.36.0", "@types/uuid": "10.0.0", "acorn": "8.12.1", "bson-objectid": "2.0.4", "dequal": "2.0.3", "escape-html": "1.0.3", "jsox": "1.2.121", "lexical": "0.28.0", "mdast-util-from-markdown": "2.0.2", "mdast-util-mdx-jsx": "3.1.3", "micromark-extension-mdx-jsx": "3.0.1", "qs-esm": "7.0.2", "react-error-boundary": "4.1.2", "ts-essentials": "10.0.3", "uuid": "10.0.0" }, "peerDependencies": { "@faceless-ui/modal": "3.0.0-beta.2", "@faceless-ui/scroll-info": "2.0.0", "@payloadcms/next": "3.36.0", "payload": "3.36.0", "react": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020", "react-dom": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020" } }, "sha512-HiT4fE7vENAXeWt8jup8wEYxtvFzx4TjnPjyCssxwbX/96ROlLQheVAq35zPAEYQnFZZMCfmaobpW/lUYgewXQ=="],
|
"@payloadcms/richtext-lexical": ["@payloadcms/richtext-lexical@3.36.0", "", { "dependencies": { "@lexical/headless": "0.28.0", "@lexical/html": "0.28.0", "@lexical/link": "0.28.0", "@lexical/list": "0.28.0", "@lexical/mark": "0.28.0", "@lexical/react": "0.28.0", "@lexical/rich-text": "0.28.0", "@lexical/selection": "0.28.0", "@lexical/table": "0.28.0", "@lexical/utils": "0.28.0", "@payloadcms/translations": "3.36.0", "@payloadcms/ui": "3.36.0", "@types/uuid": "10.0.0", "acorn": "8.12.1", "bson-objectid": "2.0.4", "dequal": "2.0.3", "escape-html": "1.0.3", "jsox": "1.2.121", "lexical": "0.28.0", "mdast-util-from-markdown": "2.0.2", "mdast-util-mdx-jsx": "3.1.3", "micromark-extension-mdx-jsx": "3.0.1", "qs-esm": "7.0.2", "react-error-boundary": "4.1.2", "ts-essentials": "10.0.3", "uuid": "10.0.0" }, "peerDependencies": { "@faceless-ui/modal": "3.0.0-beta.2", "@faceless-ui/scroll-info": "2.0.0", "@payloadcms/next": "3.36.0", "payload": "3.36.0", "react": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020", "react-dom": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020" } }, "sha512-HiT4fE7vENAXeWt8jup8wEYxtvFzx4TjnPjyCssxwbX/96ROlLQheVAq35zPAEYQnFZZMCfmaobpW/lUYgewXQ=="],
|
||||||
|
|
||||||
"@payloadcms/translations": ["@payloadcms/translations@3.36.0", "", { "dependencies": { "date-fns": "4.1.0" } }, "sha512-+TNesB9N85OAm6Cwt2wnQcrwhMv9z8S7vQYSWMyB/nS3V9cWHpsKShCTh9bLoIN2MjGTZ5QLw2Wx6p5kuvNX0w=="],
|
"@payloadcms/translations": ["@payloadcms/translations@3.36.0", "", { "dependencies": { "date-fns": "4.1.0" } }, "sha512-+TNesB9N85OAm6Cwt2wnQcrwhMv9z8S7vQYSWMyB/nS3V9cWHpsKShCTh9bLoIN2MjGTZ5QLw2Wx6p5kuvNX0w=="],
|
||||||
|
|||||||
+3
-1
@@ -10,13 +10,15 @@
|
|||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"generate:types": "payload generate:types",
|
"generate:types": "payload generate:types",
|
||||||
"db:fresh": "payload migrate:fresh",
|
"db:fresh": "payload migrate:fresh",
|
||||||
"db:seed": "bun run src/seed.ts"
|
"db:seed": "bun run src/seed.ts",
|
||||||
|
"db:reset": "bun run db:fresh && bun run db:seed"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.0.1",
|
"@hookform/resolvers": "^5.0.1",
|
||||||
"@payloadcms/db-mongodb": "^3.36.0",
|
"@payloadcms/db-mongodb": "^3.36.0",
|
||||||
"@payloadcms/next": "^3.36.0",
|
"@payloadcms/next": "^3.36.0",
|
||||||
"@payloadcms/payload-cloud": "^3.36.0",
|
"@payloadcms/payload-cloud": "^3.36.0",
|
||||||
|
"@payloadcms/plugin-multi-tenant": "3.36.0",
|
||||||
"@payloadcms/richtext-lexical": "^3.36.0",
|
"@payloadcms/richtext-lexical": "^3.36.0",
|
||||||
"@radix-ui/react-accordion": "^1.2.8",
|
"@radix-ui/react-accordion": "^1.2.8",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.11",
|
"@radix-ui/react-alert-dialog": "^1.1.11",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
|
|||||||
import { getQueryClient, trpc } from '@/trpc/server';
|
import { getQueryClient, trpc } from '@/trpc/server';
|
||||||
import { loadProductFilters } from '@/modules/products/search-params';
|
import { loadProductFilters } from '@/modules/products/search-params';
|
||||||
import { ProductListView } from '@/modules/products/ui/views/product-list-view';
|
import { ProductListView } from '@/modules/products/ui/views/product-list-view';
|
||||||
|
import { DEFAULT_LIMIT } from '@/constants';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
@@ -17,10 +18,11 @@ const Page = async ({ params, searchParams }: Props) => {
|
|||||||
const queryClient = getQueryClient();
|
const queryClient = getQueryClient();
|
||||||
const filters = await loadProductFilters(searchParams);
|
const filters = await loadProductFilters(searchParams);
|
||||||
|
|
||||||
void queryClient.prefetchQuery(
|
void queryClient.prefetchInfiniteQuery(
|
||||||
trpc.products.getMany.queryOptions({
|
trpc.products.getMany.infiniteQueryOptions({
|
||||||
category: subcategory,
|
|
||||||
...filters,
|
...filters,
|
||||||
|
category: subcategory,
|
||||||
|
limit: DEFAULT_LIMIT,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
|
|||||||
import { getQueryClient, trpc } from '@/trpc/server';
|
import { getQueryClient, trpc } from '@/trpc/server';
|
||||||
import { loadProductFilters } from '@/modules/products/search-params';
|
import { loadProductFilters } from '@/modules/products/search-params';
|
||||||
import { ProductListView } from '@/modules/products/ui/views/product-list-view';
|
import { ProductListView } from '@/modules/products/ui/views/product-list-view';
|
||||||
|
import { DEFAULT_LIMIT } from '@/constants';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
@@ -17,10 +18,11 @@ const Page = async ({ params, searchParams }: Props) => {
|
|||||||
const queryClient = getQueryClient();
|
const queryClient = getQueryClient();
|
||||||
const filters = await loadProductFilters(searchParams);
|
const filters = await loadProductFilters(searchParams);
|
||||||
|
|
||||||
void queryClient.prefetchQuery(
|
void queryClient.prefetchInfiniteQuery(
|
||||||
trpc.products.getMany.queryOptions({
|
trpc.products.getMany.infiniteQueryOptions({
|
||||||
category,
|
|
||||||
...filters,
|
...filters,
|
||||||
|
category,
|
||||||
|
limit: DEFAULT_LIMIT,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,31 @@
|
|||||||
'use client';
|
import type { SearchParams } from 'nuqs/server';
|
||||||
|
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { getQueryClient, trpc } from '@/trpc/server';
|
||||||
|
import { loadProductFilters } from '@/modules/products/search-params';
|
||||||
|
import { ProductListView } from '@/modules/products/ui/views/product-list-view';
|
||||||
|
import { DEFAULT_LIMIT } from '@/constants';
|
||||||
|
|
||||||
import { useTRPC } from "@/trpc/client";
|
interface Props {
|
||||||
|
searchParams: Promise<SearchParams>;
|
||||||
export default function Home() {
|
|
||||||
const trpc = useTRPC();
|
|
||||||
const { data } = useQuery(trpc.auth.session.queryOptions());
|
|
||||||
|
|
||||||
return <div>{JSON.stringify(data?.user, null, 2)}</div>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Page = async ({ searchParams }: Props) => {
|
||||||
|
const filters = await loadProductFilters(searchParams);
|
||||||
|
const queryClient = getQueryClient();
|
||||||
|
|
||||||
|
void queryClient.prefetchInfiniteQuery(
|
||||||
|
trpc.products.getMany.infiniteQueryOptions({
|
||||||
|
...filters,
|
||||||
|
limit: DEFAULT_LIMIT,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HydrationBoundary state={dehydrate(queryClient)}>
|
||||||
|
<ProductListView />
|
||||||
|
</HydrationBoundary>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
import { TenantField as TenantField_1d0591e3cf4f332c83a86da13a0de59a } from '@payloadcms/plugin-multi-tenant/client'
|
||||||
|
import { TenantSelector as TenantSelector_1d0591e3cf4f332c83a86da13a0de59a } from '@payloadcms/plugin-multi-tenant/client'
|
||||||
|
import { TenantSelectionProvider as TenantSelectionProvider_d6d5f193a167989e2ee7d14202901e62 } from '@payloadcms/plugin-multi-tenant/rsc'
|
||||||
|
|
||||||
export const importMap = {
|
export const importMap = {
|
||||||
|
"@payloadcms/plugin-multi-tenant/client#TenantField": TenantField_1d0591e3cf4f332c83a86da13a0de59a,
|
||||||
|
"@payloadcms/plugin-multi-tenant/client#TenantSelector": TenantSelector_1d0591e3cf4f332c83a86da13a0de59a,
|
||||||
|
"@payloadcms/plugin-multi-tenant/rsc#TenantSelectionProvider": TenantSelectionProvider_d6d5f193a167989e2ee7d14202901e62
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { CollectionConfig } from "payload";
|
||||||
|
|
||||||
|
export const Tenants: CollectionConfig = {
|
||||||
|
slug: 'tenants',
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'slug',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
label: 'Store Name',
|
||||||
|
admin: {
|
||||||
|
description: "This is the name of the store (e.g. Nezumi's Store)",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'slug',
|
||||||
|
type: 'text',
|
||||||
|
index: true,
|
||||||
|
required: true,
|
||||||
|
unique: true,
|
||||||
|
admin: {
|
||||||
|
description: "This is the subdomain for the store (e.g. [slug].funroad.com)",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'image',
|
||||||
|
type: 'upload',
|
||||||
|
relationTo: 'media',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'stripeAccountId',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
readOnly: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'stripeDetailsSubmitted',
|
||||||
|
type: 'checkbox',
|
||||||
|
admin: {
|
||||||
|
readOnly: true,
|
||||||
|
description: 'You cannot create products until you submit your Stripe details',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,4 +1,21 @@
|
|||||||
import type { CollectionConfig } from 'payload'
|
import type { CollectionConfig } from 'payload';
|
||||||
|
import { tenantsArrayField } from '@payloadcms/plugin-multi-tenant/fields';
|
||||||
|
|
||||||
|
const defaultTenantArrayField = tenantsArrayField({
|
||||||
|
tenantsArrayFieldName: 'tenants',
|
||||||
|
tenantsCollectionSlug: 'tenants',
|
||||||
|
tenantsArrayTenantFieldName: 'tenant',
|
||||||
|
arrayFieldAccess: {
|
||||||
|
read: () => true,
|
||||||
|
create: () => true,
|
||||||
|
update: () => true,
|
||||||
|
},
|
||||||
|
tenantFieldAccess: {
|
||||||
|
read: () => true,
|
||||||
|
create: () => true,
|
||||||
|
update: () => true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export const Users: CollectionConfig = {
|
export const Users: CollectionConfig = {
|
||||||
slug: 'users',
|
slug: 'users',
|
||||||
@@ -12,6 +29,23 @@ export const Users: CollectionConfig = {
|
|||||||
required: true,
|
required: true,
|
||||||
unique: true,
|
unique: true,
|
||||||
type: 'text'
|
type: 'text'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
admin: {
|
||||||
|
position: 'sidebar',
|
||||||
|
},
|
||||||
|
name: 'roles',
|
||||||
|
type: 'select',
|
||||||
|
defaultValue: ['user'],
|
||||||
|
hasMany: true,
|
||||||
|
options: ['super-admin', 'user'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...defaultTenantArrayField,
|
||||||
|
admin: {
|
||||||
|
...(defaultTenantArrayField?.admin || {}),
|
||||||
|
position: 'sidebar',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,13 +34,27 @@ export const authRouter = createTRPCRouter({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tenant = await ctx.db.create({
|
||||||
|
collection: 'tenants',
|
||||||
|
data: {
|
||||||
|
name: input.username,
|
||||||
|
slug: input.username,
|
||||||
|
stripeAccountId: 'test',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
await ctx.db.create({
|
await ctx.db.create({
|
||||||
collection: 'users',
|
collection: 'users',
|
||||||
data: {
|
data: {
|
||||||
email: input.email,
|
email: input.email,
|
||||||
username: input.username,
|
username: input.username,
|
||||||
password: input.password,
|
password: input.password,
|
||||||
}
|
tenants: [
|
||||||
|
{
|
||||||
|
tenant: tenant.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const data = await ctx.db.login({
|
const data = await ctx.db.login({
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { cn } from "@/lib/utils";
|
|||||||
import { registerSchema, RegisterSchema } from "@/modules/auth/schemas";
|
import { registerSchema, RegisterSchema } from "@/modules/auth/schemas";
|
||||||
import { useTRPC } from "@/trpc/client";
|
import { useTRPC } from "@/trpc/client";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Poppins } from "next/font/google";
|
import { Poppins } from "next/font/google";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
@@ -22,11 +22,13 @@ const poppins = Poppins({
|
|||||||
export const SignUpView = () => {
|
export const SignUpView = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const trpc = useTRPC();
|
const trpc = useTRPC();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const register = useMutation(trpc.auth.register.mutationOptions({
|
const register = useMutation(trpc.auth.register.mutationOptions({
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(error.message)
|
toast.error(error.message)
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: async () => {
|
||||||
|
await queryClient.invalidateQueries(trpc.auth.session.queryFilter())
|
||||||
router.push('/');
|
router.push('/');
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils'
|
||||||
import useDropdownPosition from './use-dropdown-position';
|
import { CategoriesGetManyOutput } from '@/modules/categories/type';
|
||||||
|
;
|
||||||
import SubCategoryMenu from './subcategory-menu';
|
import SubCategoryMenu from './subcategory-menu';
|
||||||
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { CategoriesGetManyOutput } from '@/modules/categories/type';
|
|
||||||
|
|
||||||
interface CategoryDropdownProps {
|
interface CategoryDropdownProps {
|
||||||
category: CategoriesGetManyOutput[1];
|
category: CategoriesGetManyOutput[1];
|
||||||
@@ -23,8 +23,6 @@ const CategoryDropdown = ({
|
|||||||
}: CategoryDropdownProps) => {
|
}: CategoryDropdownProps) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const { getDropdownPosition } = useDropdownPosition(dropdownRef);
|
|
||||||
const dropdownPosition = getDropdownPosition();
|
|
||||||
|
|
||||||
// TODO: Will improve in mobile screen
|
// TODO: Will improve in mobile screen
|
||||||
// const toggleDropdown = () => {
|
// const toggleDropdown = () => {
|
||||||
@@ -77,7 +75,6 @@ const CategoryDropdown = ({
|
|||||||
<SubCategoryMenu
|
<SubCategoryMenu
|
||||||
category={category}
|
category={category}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
position={dropdownPosition}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,17 +6,9 @@ import { CategoriesGetManyOutput } from '@/modules/categories/type';
|
|||||||
interface SubCategoryMenuProps {
|
interface SubCategoryMenuProps {
|
||||||
category: CategoriesGetManyOutput[1];
|
category: CategoriesGetManyOutput[1];
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
position: {
|
|
||||||
top: number;
|
|
||||||
left: number;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const SubCategoryMenu = ({
|
const SubCategoryMenu = ({ category, isOpen }: SubCategoryMenuProps) => {
|
||||||
category,
|
|
||||||
isOpen,
|
|
||||||
position,
|
|
||||||
}: SubCategoryMenuProps) => {
|
|
||||||
if (
|
if (
|
||||||
!isOpen ||
|
!isOpen ||
|
||||||
!category.subcategories ||
|
!category.subcategories ||
|
||||||
@@ -29,10 +21,10 @@ const SubCategoryMenu = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="fixed z-100"
|
className="absolute z-100"
|
||||||
style={{
|
style={{
|
||||||
top: position?.top,
|
top: '100%',
|
||||||
left: position?.left,
|
left: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Invisible bridge to maintain hover */}
|
{/* Invisible bridge to maintain hover */}
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ export interface Config {
|
|||||||
categories: Category;
|
categories: Category;
|
||||||
products: Product;
|
products: Product;
|
||||||
tags: Tag;
|
tags: Tag;
|
||||||
|
tenants: Tenant;
|
||||||
'payload-locked-documents': PayloadLockedDocument;
|
'payload-locked-documents': PayloadLockedDocument;
|
||||||
'payload-preferences': PayloadPreference;
|
'payload-preferences': PayloadPreference;
|
||||||
'payload-migrations': PayloadMigration;
|
'payload-migrations': PayloadMigration;
|
||||||
@@ -87,6 +88,7 @@ export interface Config {
|
|||||||
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>;
|
tags: TagsSelect<false> | TagsSelect<true>;
|
||||||
|
tenants: TenantsSelect<false> | TenantsSelect<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>;
|
||||||
@@ -130,6 +132,13 @@ export interface UserAuthOperations {
|
|||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
|
roles?: ('super-admin' | 'user')[] | null;
|
||||||
|
tenants?:
|
||||||
|
| {
|
||||||
|
tenant: string | Tenant;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
email: string;
|
email: string;
|
||||||
@@ -141,6 +150,29 @@ export interface User {
|
|||||||
lockUntil?: string | null;
|
lockUntil?: string | null;
|
||||||
password?: string | null;
|
password?: string | null;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "tenants".
|
||||||
|
*/
|
||||||
|
export interface Tenant {
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* This is the name of the store (e.g. Nezumi's Store)
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* This is the subdomain for the store (e.g. [slug].funroad.com)
|
||||||
|
*/
|
||||||
|
slug: string;
|
||||||
|
image?: (string | null) | Media;
|
||||||
|
stripeAccountId: string;
|
||||||
|
/**
|
||||||
|
* You cannot create products until you submit your Stripe details
|
||||||
|
*/
|
||||||
|
stripeDetailsSubmitted?: boolean | 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` "media".
|
* via the `definition` "media".
|
||||||
@@ -184,6 +216,7 @@ export interface Category {
|
|||||||
*/
|
*/
|
||||||
export interface Product {
|
export interface Product {
|
||||||
id: string;
|
id: string;
|
||||||
|
tenant?: (string | null) | Tenant;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
/**
|
/**
|
||||||
@@ -234,6 +267,10 @@ export interface PayloadLockedDocument {
|
|||||||
| ({
|
| ({
|
||||||
relationTo: 'tags';
|
relationTo: 'tags';
|
||||||
value: string | Tag;
|
value: string | Tag;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'tenants';
|
||||||
|
value: string | Tenant;
|
||||||
} | null);
|
} | null);
|
||||||
globalSlug?: string | null;
|
globalSlug?: string | null;
|
||||||
user: {
|
user: {
|
||||||
@@ -283,6 +320,13 @@ export interface PayloadMigration {
|
|||||||
*/
|
*/
|
||||||
export interface UsersSelect<T extends boolean = true> {
|
export interface UsersSelect<T extends boolean = true> {
|
||||||
username?: T;
|
username?: T;
|
||||||
|
roles?: T;
|
||||||
|
tenants?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
tenant?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
email?: T;
|
email?: T;
|
||||||
@@ -329,6 +373,7 @@ export interface CategoriesSelect<T extends boolean = true> {
|
|||||||
* via the `definition` "products_select".
|
* via the `definition` "products_select".
|
||||||
*/
|
*/
|
||||||
export interface ProductsSelect<T extends boolean = true> {
|
export interface ProductsSelect<T extends boolean = true> {
|
||||||
|
tenant?: T;
|
||||||
name?: T;
|
name?: T;
|
||||||
description?: T;
|
description?: T;
|
||||||
price?: T;
|
price?: T;
|
||||||
@@ -349,6 +394,19 @@ export interface TagsSelect<T extends boolean = true> {
|
|||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "tenants_select".
|
||||||
|
*/
|
||||||
|
export interface TenantsSelect<T extends boolean = true> {
|
||||||
|
name?: T;
|
||||||
|
slug?: T;
|
||||||
|
image?: T;
|
||||||
|
stripeAccountId?: T;
|
||||||
|
stripeDetailsSubmitted?: 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".
|
||||||
|
|||||||
+12
-1
@@ -6,12 +6,14 @@ import path from 'path'
|
|||||||
import { buildConfig } from 'payload'
|
import { buildConfig } from 'payload'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
import sharp from 'sharp'
|
import sharp from 'sharp'
|
||||||
|
import { multiTenantPlugin } from '@payloadcms/plugin-multi-tenant'
|
||||||
|
|
||||||
import { Users } from './collections/Users';
|
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'
|
import { Tags } from './collections/Tags'
|
||||||
|
import { Tenants } from './collections/Tenants'
|
||||||
|
|
||||||
const filename = fileURLToPath(import.meta.url)
|
const filename = fileURLToPath(import.meta.url)
|
||||||
const dirname = path.dirname(filename)
|
const dirname = path.dirname(filename)
|
||||||
@@ -24,7 +26,7 @@ export default buildConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
cookiePrefix: 'funroad',
|
cookiePrefix: 'funroad',
|
||||||
collections: [Users, Media, Categories, Products, Tags],
|
collections: [Users, Media, Categories, Products, Tags, Tenants],
|
||||||
editor: lexicalEditor(),
|
editor: lexicalEditor(),
|
||||||
secret: process.env.PAYLOAD_SECRET || '',
|
secret: process.env.PAYLOAD_SECRET || '',
|
||||||
typescript: {
|
typescript: {
|
||||||
@@ -36,6 +38,15 @@ export default buildConfig({
|
|||||||
sharp,
|
sharp,
|
||||||
plugins: [
|
plugins: [
|
||||||
payloadCloudPlugin(),
|
payloadCloudPlugin(),
|
||||||
|
multiTenantPlugin({
|
||||||
|
collections: {
|
||||||
|
products: {},
|
||||||
|
},
|
||||||
|
tenantsArrayField: {
|
||||||
|
includeDefaultField: false,
|
||||||
|
},
|
||||||
|
userHasAccessToAllTenants: (user) => Boolean(user?.roles?.includes('super-admin')),
|
||||||
|
})
|
||||||
// storage-adapter-placeholder
|
// storage-adapter-placeholder
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
+24
@@ -140,6 +140,30 @@ const categories = [
|
|||||||
const seed = async () => {
|
const seed = async () => {
|
||||||
const payload = await getPayload({ config });
|
const payload = await getPayload({ config });
|
||||||
|
|
||||||
|
const adminTenant = await payload.create({
|
||||||
|
collection: 'tenants',
|
||||||
|
data: {
|
||||||
|
name: 'admin',
|
||||||
|
slug: 'admin',
|
||||||
|
stripeAccountId: 'admin',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await payload.create({
|
||||||
|
collection: 'users',
|
||||||
|
data: {
|
||||||
|
email: 'admin@nezumi.pw',
|
||||||
|
password: 'demo',
|
||||||
|
roles: ['super-admin'],
|
||||||
|
username: 'admin',
|
||||||
|
tenants: [
|
||||||
|
{
|
||||||
|
tenant: adminTenant.id,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
for (const category of categories) {
|
for (const category of categories) {
|
||||||
const { name, slug, color } = category;
|
const { name, slug, color } = category;
|
||||||
const categoryDoc = await payload.create({
|
const categoryDoc = await payload.create({
|
||||||
|
|||||||
Reference in New Issue
Block a user