A modern, scalable monorepo template for ecommerce projects based on the Feature-Based Architecture.
This project follows the Feature-Based Architecture as described in the Style Guide section.
- Monorepo: Managed with
pnpmworkspaces andTurbo. - Feature-Based Modules: Each app is divided into modules (e.g.,
product,cart,order). - Standardized Structure: Each module follows a strict internal structure (components, hooks, services, types.ts).
- Kebab-case: Strict enforcement of kebab-case for all files and directories.
apps/admin: Administrative dashboard for managing products, users, and orders.apps/storefront: Customer-facing shop for browsing products, managing cart, and placing orders.packages/ui: Shared UI components (powered by Storybook).
- Node.js (LTS recommended)
- pnpm installed globally
npm i -g pnpmFrom the monorepo root:
pnpm installRun the Admin app:
pnpm dev:adminRun the Storefront app:
pnpm dev:storefrontBuild all apps:
pnpm buildBuild a specific app:
pnpm build:admin
# or
pnpm build:storefrontThis document defines the naming conventions, architectural patterns, and folder structure standards for React and Next.js projects. Adhering to these guidelines ensures consistency across the codebase, making it easier for team members to navigate, understand, and maintain the application.
All file and directory names must use kebab-case (e.g., product-form.tsx, use-get-product.ts).
This ensures consistency across different operating systems and improves readability.
Forms are critical parts of the application. Consistent naming helps identify their purpose and scope at a glance.
- bulk: Operations involving multiple items simultaneously (e.g., uploading a CSV of products).
- wizard: A step-by-step flow or multi-step process (e.g., a registration flow with personal info, then address, then payment).
<Domain>Form.tsx: A general-purpose form for creating and editing a single item.<Domain><Action>Form.tsx: A form dedicated to a specific action (e.g.,ProductCreateForm).<Domain>Bulk<Action>Form.tsx: A form for performing an action on multiple items at once.<Domain><Action>WizardForm.tsx: A multi-step form for a specific action on a single item.<Domain>Bulk<Action>WizardForm.tsx: A multi-step form for a specific action on multiple items.
ProductForm: Simple form that handles both creation and editing of a single product.ProductCreateForm: Dedicated form for creating a new product.ProductEditForm: Dedicated form for editing an existing product.
ProductBulkCreateForm: Form designed to create multiple products at once.ProductBulkEditForm: Form designed to edit multiple products simultaneously.ProductCreateWizardForm: A multi-step form for registering a new product.
Schemas are defined using Zod and follow a naming convention that mirrors their usage in the application.
<domain>Schema: Represents the core data model of a single domain.<domain>FiltersSchema: Criteria used for searching or filtering domains.<domain><Action>InputSchema: The data structure required for a specific action (Request body).<domain><Action>OutputSchema: The data structure returned by a specific action (Response body).<domain>Bulk<Action>InputSchema: Input for a bulk operation.<domain>Bulk<Action>OutputSchema: Output from a bulk operation.
productSchemaproductFiltersSchemaproductCreateInputSchemaproductCreateOutputSchema
// Example: src/modules/product/schema.ts
export const productCreateInputSchema = z.object({
name: z.string(),
price: z.number(),
});Standardized type naming makes it clear what data structure is being handled. Types are often inferred from schemas.
<Domain>: Represents the core data model of a single domain.<Domain>Filters: Criteria used for searching or filtering domains.<Domain><Action>Input: The data structure required for a specific action.<Domain><Action>Output: The data structure returned by a specific action.<Domain>Bulk<Action>Input: Input for a bulk operation.<Domain>Bulk<Action>Output: Output from a bulk operation.<ComponentName>Props: Props for a specific React component.<HookName>Params: Params for a specific React hook.
ProductProductFiltersProductCreateInput
// Example: src/modules/product/types.ts
import { z } from "zod";
import { productCreateInputSchema } from "./schema";
export type ProductCreateInput = z.infer<typeof productCreateInputSchema>;
export type Product = {
name: string;
price: number;
};If the output of various actions (create, edit, get) for a domain is identical, use the common <Domain> type instead
of creating redundant output types.
// Example: src/modules/product/services/product-service.ts
export function getProducts(): Result<Product[]> {
// ...
}
export function createProduct(input: ProductCreateInput): Result<Product> {
// ...
}When creating components, destructure props in the function body for clarity and consistency.
// Example: src/modules/product/components/product-form-dialog.tsx
type ProductFormDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
productId?: string | null;
};
export function ProductFormDialog(props: ProductFormDialogProps) {
const { open, onOpenChange } = props;
// ...
}Pages should be treated as entry points that compose multiple smaller components. Logic for fetching data or handling forms should be kept at the page level or in high-level container components.
src/modules/<feature>/components/pages/<domain>-page.tsx: Main page component.src/modules/<feature>/components/pages/<domain>-details-page.tsx: Details page component.
// Example: src/modules/product/components/pages/products-page.tsx
export function ProductsPage() {
const handleAddProduct = () => {
// Navigate to the create product page
};
const pagination = {
// ...
};
const filters = useMemo(() => {
// ...
}, []);
const handleFiltersChange = useCallback((filters: ProductFilters) => {
// Handle filters change logic
}, []);
const { data, isLoading } = useGetProducts({
filters,
pagination,
});
return (
<div className="flex flex-col gap-4">
<ProductListHeader onAdd={handleAddProduct} />
<ProductFiltersBox
filters={filters}
onFiltersChange={handleFiltersChange}
/>
<ProductList items={data?.data} isLoading={isLoading} />
</div>
);
}For components that support both viewing and editing, split the logic into separate components:
<Domain>Display: A read-only view of the domain's data.<Domain>Form: An editable form for the domain's data.<Domain>: A parent component that manages the toggle state betweenDisplayandForm.
// Example: src/modules/product/components/product-details.tsx
export function ProductDetails() {
const [isEditing, setIsEditing] = useState(false);
if (isEditing) {
return <ProductDetailsForm onCancel={() => setIsEditing(false)} />;
}
return <ProductDetailsDisplay onEdit={() => setIsEditing(true)} />;
}Code should be organized by feature or domain under the src/modules directory. This promotes encapsulation and
modularity. Each module should follow this structure where applicable:
src/modules/<feature-name>/components/: React components specific to this feature. Use sub-directories for complex components.src/modules/<feature-name>/hooks/: Custom React hooks, often wrapping services with@tanstack/react-query.src/modules/<feature-name>/services/: Functions for API calls, data transformation, and core business logic.src/modules/<feature-name>/types.ts: TypeScript interfaces and types specific to the module.src/modules/<feature-name>/schema.ts: Zod validation schemas for forms and API interactions.src/modules/<feature-name>/context.ts: React Context and Providers for module-level state management.src/modules/<feature-name>/constants.ts: Module-specific constants, enums, and configuration.src/modules/<feature-name>/utils.ts: Small utility functions that don't belong in services.src/modules/<feature-name>/index.ts: The entry point for the module, exporting only what is needed by the rest of the application.
Hooks are primarily used to manage side effects and state. When using @tanstack/react-query, hooks should be prefixed
based on the operation type.
useGet<Domains>: For fetching a list of domains.useGet<Domain>: For fetching a single domain by ID.use<Action><Domain>Mutation: For specific actions (e.g.,useActivateProductMutation).use<Domain>Form: For managing form state and logic.use<Action><Domain>Form: For managing form state and logic.
useGetProductsuseGetProductuseUpdateProductMutationuseProductFormuseEditProductForm
Query keys should be structured as an array, starting with the domain name or module name, followed by any specific identifiers or parameters.
// Example
queryKey: ["products"];
queryKey: ["products", productId];
queryKey: ["products", { filters }];Mutations should generally invalidate relevant queries upon success to ensure data consistency.
// Example: src/modules/product/services/product-service.ts
export function useUpdateProductMutation() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: ProductUpdateInput) => updateProduct(data),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: ["products"],
});
},
});
}Services contain the core logic for interacting with external APIs or performing complex data transformations.
get<Domains>: Fetch a list of domains.get<Domain>: Fetch a single domain by ID.create<Domain>: Create a new domain.update<Domain>: Update an existing domain.delete<Domain>: Delete a domain.<action><Domain>: Perform a specific action on a domain (e.g.,activateProduct).
Standardizing how we interact with APIs ensures consistent error handling and data retrieval.
Usage Responsibility: Error handling is the responsibility of the calling code (the top level). Use try...catch
blocks in event handlers or components when calling service methods or mutations to handle potential failures and
display user-friendly messages (e.g., via toast).
Error Normalization: We use a standardized ResultError type for API errors. Any error (Axios, server, or runtime)
should be resolved to use the toResultError utility function to ensure it follows a consistent structure across the
application.
// src/modules/product/components/products-page.tsx
const handleUpdateProduct = async (id: string) => {
try {
await updateProduct.mutateAsync(id);
toast.success("Product updated successfully");
} catch (error) {
// Standardize any error using toResultError
toast.error(toResultError(error).message);
}
};When performing destructive or critical actions, use alertDialogManager to get user confirmation. This should be
handled in the event handler (e.g., handleDelete).
- Handle confirmation and later logic inside the event handler.
- Wrap the operation in a
try...catchblock. - Pass the async operation to the
onConfirmcallback ofalertDialogManager.confirm. - Provide immediate feedback via
toast.
// src/modules/product/components/products-page.tsx
const handleDeleteProduct = async (id: string) => {
try {
const confirmed = await alertDialogManager.confirm({
title: "Delete Product",
description: "Are you sure you want to delete this product?",
confirmText: "Delete",
onConfirm: () => deleteProduct.mutateAsync(id),
});
if (confirmed) {
toast.success("Product deleted successfully");
}
} catch (error) {
toast.error(toResultError(error).message);
}
};Result<T>: A standard response wrapper for a single object or simple list.ResultPaginated<T>: A response wrapper that includes pagination metadata.ResultError<T>: A standardized structure for API errors, including anid,message, and optional validationmeta.
Constants and utilities should be named descriptively and follow camelCase.
Use PascalCase for Enums and camelCase for constant objects or arrays.
// constants.ts
export enum ProductStatus {
Active = "active",
Inactive = "inactive",
}
export const PRODUCT_TABS = [
{ value: "overview", label: "Overview" },
{ value: "inventory", label: "Inventory" },
];Utility functions should be pure and focused on a single task.
translate<Domain><Purpose>: For translating domain-specific values.format<Domain><Purpose>: For formatting values (e.g., currency, dates).parse<Domain><Purpose>: For extracting data from strings or objects.calculate<Domain><Purpose>: For logic-heavy computations.generate<Domain><Purpose>: For creating new values (e.g., slugs, IDs).normalize<Domain><Purpose>: For standardizing data structures.build<Domain><Purpose>: For constructing complex objects or params.map<Domain>To<Target>: For transforming one data type to another.sanitize<Domain><Purpose>: For cleaning up data.sort<Domain>By<Property>: For ordering collections.
translateProductStatus(status, t)formatProductPrice(price, currency)parseProductFiltersParams(searchParams)calculateProductDiscount(price, compareAtPrice)generateProductSlug(name)normalizeProductFilters(filters)buildProductFilterParams(filters)mapProductToFormValues(product)sanitizeProductPayload(payload)sortProductsByPrice(products)
// utils.ts
export function formatProductPrice(amount: number, currency = "USD"): string {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency,
}).format(amount);
}We use next-intl for translations. Always use the useTranslations hook for managing UI text.
import { useTranslations } from "next-intl";
export function ProductDetails({ product }: { product: Product }) {
// Translation key should be same as Component Name.
const t = useTranslations("ProductDetails");
return (
<div>
<h1>{t("title")}</h1>
<p>{t("description", { name: product.name })}</p>
<button>{t("actions.edit")}</button>
</div>
);
}For formatting dates and times, use the provided hooks in src/modules/i18n/hooks/utils.ts.
import {
useDisplayDateTimeFormatter,
useDisplayDateFormatter,
} from "@/modules/i18n/hooks/utils";
type ProductActivityItemProps = {
date: Date;
};
export function ProductActivityItem(props: ProductActivityItemProps) {
const { date } = props;
const formatDateTime = useDisplayDateTimeFormatter();
const formatDate = useDisplayDateFormatter();
return (
<div>
<span>{formatDateTime(date)}</span>
<span>{formatDate(date)}</span>
</div>
);
}When you need to translate Enums or status codes outside of a component or for reuse, create a utility function using
the translate<Domain><Purpose> pattern that accepts the translation function t.
// src/modules/product/utils.ts
import { useTranslations } from "next-intl";
import { ProductStatus } from "./constants";
export function translateProductStatus(
status: ProductStatus,
t: ReturnType<typeof useTranslations<never>>,
) {
return {
[ProductStatus.Active]: t("ProductStatus.active"),
[ProductStatus.Inactive]: t("ProductStatus.inactive"),
}[status];
}Usage in a component:
// src/modules/product/components/product-status-badge.tsx
import { useTranslations } from "next-intl";
import { translateProductStatus } from "../utils";
export function ProductStatusBadge({ status }: { status: ProductStatus }) {
const t = useTranslations("Product");
return <Badge>{translateProductStatus(status, t)}</Badge>;
}src/modules/product/
├── components/
│ ├── products-page.tsx // Compose products list page parts: useGetProducts, ProductList, ProductListFiltersBox, ...
│ ├── product-list.tsx // Pure component for rendering the product list/table
│ ├── product-list-columns.tsx // Columns config for ProductList
│ ├── product-list-actions.tsx // Row/bulk actions for ProductList
│ ├── product-filters-box.tsx // Filters related to Product list
│ ├── product-details-page.tsx // Compose product details page: getProduct, ProductDetailsTabs, actions, ...
│ ├── product-details-tabs.tsx // Tab navigation/container for ProductDetails
│ ├── product-details-overview-tab.tsx // Overview/general product information tab
│ ├── product-details-overview.tsx // Compose ProductDetailsOverviewForm, ProductDetailsOverviewDisplay, getProduct, ...
│ ├── product-details-overview-form.tsx // Shared form used by one or more details tabs
│ ├── product-details-overview-display.tsx // Shared read-only display used by one or more details tabs
│ ├── product-details-inventory-tab.tsx // Stock/inventory-related tab
│ ├── product-details-pricing-tab.tsx // Pricing-related tab
│ ├── product-details-images-tab.tsx // Product images/media tab
│ ├── product-details-settings-tab.tsx // Product settings/status/configuration tab
│ ├── product-deactivate-form-dialog.tsx // Dialog for deactivating a product
│ ├── product-status-badge.tsx
│ ├── product-type-badge.tsx
│ └── product-thumbnail.tsx
├── hooks/
│ ├── use-product-details-overview-form.ts
│ └── use-product-service.ts // Hook wrapper of product services inside product-service.ts
├── services/
│ └── product-service.ts
├── constants.ts
├── schema.ts
├── types.ts
├── utils.ts
└── index.ts
src/modules/
├── product/
│ ├── components/
│ │ ├── pages/
│ │ │ ├── products-page.tsx
│ │ │ ├── product-create-page.tsx
│ │ │ ├── product-edit-page.tsx
│ │ │ ├── product-details-page.tsx
│ │ │ └── index.ts
│ │ ├── product-list/
│ │ │ ├── product-list.tsx
│ │ │ ├── product-list-columns.tsx
│ │ │ ├── product-list-actions.tsx
│ │ │ ├── product-list-toolbar.tsx
│ │ │ ├── product-list-filters-box.tsx
│ │ │ └── index.ts
│ │ ├── product-form/
│ │ │ ├── product-form.tsx
│ │ │ ├── product-form-basic-section.tsx
│ │ │ ├── product-form-description-section.tsx
│ │ │ ├── product-form-status-section.tsx
│ │ │ └── index.ts
│ │ ├── product-details/
│ │ │ ├── product-details-header.tsx
│ │ │ ├── product-details-summary-card.tsx
│ │ │ ├── product-details-actions.tsx
│ │ │ ├── product-details-tabs.tsx
│ │ │ ├── product-details-overview-tab.tsx
│ │ │ ├── product-details-activity-tab.tsx
│ │ │ └── index.ts
│ │ ├── product-dialogs/
│ │ │ ├── product-activate-dialog.tsx
│ │ │ ├── product-deactivate-dialog.tsx
│ │ │ ├── product-delete-dialog.tsx
│ │ │ ├── product-duplicate-dialog.tsx
│ │ │ └── index.ts
│ │ └── shared/
│ │ ├── product-status-badge.tsx
│ │ ├── product-type-badge.tsx
│ │ ├── product-thumbnail.tsx
│ │ ├── product-sku-text.tsx
│ │ └── index.ts
│ ├── hooks/
│ │ ├── use-get-products.ts
│ │ ├── use-get-product.ts
│ │ ├── use-create-product-mutation.ts
│ │ ├── use-update-product-mutation.ts
│ │ ├── use-delete-product-mutation.ts
│ │ ├── use-product-form.ts
│ │ └── index.ts
│ ├── services/
│ │ ├── product-service.ts
│ │ └── index.ts
│ ├── schemas/
│ │ ├── product-schema.ts
│ │ ├── product-form-schema.ts
│ │ ├── product-filters-schema.ts
│ │ └── index.ts
│ ├── types/
│ │ ├── product.ts
│ │ ├── product-form.ts
│ │ ├── product-filters.ts
│ │ └── index.ts
│ ├── constants/
│ │ ├── product-status.ts
│ │ ├── product-type.ts
│ │ ├── product-tabs.ts
│ │ └── index.ts
│ ├── utils/
│ │ ├── generate-product-slug.ts
│ │ ├── translate-product-status.ts
│ │ └── index.ts
│ └── index.ts
│
├── product-media/
│ ├── components/
│ │ ├── product-media-gallery.tsx
│ │ ├── product-media-uploader.tsx
│ │ ├── product-media-dropzone.tsx
│ │ ├── product-media-item.tsx
│ │ ├── product-media-sortable-list.tsx
│ │ ├── product-media-preview-dialog.tsx
│ │ ├── product-media-delete-dialog.tsx
│ │ └── index.ts
│ ├── hooks/
│ │ ├── use-get-product-media.ts
│ │ ├── use-upload-product-media-mutation.ts
│ │ ├── use-delete-product-media-mutation.ts
│ │ ├── use-reorder-product-media-mutation.ts
│ │ └── index.ts
│ ├── services/
│ │ ├── product-media-service.ts
│ │ └── index.ts
│ ├── schemas/
│ │ ├── product-media-schema.ts
│ │ └── index.ts
│ ├── types/
│ │ ├── product-media.ts
│ │ └── index.ts
│ └── index.ts
│
├── product-variant/
│ ├── components/
│ │ ├── product-variants-table.tsx
│ │ ├── product-variants-columns.tsx
│ │ ├── product-variant-form.tsx
│ │ ├── product-variant-create-dialog.tsx
│ │ ├── product-variant-edit-dialog.tsx
│ │ ├── product-variant-delete-dialog.tsx
│ │ ├── product-variant-options-editor.tsx
│ │ └── index.ts
│ ├── hooks/
│ │ ├── use-get-product-variants.ts
│ │ ├── use-get-product-variant.ts
│ │ ├── use-create-product-variant-mutation.ts
│ │ ├── use-update-product-variant-mutation.ts
│ │ ├── use-delete-product-variant-mutation.ts
│ │ └── index.ts
│ ├── services/
│ │ ├── product-variant-service.ts
│ │ └── index.ts
│ ├── schemas/
│ │ ├── product-variant-schema.ts
│ │ ├── product-variant-form-schema.ts
│ │ └── index.ts
│ ├── types/
│ │ ├── product-variant.ts
│ │ ├── product-variant-form.ts
│ │ └── index.ts
│ └── index.ts
│
├── product-pricing/
│ ├── components/
│ │ ├── product-price-card.tsx
│ │ ├── product-price-form.tsx
│ │ ├── product-discount-form.tsx
│ │ ├── product-sale-price-form.tsx
│ │ ├── product-tax-class-select.tsx
│ │ ├── product-currency-select.tsx
│ │ └── index.ts
│ ├── hooks/
│ │ ├── use-get-product-pricing.ts
│ │ ├── use-update-product-pricing-mutation.ts
│ │ └── index.ts
│ ├── services/
│ │ ├── product-pricing-service.ts
│ │ └── index.ts
│ ├── schemas/
│ │ ├── product-pricing-schema.ts
│ │ ├── product-pricing-form-schema.ts
│ │ └── index.ts
│ ├── types/
│ │ ├── product-pricing.ts
│ │ └── index.ts
│ └── index.ts
│
├── product-inventory/
│ ├── components/
│ │ ├── product-inventory-card.tsx
│ │ ├── product-inventory-form.tsx
│ │ ├── product-stock-status-badge.tsx
│ │ ├── product-stock-adjustment-dialog.tsx
│ │ ├── product-low-stock-alert.tsx
│ │ ├── product-warehouse-stock-list.tsx
│ │ └── index.ts
│ ├── hooks/
│ │ ├── use-get-product-inventory.ts
│ │ ├── use-update-product-inventory-mutation.ts
│ │ ├── use-adjust-product-stock-mutation.ts
│ │ └── index.ts
│ ├── services/
│ │ ├── product-inventory-service.ts
│ │ └── index.ts
│ ├── schemas/
│ │ ├── product-inventory-schema.ts
│ │ ├── product-stock-adjustment-schema.ts
│ │ └── index.ts
│ ├── types/
│ │ ├── product-inventory.ts
│ │ ├── product-stock-adjustment.ts
│ │ └── index.ts
│ └── index.ts
│
├── product-attribute/
│ ├── components/
│ │ ├── product-attributes-table.tsx
│ │ ├── product-attributes-form.tsx
│ │ ├── product-attribute-row.tsx
│ │ ├── product-attribute-value-input.tsx
│ │ ├── product-attribute-group-select.tsx
│ │ └── index.ts
│ ├── hooks/
│ │ ├── use-get-product-attributes.ts
│ │ ├── use-update-product-attributes-mutation.ts
│ │ └── index.ts
│ ├── services/
│ │ ├── product-attribute-service.ts
│ │ └── index.ts
│ ├── schemas/
│ │ ├── product-attribute-schema.ts
│ │ └── index.ts
│ ├── types/
│ │ ├── product-attribute.ts
│ │ └── index.ts
│ └── index.ts
│
├── product-category/
│ ├── components/
│ │ ├── product-category-select.tsx
│ │ ├── product-category-tree.tsx
│ │ ├── product-category-breadcrumb.tsx
│ │ ├── product-category-assignment-list.tsx
│ │ └── index.ts
│ ├── hooks/
│ │ ├── use-get-product-categories.ts
│ │ ├── use-update-product-categories-mutation.ts
│ │ └── index.ts
│ ├── services/
│ │ ├── product-category-service.ts
│ │ └── index.ts
│ ├── schemas/
│ │ ├── product-category-schema.ts
│ │ └── index.ts
│ ├── types/
│ │ ├── product-category.ts
│ │ └── index.ts
│ └── index.ts
│
├── product-seo/
│ ├── components/
│ │ ├── product-seo-card.tsx
│ │ ├── product-seo-form.tsx
│ │ ├── product-slug-input.tsx
│ │ ├── product-meta-title-input.tsx
│ │ ├── product-meta-description-input.tsx
│ │ ├── product-search-preview.tsx
│ │ └── index.ts
│ ├── hooks/
│ │ ├── use-get-product-seo.ts
│ │ ├── use-update-product-seo-mutation.ts
│ │ └── index.ts
│ ├── services/
│ │ ├── product-seo-service.ts
│ │ └── index.ts
│ ├── schemas/
│ │ ├── product-seo-schema.ts
│ │ ├── product-seo-form-schema.ts
│ │ └── index.ts
│ ├── types/
│ │ ├── product-seo.ts
│ │ └── index.ts
│ └── index.ts
│
├── product-shipping/
│ ├── components/
│ │ ├── product-shipping-card.tsx
│ │ ├── product-shipping-form.tsx
│ │ ├── product-weight-input.tsx
│ │ ├── product-dimensions-input.tsx
│ │ ├── product-shipping-class-select.tsx
│ │ └── index.ts
│ ├── hooks/
│ │ ├── use-get-product-shipping.ts
│ │ ├── use-update-product-shipping-mutation.ts
│ │ └── index.ts
│ ├── services/
│ │ ├── product-shipping-service.ts
│ │ └── index.ts
│ ├── schemas/
│ │ ├── product-shipping-schema.ts
│ │ ├── product-dimensions-schema.ts
│ │ └── index.ts
│ ├── types/
│ │ ├── product-shipping.ts
│ │ ├── product-dimensions.ts
│ │ └── index.ts
│ └── index.ts
│
└── product-relation/
├── components/
│ ├── related-products-list.tsx
│ ├── related-products-selector.tsx
│ ├── upsell-products-selector.tsx
│ ├── cross-sell-products-selector.tsx
│ └── index.ts
├── hooks/
│ ├── use-get-related-products.ts
│ ├── use-update-related-products-mutation.ts
│ ├── use-upsell-products.ts
│ ├── use-cross-sell-products.ts
│ └── index.ts
├── services/
│ ├── product-relation-service.ts
│ └── index.ts
├── schemas/
│ ├── product-relation-schema.ts
│ └── index.ts
├── types/
│ ├── product-relation.ts
│ └── index.ts
└── index.ts