├── .npmrc
├── src
├── stores
│ ├── Currency
│ │ ├── types.ts
│ │ ├── index.tsx
│ │ └── CurrencySelector
│ │ │ └── index.tsx
│ ├── WishlistStore
│ │ └── types.ts
│ ├── CartStore
│ │ └── types.ts
│ ├── CartStateStore
│ │ └── index.tsx
│ └── WishListStateStore
│ │ └── index.tsx
├── access
│ ├── anyone.ts
│ ├── authenticated.ts
│ └── authenticatedOrPublished.ts
├── app
│ ├── favicon.ico
│ ├── icon1.png
│ ├── apple-icon.png
│ ├── (frontend)
│ │ ├── [locale]
│ │ │ ├── (with-cart)
│ │ │ │ ├── page.tsx
│ │ │ │ ├── account
│ │ │ │ │ ├── orders
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── help
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── page.tsx
│ │ │ │ │ ├── settings
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── orders-data
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ └── layout.tsx
│ │ │ │ ├── [slug]
│ │ │ │ │ └── page.client.tsx
│ │ │ │ ├── layout.tsx
│ │ │ │ └── product
│ │ │ │ │ └── [slug]
│ │ │ │ │ └── page.tsx
│ │ │ ├── (without-cart)
│ │ │ │ ├── admin
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── layout.tsx
│ │ │ │ ├── checkout
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── posts
│ │ │ │ │ ├── page.client.tsx
│ │ │ │ │ ├── [slug]
│ │ │ │ │ │ └── page.client.tsx
│ │ │ │ │ └── page
│ │ │ │ │ │ └── [pageNumber]
│ │ │ │ │ │ └── page.client.tsx
│ │ │ │ ├── postSearch
│ │ │ │ │ └── page.client.tsx
│ │ │ │ ├── register
│ │ │ │ │ └── page.tsx
│ │ │ │ └── login
│ │ │ │ │ └── page.tsx
│ │ │ ├── not-found.tsx
│ │ │ └── reset-password
│ │ │ │ └── page.tsx
│ │ ├── next
│ │ │ ├── exit-preview
│ │ │ │ ├── GET.ts
│ │ │ │ └── route.ts
│ │ │ ├── verify-email
│ │ │ │ └── route.ts
│ │ │ ├── seed
│ │ │ │ └── route.ts
│ │ │ └── wishListProducts
│ │ │ │ └── route.ts
│ │ ├── not-found.tsx
│ │ └── (sitemaps)
│ │ │ └── posts-sitemap.xml
│ │ │ └── route.ts
│ ├── (payload)
│ │ ├── api
│ │ │ ├── graphql-playground
│ │ │ │ └── route.ts
│ │ │ ├── graphql
│ │ │ │ └── route.ts
│ │ │ └── [...slug]
│ │ │ │ └── route.ts
│ │ ├── admin
│ │ │ └── [[...segments]]
│ │ │ │ ├── page.tsx
│ │ │ │ └── not-found.tsx
│ │ └── layout.tsx
│ └── manifest.json
├── blocks
│ ├── Form
│ │ ├── Error
│ │ │ └── index.tsx
│ │ ├── Width
│ │ │ └── index.tsx
│ │ ├── Message
│ │ │ └── index.tsx
│ │ ├── fields.tsx
│ │ ├── config.ts
│ │ ├── Number
│ │ │ └── index.tsx
│ │ ├── Text
│ │ │ └── index.tsx
│ │ ├── Email
│ │ │ └── index.tsx
│ │ ├── Textarea
│ │ │ └── index.tsx
│ │ ├── buildInitialFormState.tsx
│ │ └── Checkbox
│ │ │ └── index.tsx
│ ├── MediaBlock
│ │ └── config.ts
│ ├── Code
│ │ ├── Component.tsx
│ │ ├── config.ts
│ │ ├── CopyButton.tsx
│ │ └── Component.client.tsx
│ ├── CallToAction
│ │ ├── config.ts
│ │ └── Component.tsx
│ ├── Banner
│ │ ├── Component.tsx
│ │ └── config.ts
│ ├── RelatedPosts
│ │ └── Component.tsx
│ ├── globals.ts
│ └── Accordion
│ │ ├── config.ts
│ │ └── Component.tsx
├── components
│ ├── AdminBar
│ │ └── index.scss
│ ├── AdminAvatar
│ │ └── index.tsx
│ ├── BeforeDashboard
│ │ ├── SeedButton
│ │ │ └── index.scss
│ │ └── index.scss
│ ├── (ecommerce)
│ │ ├── RowLabels
│ │ │ ├── DeliveryZonesRowLabel
│ │ │ │ └── index.tsx
│ │ │ ├── PriceRowLabel
│ │ │ │ └── index.tsx
│ │ │ ├── WeightRangeRowLabel
│ │ │ │ └── index.tsx
│ │ │ └── OrderProductsRowLabel
│ │ │ │ └── index.tsx
│ │ ├── Cart
│ │ │ └── SynchronizeCart
│ │ │ │ └── index.tsx
│ │ ├── AdminDashboard
│ │ │ └── components
│ │ │ │ ├── views
│ │ │ │ └── index.tsx
│ │ │ │ └── OverviewCard
│ │ │ │ └── index.tsx
│ │ ├── PriceClient
│ │ │ └── index.tsx
│ │ ├── LogoutButton
│ │ │ └── index.tsx
│ │ ├── AdminDashboardNavLink
│ │ │ └── index.tsx
│ │ ├── CurrencySelect
│ │ │ └── index.tsx
│ │ ├── RegisterPage
│ │ │ └── WithoutOAuth
│ │ │ │ └── index.tsx
│ │ └── ListingBreadcrumbs
│ │ │ └── index.tsx
│ ├── LivePreviewListener
│ │ └── index.tsx
│ ├── AdminNavbar
│ │ ├── NavHamburger
│ │ │ └── index.tsx
│ │ ├── NavWrapper
│ │ │ └── index.tsx
│ │ └── getNavPrefs.ts
│ ├── ui
│ │ ├── AdminInput.tsx
│ │ ├── label.tsx
│ │ ├── textarea.tsx
│ │ ├── input.tsx
│ │ ├── checkbox.tsx
│ │ ├── popover.tsx
│ │ └── radio-group.tsx
│ ├── AdminColorPicker
│ │ └── index.tsx
│ ├── heros
│ │ ├── LowImpact
│ │ │ └── index.tsx
│ │ ├── RenderHero.tsx
│ │ ├── MediumImpact
│ │ │ └── index.tsx
│ │ └── HighImpact
│ │ │ └── index.tsx
│ ├── Emails
│ │ ├── OrderStatusEmail
│ │ │ └── index.tsx
│ │ ├── WelcomeEmail
│ │ │ └── index.tsx
│ │ ├── VerifyAccountEmail
│ │ │ └── index.tsx
│ │ └── ResetPasswordEmail
│ │ │ └── index.tsx
│ ├── Media
│ │ ├── index.tsx
│ │ ├── types.ts
│ │ └── VideoMedia
│ │ │ └── index.tsx
│ ├── Logo
│ │ └── Logo.tsx
│ ├── LocaleSwitch
│ │ └── LocaleSwitch.tsx
│ ├── CollectionArchive
│ │ └── index.tsx
│ ├── search
│ │ ├── Component.tsx
│ │ ├── fieldOverrides.ts
│ │ └── beforeSync.ts
│ ├── AdminResetPassword
│ │ └── index.tsx
│ └── PageRange
│ │ └── index.tsx
├── utilities
│ ├── canUseDOM.ts
│ ├── toKebabCase.ts
│ ├── cn.ts
│ ├── useDebounce.ts
│ ├── formatPrices.ts
│ ├── mergeOpenGraph.ts
│ ├── getRedirects.ts
│ ├── formatDateTime.ts
│ ├── getURL.ts
│ ├── getDocument.ts
│ ├── deepMerge.ts
│ ├── formatAuthors.ts
│ ├── getOrderProducts.ts
│ ├── getGlobals.ts
│ ├── getCustomer.ts
│ ├── generatePreviewPath.ts
│ ├── getMeUser.ts
│ ├── generateMeta.ts
│ ├── nodemailer.ts
│ └── getPriceRange.ts
├── i18n
│ ├── config.ts
│ ├── routing.ts
│ └── request.ts
├── providers
│ ├── Theme
│ │ ├── ThemeSelector
│ │ │ ├── types.ts
│ │ │ └── index.tsx
│ │ ├── types.ts
│ │ ├── shared.ts
│ │ └── InitTheme
│ │ │ └── index.tsx
│ ├── index.tsx
│ └── HeaderTheme
│ │ └── index.tsx
├── fields
│ ├── slug
│ │ ├── index.scss
│ │ └── formatSlug.ts
│ ├── currencyField.ts
│ ├── backgroundPicker.ts
│ ├── countryPickerField.ts
│ ├── alignmentField.ts
│ ├── courierSettingsFields.ts
│ ├── linkGroup.ts
│ ├── freeShippingField.ts
│ ├── defaultLexical.ts
│ ├── noBlocksLexical.ts
│ └── courierFields.ts
├── cssVariables.ts
├── collections
│ ├── (ecommerce)
│ │ ├── Products
│ │ │ └── components
│ │ │ │ └── RowLabels
│ │ │ │ ├── DetailLabel
│ │ │ │ └── index.tsx
│ │ │ │ ├── OptionLabel
│ │ │ │ └── index.tsx
│ │ │ │ └── VariantLabel
│ │ │ │ └── index.tsx
│ │ ├── Orders
│ │ │ ├── components
│ │ │ │ ├── couriers
│ │ │ │ │ └── CourierShipmentMenu.tsx
│ │ │ │ ├── inpost-pickup
│ │ │ │ │ └── PickupShipmentMenu.tsx
│ │ │ │ ├── VariantSelect
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── OrderTotalPriceField
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── OrderTotalWithShippingField
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── ProductTotalPriceField
│ │ │ │ │ └── index.tsx
│ │ │ │ └── ProductNameField
│ │ │ │ │ └── index.tsx
│ │ │ ├── hooks
│ │ │ │ ├── generateID.ts
│ │ │ │ └── sendStatusEmail.ts
│ │ │ └── utils
│ │ │ │ └── getShippingLabel.ts
│ │ ├── Customers
│ │ │ ├── ui
│ │ │ │ └── RowLabels
│ │ │ │ │ └── ShippingAddressRowLabel
│ │ │ │ │ └── index.tsx
│ │ │ └── hooks
│ │ │ │ ├── sendWelcomeEmail.ts
│ │ │ │ └── createTokenAndSendEmail.ts
│ │ ├── ProductReviews
│ │ │ └── index.ts
│ │ ├── ProductSubCategories
│ │ │ └── index.ts
│ │ └── ProductCategories
│ │ │ └── index.ts
│ ├── Categories.ts
│ ├── Administrators
│ │ └── index.ts
│ ├── Posts
│ │ └── hooks
│ │ │ ├── populateAuthors.ts
│ │ │ └── revalidatePost.ts
│ └── Pages
│ │ └── hooks
│ │ └── revalidatePage.ts
├── middleware.ts
├── hooks
│ ├── revalidateRedirects.ts
│ ├── revalidateGlobal.ts
│ ├── populatePublishedAt.ts
│ ├── use-mobile.tsx
│ └── formatSlug.ts
├── globals
│ ├── Footer
│ │ ├── RowLabel.tsx
│ │ └── config.ts
│ ├── Header
│ │ ├── RowLabel.tsx
│ │ ├── Component.tsx
│ │ └── Component.client.tsx
│ └── (ecommerce)
│ │ └── Layout
│ │ ├── ProductList
│ │ └── variants
│ │ │ └── filters
│ │ │ ├── None.tsx
│ │ │ └── WithSidebar
│ │ │ ├── stores
│ │ │ └── MobileFiltersContext.tsx
│ │ │ └── components
│ │ │ ├── MobileFiltersDialog.tsx
│ │ │ ├── MobileFunnelFiltersButton.tsx
│ │ │ ├── MobileFiltersCloseButton.tsx
│ │ │ └── SortSelect.tsx
│ │ ├── ClientPanel
│ │ ├── Help
│ │ │ └── Component.tsx
│ │ ├── Orders
│ │ │ └── Component.tsx
│ │ ├── Component.tsx
│ │ └── variants
│ │ │ └── WithSidebar
│ │ │ └── index.tsx
│ │ ├── Cart
│ │ └── Component.tsx
│ │ ├── WishList
│ │ └── Component.tsx
│ │ ├── ProductDetails
│ │ ├── types
│ │ │ └── index.ts
│ │ └── Component.tsx
│ │ └── Checkout
│ │ ├── Component.tsx
│ │ └── variants
│ │ └── OneStepWithSummary
│ │ └── index.tsx
├── schemas
│ ├── loginForm.schema.ts
│ ├── ResetPasswordFormSchema.ts
│ ├── changePasswordModalForm.schema.ts
│ └── registerForm.schema.ts
├── environment.d.ts
└── lib
│ ├── getTotalWeight.ts
│ ├── couriers
│ └── labels
│ │ └── getInpostLabel.ts
│ ├── getTotal.ts
│ └── paywalls
│ └── getAutopayPaymentURL.ts
├── postcss.config.js
├── .vscode
├── extensions.json
├── launch.json
└── settings.json
├── public
├── favicon.ico
├── paczkomat.png
├── storefront.png
├── admin-dashboard.png
├── inpost_courier.png
├── website-template-OG.webp
├── blocksThumbnails
│ ├── carousel.png
│ └── accordion.png
├── web-app-manifest-192x192.png
└── web-app-manifest-512x512.png
├── .gitignore
├── .editorconfig
├── .prettierrc.json
├── next-env.d.ts
├── .prettierignore
├── .env.example
├── components.json
├── redirects.js
├── next-sitemap.config.cjs
├── LICENSE.md
├── next.config.mjs
├── tsconfig.json
└── Dockerfile
/.npmrc:
--------------------------------------------------------------------------------
1 | legacy-peer-deps=true
2 | enable-pre-post-scripts=true
3 |
--------------------------------------------------------------------------------
/src/stores/Currency/types.ts:
--------------------------------------------------------------------------------
1 | export type Currency = "USD" | "PLN" | "GBP" | "EUR";
2 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | '@tailwindcss/postcss': {},
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
3 | }
4 |
--------------------------------------------------------------------------------
/src/access/anyone.ts:
--------------------------------------------------------------------------------
1 | import type { Access } from "payload";
2 |
3 | export const anyone: Access = () => true;
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mandala-Software-House/payload-ecommerce-template/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mandala-Software-House/payload-ecommerce-template/HEAD/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/app/icon1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mandala-Software-House/payload-ecommerce-template/HEAD/src/app/icon1.png
--------------------------------------------------------------------------------
/public/paczkomat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mandala-Software-House/payload-ecommerce-template/HEAD/public/paczkomat.png
--------------------------------------------------------------------------------
/public/storefront.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mandala-Software-House/payload-ecommerce-template/HEAD/public/storefront.png
--------------------------------------------------------------------------------
/src/app/apple-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mandala-Software-House/payload-ecommerce-template/HEAD/src/app/apple-icon.png
--------------------------------------------------------------------------------
/public/admin-dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mandala-Software-House/payload-ecommerce-template/HEAD/public/admin-dashboard.png
--------------------------------------------------------------------------------
/public/inpost_courier.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mandala-Software-House/payload-ecommerce-template/HEAD/public/inpost_courier.png
--------------------------------------------------------------------------------
/public/website-template-OG.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mandala-Software-House/payload-ecommerce-template/HEAD/public/website-template-OG.webp
--------------------------------------------------------------------------------
/public/blocksThumbnails/carousel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mandala-Software-House/payload-ecommerce-template/HEAD/public/blocksThumbnails/carousel.png
--------------------------------------------------------------------------------
/public/web-app-manifest-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mandala-Software-House/payload-ecommerce-template/HEAD/public/web-app-manifest-192x192.png
--------------------------------------------------------------------------------
/public/web-app-manifest-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mandala-Software-House/payload-ecommerce-template/HEAD/public/web-app-manifest-512x512.png
--------------------------------------------------------------------------------
/src/blocks/Form/Error/index.tsx:
--------------------------------------------------------------------------------
1 | export const Error = () => {
2 | return
This field is required
;
3 | };
4 |
--------------------------------------------------------------------------------
/src/components/AdminBar/index.scss:
--------------------------------------------------------------------------------
1 | @import "~@payloadcms/ui/scss";
2 |
3 | .admin-bar {
4 | @include small-break {
5 | display: none;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/utilities/canUseDOM.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | export default !!(typeof window !== "undefined" && window.document && window.document.createElement);
3 |
--------------------------------------------------------------------------------
/public/blocksThumbnails/accordion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mandala-Software-House/payload-ecommerce-template/HEAD/public/blocksThumbnails/accordion.png
--------------------------------------------------------------------------------
/src/components/AdminAvatar/index.tsx:
--------------------------------------------------------------------------------
1 | import { UserCog2 } from "lucide-react";
2 |
3 | export const AdminAvatar = () => {
4 | return ;
5 | };
6 |
--------------------------------------------------------------------------------
/src/i18n/config.ts:
--------------------------------------------------------------------------------
1 | export type Locale = (typeof locales)[number];
2 |
3 | export const locales = ["pl", "en"] as const;
4 | export const defaultLocale: Locale = "pl";
5 |
--------------------------------------------------------------------------------
/src/providers/Theme/ThemeSelector/types.ts:
--------------------------------------------------------------------------------
1 | export type Theme = "dark" | "light";
2 |
3 | export const themeLocalStorageKey = "payload-theme";
4 |
5 | export const defaultTheme = "light";
6 |
--------------------------------------------------------------------------------
/src/utilities/toKebabCase.ts:
--------------------------------------------------------------------------------
1 | export const toKebabCase = (string: string): string =>
2 | string
3 | ?.replace(/([a-z])([A-Z])/g, "$1-$2")
4 | .replace(/\s+/g, "-")
5 | .toLowerCase();
6 |
--------------------------------------------------------------------------------
/src/utilities/cn.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | dist / media
3 | node_modules
4 | .DS_Store
5 | .env
6 | .next
7 | .vercel
8 |
9 | # Payload default media upload directory
10 | public/media/
11 |
12 | public/robots.txt
13 | public/sitemap*.xml
14 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 | end_of_line = lf
10 | max_line_length = null
11 |
--------------------------------------------------------------------------------
/src/app/(frontend)/[locale]/(with-cart)/page.tsx:
--------------------------------------------------------------------------------
1 | import PageTemplate, { generateMetadata } from "./[slug]/page";
2 |
3 | export const dynamic = "force-dynamic";
4 |
5 | export default PageTemplate;
6 |
7 | export { generateMetadata };
8 |
--------------------------------------------------------------------------------
/src/app/(frontend)/[locale]/(without-cart)/admin/page.tsx:
--------------------------------------------------------------------------------
1 | import { redirect } from "next/navigation";
2 |
3 | export const dynamic = "force-dynamic";
4 |
5 | const Page = () => {
6 | redirect("/admin");
7 | };
8 | export default Page;
9 |
--------------------------------------------------------------------------------
/src/stores/WishlistStore/types.ts:
--------------------------------------------------------------------------------
1 | import { type Product } from "@/payload-types";
2 |
3 | export type WishListProduct = {
4 | id: Product["id"];
5 | choosenVariantSlug?: string;
6 | };
7 |
8 | export type WishList = WishListProduct[];
9 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "singleQuote": false,
4 | "trailingComma": "all",
5 | "printWidth": 110,
6 | "tabWidth": 2,
7 | "plugins": ["prettier-plugin-tailwindcss"],
8 | "tailwindConfig": "./tailwind.config.ts"
9 | }
10 |
--------------------------------------------------------------------------------
/src/stores/CartStore/types.ts:
--------------------------------------------------------------------------------
1 | import { type Product } from "@/payload-types";
2 |
3 | export type CartProduct = {
4 | id: Product["id"];
5 | quantity: number;
6 | choosenVariantSlug?: string;
7 | };
8 |
9 | export type Cart = CartProduct[];
10 |
--------------------------------------------------------------------------------
/src/app/(frontend)/next/exit-preview/GET.ts:
--------------------------------------------------------------------------------
1 | import { draftMode } from "next/headers";
2 |
3 | export async function GET(): Promise {
4 | const draft = await draftMode();
5 | draft.disable();
6 | return new Response("Draft mode is disabled");
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/(frontend)/next/exit-preview/route.ts:
--------------------------------------------------------------------------------
1 | import { draftMode } from "next/headers";
2 |
3 | export async function GET(): Promise {
4 | const draft = await draftMode();
5 | draft.disable();
6 | return new Response("Draft mode is disabled");
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/BeforeDashboard/SeedButton/index.scss:
--------------------------------------------------------------------------------
1 | .seedButton {
2 | appearance: none;
3 | background: none;
4 | border: none;
5 | padding: 0;
6 | text-decoration: underline;
7 |
8 | &:hover {
9 | cursor: pointer;
10 | opacity: 0.85;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/fields/slug/index.scss:
--------------------------------------------------------------------------------
1 | .slug-field-component {
2 | .label-wrapper {
3 | display: flex;
4 | justify-content: space-between;
5 | align-items: center;
6 | }
7 |
8 | .lock-button {
9 | margin: 0;
10 | padding-bottom: 0.3125rem;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 |
5 | // NOTE: This file should not be edited
6 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
7 |
--------------------------------------------------------------------------------
/src/app/(frontend)/[locale]/(with-cart)/account/orders/page.tsx:
--------------------------------------------------------------------------------
1 | import { Orders } from "@/globals/(ecommerce)/Layout/ClientPanel/Orders/Component";
2 |
3 | export const dynamic = "force-dynamic";
4 |
5 | const OrdersPage = () => {
6 | return ;
7 | };
8 | export default OrdersPage;
9 |
--------------------------------------------------------------------------------
/src/cssVariables.ts:
--------------------------------------------------------------------------------
1 | // Keep these in sync with the CSS variables in your tailwind configuration
2 |
3 | export const cssVariables = {
4 | breakpoints: {
5 | "3xl": 1920,
6 | "2xl": 1536,
7 | xl: 1280,
8 | lg: 1024,
9 | md: 768,
10 | sm: 640,
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | **/payload-types.ts
2 | .tmp
3 | **/.git
4 | **/.hg
5 | **/.pnp.*
6 | **/.svn
7 | **/.yarn/**
8 | **/build
9 | **/dist/**
10 | **/node_modules
11 | **/temp
12 | **/docs/**
13 | tsconfig.json
14 | .next
15 | node_modules
16 | package-lock.json
17 | bunb.lock
18 | pnpm-lock.yaml
19 |
--------------------------------------------------------------------------------
/src/collections/(ecommerce)/Products/components/RowLabels/DetailLabel/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRowLabel } from "@payloadcms/ui";
4 |
5 | export const DetailLabel = () => {
6 | const { data } = useRowLabel<{
7 | title: string;
8 | }>();
9 |
10 | return {data?.title}
;
11 | };
12 |
--------------------------------------------------------------------------------
/src/collections/(ecommerce)/Products/components/RowLabels/OptionLabel/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRowLabel } from "@payloadcms/ui";
4 |
5 | export const OptionLabel = () => {
6 | const { data } = useRowLabel<{
7 | slug: string;
8 | }>();
9 |
10 | return {data.slug}
;
11 | };
12 |
--------------------------------------------------------------------------------
/src/fields/currencyField.ts:
--------------------------------------------------------------------------------
1 | import { type Field } from "payload";
2 |
3 | export const currencyField: Field = {
4 | name: "currency",
5 | type: "text",
6 | required: true,
7 | admin: {
8 | components: {
9 | Field: "@/components/(ecommerce)/CurrencySelect#CurrencySelect",
10 | },
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/src/collections/(ecommerce)/Products/components/RowLabels/VariantLabel/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRowLabel } from "@payloadcms/ui";
4 |
5 | export const VariantLabel = () => {
6 | const { data } = useRowLabel<{
7 | variantSlug: string;
8 | }>();
9 |
10 | return {data.variantSlug}
;
11 | };
12 |
--------------------------------------------------------------------------------
/src/fields/backgroundPicker.ts:
--------------------------------------------------------------------------------
1 | import { type Field } from "payload";
2 |
3 | export const backgroundPicker: Field = {
4 | name: "background",
5 | label: "Background",
6 | type: "text",
7 | admin: {
8 | components: {
9 | Field: "@/components/AdminColorPicker#AdminColorPicker",
10 | },
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/src/components/(ecommerce)/RowLabels/DeliveryZonesRowLabel/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRowLabel } from "@payloadcms/ui";
4 |
5 | export const DeliveryZonesRowLabel = () => {
6 | const { data } = useRowLabel<{
7 | countries?: string[];
8 | }>();
9 |
10 | return {data.countries?.join(", ")}
;
11 | };
12 |
--------------------------------------------------------------------------------
/src/access/authenticated.ts:
--------------------------------------------------------------------------------
1 | import type { Administrator } from "@/payload-types";
2 | import type { AccessArgs } from "payload";
3 |
4 | type isAuthenticated = (args: AccessArgs) => boolean;
5 |
6 | export const authenticated: isAuthenticated = ({ req: { user } }) => {
7 | return Boolean(user?.collection === "administrators");
8 | };
9 |
--------------------------------------------------------------------------------
/src/access/authenticatedOrPublished.ts:
--------------------------------------------------------------------------------
1 | import type { Access } from "payload";
2 |
3 | export const authenticatedOrPublished: Access = ({ req: { user } }) => {
4 | if (user?.collection === "administrators") {
5 | return true;
6 | }
7 |
8 | return {
9 | _status: {
10 | equals: "published",
11 | },
12 | };
13 | };
14 |
--------------------------------------------------------------------------------
/src/collections/(ecommerce)/Orders/components/couriers/CourierShipmentMenu.tsx:
--------------------------------------------------------------------------------
1 | import { type Order } from "@/payload-types";
2 |
3 | import { CourierShipmentMenuClient } from "./CourierShipmentMenu.client";
4 |
5 | export const CourierShipmentMenu = ({ data }: { data: Order }) => {
6 | return ;
7 | };
8 |
--------------------------------------------------------------------------------
/src/collections/(ecommerce)/Orders/components/inpost-pickup/PickupShipmentMenu.tsx:
--------------------------------------------------------------------------------
1 | import { type Order } from "@/payload-types";
2 |
3 | import { PickupShipmentMenuClient } from "./PickupShipmentMenu.client";
4 |
5 | export const PickupShipmentMenu = ({ data }: { data: Order }) => {
6 | return ;
7 | };
8 |
--------------------------------------------------------------------------------
/src/providers/Theme/types.ts:
--------------------------------------------------------------------------------
1 | export type Theme = "dark" | "light";
2 |
3 | export type ThemeContextType = {
4 | setTheme: (theme: Theme | null) => void;
5 | theme?: Theme | null;
6 | };
7 |
8 | export function themeIsValid(string: null | string): string is Theme {
9 | return string ? ["dark", "light"].includes(string) : false;
10 | }
11 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import createMiddleware from "next-intl/middleware";
2 |
3 | import { routing } from "./i18n/routing";
4 |
5 | export default createMiddleware(routing);
6 |
7 | export const config = {
8 | // Match only internationalized pathnames
9 | matcher: ["/", "/(pl|en)/:path*", "/((?!api|_next|next|admin|route|proxy|.*\\..*).*)"],
10 | };
11 |
--------------------------------------------------------------------------------
/src/app/(payload)/api/graphql-playground/route.ts:
--------------------------------------------------------------------------------
1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
3 | import config from '@payload-config'
4 | import '@payloadcms/next/css'
5 | import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
6 |
7 | export const GET = GRAPHQL_PLAYGROUND_GET(config)
8 |
--------------------------------------------------------------------------------
/src/app/(payload)/api/graphql/route.ts:
--------------------------------------------------------------------------------
1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
3 | import config from '@payload-config'
4 | import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
5 |
6 | export const POST = GRAPHQL_POST(config)
7 |
8 | export const OPTIONS = REST_OPTIONS(config)
9 |
--------------------------------------------------------------------------------
/src/blocks/Form/Width/index.tsx:
--------------------------------------------------------------------------------
1 | export const Width = ({
2 | children,
3 | className,
4 | width,
5 | }: {
6 | children: React.ReactNode;
7 | className?: string;
8 | width?: number | string;
9 | }) => {
10 | return (
11 |
12 | {children}
13 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/src/components/(ecommerce)/Cart/SynchronizeCart/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect } from "react";
4 |
5 | import { useCart } from "@/stores/CartStore";
6 |
7 | export const SynchronizeCart = () => {
8 | const { synchronizeCart } = useCart();
9 | useEffect(() => {
10 | void synchronizeCart();
11 | }, [synchronizeCart]);
12 | return <>>;
13 | };
14 |
--------------------------------------------------------------------------------
/src/components/(ecommerce)/RowLabels/PriceRowLabel/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRowLabel } from "@payloadcms/ui";
4 |
5 | export const PriceRowLabel = () => {
6 | const { data } = useRowLabel<{
7 | currency: string;
8 | value: string;
9 | }>();
10 |
11 | return (
12 |
13 | {data.value} {data.currency}
14 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/src/app/(frontend)/[locale]/(without-cart)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { type ReactNode } from "react";
2 |
3 | import { Header } from "@/globals/Header/Component";
4 |
5 | const WithoutCartLayout = ({ children }: { children: ReactNode }) => {
6 | return (
7 | <>
8 |
9 | {children}
10 | >
11 | );
12 | };
13 | export default WithoutCartLayout;
14 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_SERVER_URL=http://localhost:3000
2 |
3 | DATABASE_URI=mongodb://myUser:myPassword@213.76.110.18:27017/payload-ecomm-db
4 | PAYLOAD_SECRET=S0M3R4ND0M5TR1NG
5 |
6 | S3_BUCKET="payload-ecomm"
7 | S3_ENDPOINT="https://payload-ecomm.r2.cloudflarestorage.com"
8 | S3_ACCESS_KEY_ID="myAccessKey"
9 | S3_SECRET_ACCESS_KEY="mySecretKey"
10 |
11 | STRIPE_SECRET_KEY="myStripeSecretKey"
--------------------------------------------------------------------------------
/src/hooks/revalidateRedirects.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import { revalidateTag } from "next/cache";
3 |
4 | import type { CollectionAfterChangeHook } from "payload";
5 |
6 | export const revalidateRedirects: CollectionAfterChangeHook = ({ doc, req: { payload } }) => {
7 | payload.logger.info(`Revalidating redirects`);
8 |
9 | revalidateTag("redirects");
10 |
11 | return doc;
12 | };
13 |
--------------------------------------------------------------------------------
/src/fields/countryPickerField.ts:
--------------------------------------------------------------------------------
1 | import { type Field } from "payload";
2 |
3 | import { countryList } from "@/globals/(ecommerce)/Couriers/utils/countryList";
4 |
5 | export const countryPickerField: Field = {
6 | name: "countries",
7 | type: "select",
8 | label: {
9 | en: "Countries",
10 | pl: "Kraje",
11 | },
12 | hasMany: true,
13 | options: [...countryList],
14 | required: true,
15 | };
16 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "src/app/(frontend)/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/utilities/cn"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/app/(frontend)/[locale]/(without-cart)/checkout/page.tsx:
--------------------------------------------------------------------------------
1 | import { Checkout } from "@/globals/(ecommerce)/Layout/Checkout/Component";
2 | import { type Locale } from "@/i18n/config";
3 |
4 | export const dynamic = "force-dynamic";
5 |
6 | const CheckoutPage = async ({ params }: { params: Promise<{ locale: Locale }> }) => {
7 | const { locale } = await params;
8 | return ;
9 | };
10 | export default CheckoutPage;
11 |
--------------------------------------------------------------------------------
/src/blocks/Form/Message/index.tsx:
--------------------------------------------------------------------------------
1 | import { type SerializedEditorState } from "@payloadcms/richtext-lexical/lexical";
2 |
3 | import RichText from "@/components/RichText";
4 |
5 | import { Width } from "../Width";
6 |
7 | export const Message = ({ message }: { message: SerializedEditorState }) => {
8 | return (
9 |
10 | {message && }
11 |
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/src/components/BeforeDashboard/index.scss:
--------------------------------------------------------------------------------
1 | @import "~@payloadcms/ui/scss";
2 |
3 | .dashboard .before-dashboard {
4 | margin-bottom: base(1.5);
5 |
6 | &__banner {
7 | & h4 {
8 | margin: 0;
9 | }
10 | }
11 |
12 | &__instructions {
13 | list-style: decimal;
14 | margin-bottom: base(0.5);
15 |
16 | & li {
17 | width: 100%;
18 | }
19 | }
20 |
21 | & a:hover {
22 | opacity: 0.85;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/blocks/MediaBlock/config.ts:
--------------------------------------------------------------------------------
1 | import { marginFields, paddingFields } from "@/fields/spacingFields";
2 |
3 | import type { Block } from "payload";
4 |
5 | export const MediaBlock: Block = {
6 | slug: "mediaBlock",
7 | interfaceName: "MediaBlock",
8 | fields: [
9 | {
10 | name: "media",
11 | type: "upload",
12 | relationTo: "media",
13 | required: true,
14 | },
15 | marginFields,
16 | paddingFields,
17 | ],
18 | };
19 |
--------------------------------------------------------------------------------
/src/hooks/revalidateGlobal.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import { revalidateTag } from "next/cache";
3 |
4 | import type { GlobalAfterChangeHook } from "payload";
5 |
6 | export const revalidateGlobal: GlobalAfterChangeHook = ({ doc, req: { payload, context } }) => {
7 | if (!context.disableRevalidate) {
8 | payload.logger.info(`Revalidating ${doc.globalType}`);
9 |
10 | revalidateTag(`global_${doc.globalType}`);
11 | }
12 |
13 | return doc;
14 | };
15 |
--------------------------------------------------------------------------------
/src/hooks/populatePublishedAt.ts:
--------------------------------------------------------------------------------
1 | import type { CollectionBeforeChangeHook } from "payload";
2 |
3 | export const populatePublishedAt: CollectionBeforeChangeHook = ({ data, operation, req }) => {
4 | if (operation === "create" || operation === "update") {
5 | if (req.data && !req.data.publishedAt) {
6 | const now = new Date();
7 | return {
8 | ...data,
9 | publishedAt: now,
10 | };
11 | }
12 | }
13 |
14 | return data;
15 | };
16 |
--------------------------------------------------------------------------------
/src/utilities/useDebounce.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 |
3 | export function useDebounce(value: T, delay = 200): T {
4 | const [debouncedValue, setDebouncedValue] = useState(value);
5 |
6 | useEffect(() => {
7 | const handler = setTimeout(() => {
8 | setDebouncedValue(value);
9 | }, delay);
10 |
11 | return () => {
12 | clearTimeout(handler);
13 | };
14 | }, [value, delay]);
15 |
16 | return debouncedValue;
17 | }
18 |
--------------------------------------------------------------------------------
/src/app/(frontend)/[locale]/(with-cart)/account/help/page.tsx:
--------------------------------------------------------------------------------
1 | import { setRequestLocale } from "next-intl/server";
2 |
3 | import { ClientHelp } from "@/globals/(ecommerce)/Layout/ClientPanel/Help/Component";
4 | import { type Locale } from "@/i18n/config";
5 |
6 | const HelpPage = async ({ params }: { params: Promise<{ locale: Locale }> }) => {
7 | const { locale } = await params;
8 | setRequestLocale(locale);
9 | return ;
10 | };
11 | export default HelpPage;
12 |
--------------------------------------------------------------------------------
/src/globals/Footer/RowLabel.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useRowLabel } from "@payloadcms/ui";
3 |
4 | import { type Header } from "@/payload-types";
5 |
6 | export const RowLabel = () => {
7 | const data = useRowLabel[number]>();
8 |
9 | const label = data?.data?.link?.label
10 | ? `Nav item ${data.rowNumber !== undefined ? data.rowNumber + 1 : ""}: ${data?.data?.link?.label}`
11 | : "Row";
12 |
13 | return {label}
;
14 | };
15 |
--------------------------------------------------------------------------------
/src/globals/Header/RowLabel.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useRowLabel } from "@payloadcms/ui";
3 |
4 | import { type Header } from "@/payload-types";
5 |
6 | export const RowLabel = () => {
7 | const data = useRowLabel[number]>();
8 |
9 | const label = data?.data?.link?.label
10 | ? `Nav item ${data.rowNumber !== undefined ? data.rowNumber + 1 : ""}: ${data?.data?.link?.label}`
11 | : "Row";
12 |
13 | return {label}
;
14 | };
15 |
--------------------------------------------------------------------------------
/src/components/(ecommerce)/RowLabels/WeightRangeRowLabel/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRowLabel } from "@payloadcms/ui";
4 |
5 | export const WeightRangeRowLabel = () => {
6 | const { data } = useRowLabel<{
7 | weightFrom: number;
8 | weightTo: number;
9 | }>();
10 |
11 | return (
12 |
13 | {data.weightFrom}
14 | {(data.weightFrom || data.weightFrom === 0) && data.weightTo && " - "}
15 | {data.weightTo}
16 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/src/app/(frontend)/[locale]/(with-cart)/[slug]/page.client.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React, { useEffect } from "react";
3 |
4 | import { useHeaderTheme } from "@/providers/HeaderTheme";
5 |
6 | const PageClient = () => {
7 | /* Force the header to be dark mode while we have an image behind it */
8 | const { setHeaderTheme } = useHeaderTheme();
9 |
10 | useEffect(() => {
11 | setHeaderTheme("light");
12 | }, [setHeaderTheme]);
13 | return <>>;
14 | };
15 |
16 | export default PageClient;
17 |
--------------------------------------------------------------------------------
/src/app/(frontend)/[locale]/(with-cart)/account/page.tsx:
--------------------------------------------------------------------------------
1 | import { setRequestLocale } from "next-intl/server";
2 |
3 | import { type Locale } from "@/i18n/config";
4 | import { redirect } from "@/i18n/routing";
5 | export const dynamic = "force-dynamic";
6 |
7 | const Page = async ({ params }: { params: Promise<{ locale: Locale }> }) => {
8 | const { locale } = await params;
9 | setRequestLocale(locale);
10 |
11 | return redirect({ locale, href: "/account/orders" });
12 | };
13 |
14 | export default Page;
15 |
--------------------------------------------------------------------------------
/src/app/(frontend)/[locale]/(without-cart)/posts/page.client.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React, { useEffect } from "react";
3 |
4 | import { useHeaderTheme } from "@/providers/HeaderTheme";
5 |
6 | const PageClient = () => {
7 | /* Force the header to be dark mode while we have an image behind it */
8 | const { setHeaderTheme } = useHeaderTheme();
9 |
10 | useEffect(() => {
11 | setHeaderTheme("light");
12 | }, [setHeaderTheme]);
13 | return <>>;
14 | };
15 |
16 | export default PageClient;
17 |
--------------------------------------------------------------------------------
/src/components/LivePreviewListener/index.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | "use client";
3 | import { RefreshRouteOnSave as PayloadLivePreview } from "@payloadcms/live-preview-react";
4 |
5 | import { useRouter } from "@/i18n/routing";
6 |
7 | export const LivePreviewListener = () => {
8 | const router = useRouter();
9 | return (
10 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/src/app/(frontend)/[locale]/(without-cart)/postSearch/page.client.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React, { useEffect } from "react";
3 |
4 | import { useHeaderTheme } from "@/providers/HeaderTheme";
5 |
6 | const PageClient = () => {
7 | /* Force the header to be dark mode while we have an image behind it */
8 | const { setHeaderTheme } = useHeaderTheme();
9 |
10 | useEffect(() => {
11 | setHeaderTheme("light");
12 | }, [setHeaderTheme]);
13 | return <>>;
14 | };
15 |
16 | export default PageClient;
17 |
--------------------------------------------------------------------------------
/src/app/(frontend)/[locale]/(without-cart)/posts/[slug]/page.client.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React, { useEffect } from "react";
3 |
4 | import { useHeaderTheme } from "@/providers/HeaderTheme";
5 |
6 | const PageClient = () => {
7 | /* Force the header to be dark mode while we have an image behind it */
8 | const { setHeaderTheme } = useHeaderTheme();
9 |
10 | useEffect(() => {
11 | setHeaderTheme("dark");
12 | }, [setHeaderTheme]);
13 | return <>>;
14 | };
15 |
16 | export default PageClient;
17 |
--------------------------------------------------------------------------------
/src/app/(frontend)/[locale]/(without-cart)/posts/page/[pageNumber]/page.client.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React, { useEffect } from "react";
3 |
4 | import { useHeaderTheme } from "@/providers/HeaderTheme";
5 |
6 | const PageClient = () => {
7 | /* Force the header to be dark mode while we have an image behind it */
8 | const { setHeaderTheme } = useHeaderTheme();
9 |
10 | useEffect(() => {
11 | setHeaderTheme("light");
12 | }, [setHeaderTheme]);
13 | return <>>;
14 | };
15 |
16 | export default PageClient;
17 |
--------------------------------------------------------------------------------
/src/fields/alignmentField.ts:
--------------------------------------------------------------------------------
1 | import { type Field } from "payload";
2 |
3 | export const AlignmentField: Field = {
4 | name: "alignment",
5 | label: "Alignment",
6 | type: "select",
7 | defaultValue: "center",
8 | options: [
9 | {
10 | label: "Center",
11 | value: "center",
12 | },
13 | {
14 | label: "Left",
15 | value: "left",
16 | },
17 | {
18 | label: "Right",
19 | value: "right",
20 | },
21 | { label: "Full width", value: "full" },
22 | ],
23 | };
24 |
--------------------------------------------------------------------------------
/src/globals/(ecommerce)/Layout/ProductList/variants/filters/None.tsx:
--------------------------------------------------------------------------------
1 | import { type ReactNode } from "react";
2 |
3 | export const None = ({ title, children }: { title: string; children: ReactNode }) => {
4 | return (
5 |
6 | {title}
7 |
8 | {children}
9 |
10 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/src/schemas/loginForm.schema.ts:
--------------------------------------------------------------------------------
1 | import { useTranslations } from "next-intl";
2 | import { z } from "zod";
3 |
4 | export type LoginFormData = {
5 | email: string;
6 | password: string;
7 | };
8 |
9 | export const useLoginFormSchema = () => {
10 | const t = useTranslations("LoginForm.errors");
11 |
12 | const LoginFormSchema = z.object({
13 | email: z.string().nonempty(t("email-empty")).email(t("email")),
14 | password: z.string().nonempty(t("password")),
15 | });
16 |
17 | return { LoginFormSchema };
18 | };
19 |
--------------------------------------------------------------------------------
/src/app/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Ecommerce",
3 | "short_name": "Ecomm",
4 | "icons": [
5 | {
6 | "src": "/web-app-manifest-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png",
9 | "purpose": "maskable"
10 | },
11 | {
12 | "src": "/web-app-manifest-512x512.png",
13 | "sizes": "512x512",
14 | "type": "image/png",
15 | "purpose": "maskable"
16 | }
17 | ],
18 | "theme_color": "#ffffff",
19 | "background_color": "#ffffff",
20 | "display": "standalone"
21 | }
--------------------------------------------------------------------------------
/src/components/(ecommerce)/RowLabels/OrderProductsRowLabel/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRowLabel } from "@payloadcms/ui";
4 |
5 | export const OrderProductsRowLabel = () => {
6 | const { data } = useRowLabel<{
7 | productName?: string;
8 | color?: string;
9 | size?: string;
10 | quantity: number;
11 | }>();
12 |
13 | const label = [data.productName, data.color, data.size].filter(Boolean).join(", ");
14 |
15 | return (
16 |
17 | {label} x {data.quantity}
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/providers/Theme/shared.ts:
--------------------------------------------------------------------------------
1 | import type { Theme } from "./types";
2 |
3 | export const themeLocalStorageKey = "payload-theme";
4 |
5 | export const defaultTheme = "light";
6 |
7 | export const getImplicitPreference = (): Theme | null => {
8 | const mediaQuery = "(prefers-color-scheme: dark)";
9 | const mql = window.matchMedia(mediaQuery);
10 | const hasImplicitPreference = typeof mql.matches === "boolean";
11 |
12 | if (hasImplicitPreference) {
13 | return mql.matches ? "dark" : "light";
14 | }
15 |
16 | return null;
17 | };
18 |
--------------------------------------------------------------------------------
/src/utilities/formatPrices.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Function that formats a price using Intl.NumberFormat
3 | * @param price - Price to format
4 | * @param currency - Currency to format
5 | * @param locale - Locale to format
6 | * @returns - Formatted price
7 | */
8 | export const formatPrice = (price: number, currency: string, locale: string) => {
9 | const formattedPrice = new Intl.NumberFormat(`${locale}-${locale.toUpperCase()}`, {
10 | style: "currency",
11 | currency: currency,
12 | }).format(price);
13 | return formattedPrice;
14 | };
15 |
--------------------------------------------------------------------------------
/src/blocks/Code/Component.tsx:
--------------------------------------------------------------------------------
1 | import { Code } from "./Component.client";
2 |
3 | export type CodeBlockProps = {
4 | code: string;
5 | language?: string;
6 | blockType: "code";
7 | };
8 |
9 | type Props = CodeBlockProps & {
10 | className?: string;
11 | };
12 |
13 | export const CodeBlock = ({ className, code, language }: Props) => {
14 | return (
15 |
16 |
17 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/src/components/AdminNavbar/NavHamburger/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { Hamburger, useNav } from "@payloadcms/ui";
3 |
4 | export const NavHamburger = ({ baseClass }: { baseClass?: string }) => {
5 | const { navOpen, setNavOpen } = useNav();
6 |
7 | return (
8 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/redirects.js:
--------------------------------------------------------------------------------
1 | const redirects = async () => {
2 | const internetExplorerRedirect = {
3 | destination: "/ie-incompatible.html",
4 | has: [
5 | {
6 | type: "header",
7 | key: "user-agent",
8 | value: "(.*Trident.*)", // all ie browsers
9 | },
10 | ],
11 | permanent: false,
12 | source: "/:path((?!ie-incompatible.html$).*)", // all pages except the incompatibility page
13 | };
14 |
15 | const redirects = [internetExplorerRedirect];
16 |
17 | return redirects;
18 | };
19 |
20 | export default redirects;
21 |
--------------------------------------------------------------------------------
/src/app/(frontend)/not-found.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import React from "react";
3 |
4 | import { Button } from "@/components/ui/button";
5 |
6 | export default function NotFound() {
7 | return (
8 |
9 |
10 |
404
11 |
This page could not be found.
12 |
13 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/collections/(ecommerce)/Customers/ui/RowLabels/ShippingAddressRowLabel/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRowLabel } from "@payloadcms/ui";
4 |
5 | export const ShippingAddressRowLabel = () => {
6 | const { data } = useRowLabel<{
7 | name?: string;
8 | address?: string;
9 | city?: string;
10 | postalCode?: string;
11 | phone?: string;
12 | email?: string;
13 | }>();
14 |
15 | return (
16 |
17 | {data?.name}, {data?.address} {data?.postalCode} {data?.city} | {data?.phone}, {data?.email}
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/components/ui/AdminInput.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | export type InputProps = {} & React.InputHTMLAttributes;
4 |
5 | const AdminInput = React.forwardRef(({ type, className, ...props }, ref) => {
6 | return (
7 |
12 | );
13 | });
14 |
15 | AdminInput.displayName = "AdminInput";
16 |
17 | export { AdminInput };
18 |
--------------------------------------------------------------------------------
/src/globals/(ecommerce)/Layout/ProductList/variants/filters/WithSidebar/stores/MobileFiltersContext.tsx:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 |
3 | type MobileFiltersState = {
4 | mobileFiltersOpen: boolean;
5 | setMobileFiltersOpen: (open: boolean) => void;
6 | };
7 |
8 | const useMobileFiltersStore = create((set) => ({
9 | mobileFiltersOpen: false, // domyślna wartość
10 | setMobileFiltersOpen: (open: boolean) => set({ mobileFiltersOpen: open }),
11 | }));
12 |
13 | export const useMobileFilters = () => {
14 | return useMobileFiltersStore();
15 | };
16 |
--------------------------------------------------------------------------------
/src/app/(frontend)/[locale]/not-found.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import React from "react";
3 |
4 | import { Button } from "@/components/ui/button";
5 |
6 | export default function NotFound() {
7 | return (
8 |
9 |
10 |
404
11 |
This page could not be found.
12 |
13 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/i18n/routing.ts:
--------------------------------------------------------------------------------
1 | import { createNavigation } from "next-intl/navigation";
2 | import { defineRouting } from "next-intl/routing";
3 |
4 | export const routing = defineRouting({
5 | // A list of all locales that are supported
6 | locales: ["en", "pl"],
7 |
8 | // Used when no locale matches
9 | defaultLocale: "en",
10 |
11 | localeDetection: true,
12 | });
13 |
14 | // Lightweight wrappers around Next.js' navigation APIs
15 | // that will consider the routing configuration
16 | export const { Link, redirect, usePathname, useRouter, getPathname } = createNavigation(routing);
17 |
--------------------------------------------------------------------------------
/src/stores/CartStateStore/index.tsx:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 |
3 | type CartState = {
4 | isOpen: boolean;
5 | toggleCart: () => void;
6 | setCartState: (isOpen: boolean) => void;
7 | };
8 |
9 | const useCartStateStore = create((set) => ({
10 | isOpen: false,
11 | toggleCart: () => set((state) => ({ isOpen: !state.isOpen })),
12 | setCartState: (isOpen) => set({ isOpen }),
13 | }));
14 |
15 | export const useCartState = () => {
16 | const { isOpen, toggleCart, setCartState } = useCartStateStore();
17 |
18 | return { isOpen, toggleCart, setCartState };
19 | };
20 |
--------------------------------------------------------------------------------
/src/environment.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import type en from "../translations/en.json";
3 |
4 | type Messages = typeof en;
5 |
6 | declare global {
7 | namespace NodeJS {
8 | interface ProcessEnv {
9 | PAYLOAD_SECRET: string;
10 | DATABASE_URI: string;
11 | NEXT_PUBLIC_SERVER_URL: string;
12 | VERCEL_PROJECT_PRODUCTION_URL: string;
13 | }
14 | }
15 | interface IntlMessages extends Messages {}
16 | }
17 |
18 | // If this file has no import/export statements (i.e. is a script)
19 | // convert it into a module by adding an empty export statement.
20 | export {};
21 |
--------------------------------------------------------------------------------
/src/globals/Header/Component.tsx:
--------------------------------------------------------------------------------
1 | import { getLocale } from "next-intl/server";
2 |
3 | import { type Locale } from "@/i18n/config";
4 | import { getCachedGlobal } from "@/utilities/getGlobals";
5 |
6 | import { HeaderClient } from "./Component.client";
7 |
8 | import type { Header } from "@/payload-types";
9 |
10 | export async function Header({ disableCart }: { disableCart?: boolean }) {
11 | const locale = (await getLocale()) as Locale;
12 | const headerData: Header = await getCachedGlobal("header", locale, 1)();
13 |
14 | return ;
15 | }
16 |
--------------------------------------------------------------------------------
/src/i18n/request.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import { getRequestConfig } from "next-intl/server";
3 |
4 | import { routing } from "./routing";
5 |
6 | export default getRequestConfig(async ({ requestLocale }) => {
7 | // This typically corresponds to the `[locale]` segment
8 | let locale = await requestLocale;
9 |
10 | // Ensure that a valid locale is used
11 | if (!locale || !routing.locales.includes(locale as any)) {
12 | locale = routing.defaultLocale;
13 | }
14 |
15 | return {
16 | locale,
17 | messages: (await import(`../../translations/${locale}.json`)).default,
18 | };
19 | });
20 |
--------------------------------------------------------------------------------
/next-sitemap.config.cjs:
--------------------------------------------------------------------------------
1 | const SITE_URL =
2 | process.env.NEXT_PUBLIC_SERVER_URL || process.env.VERCEL_PROJECT_PRODUCTION_URL || "https://example.com";
3 |
4 | /** @type {import('next-sitemap').IConfig} */
5 | module.exports = {
6 | siteUrl: SITE_URL,
7 | generateRobotsTxt: true,
8 | exclude: ["/posts-sitemap.xml", "/pages-sitemap.xml", "/*", "/posts/*"],
9 | robotsTxtOptions: {
10 | policies: [
11 | {
12 | userAgent: "*",
13 | disallow: "/admin/*",
14 | },
15 | ],
16 | additionalSitemaps: [`${SITE_URL}/pages-sitemap.xml`, `${SITE_URL}/posts-sitemap.xml`],
17 | },
18 | };
19 |
--------------------------------------------------------------------------------
/src/app/(frontend)/[locale]/(with-cart)/account/settings/page.tsx:
--------------------------------------------------------------------------------
1 | import { setRequestLocale } from "next-intl/server";
2 |
3 | import { Settings } from "@/globals/(ecommerce)/Layout/ClientPanel/Settings";
4 | import { type Locale } from "@/i18n/config";
5 | import { getCustomer } from "@/utilities/getCustomer";
6 |
7 | const SettingsPage = async ({ params }: { params: Promise<{ locale: Locale }> }) => {
8 | const user = await getCustomer();
9 | const { locale } = await params;
10 | setRequestLocale(locale);
11 | if (!user) return null;
12 | return ;
13 | };
14 | export default SettingsPage;
15 |
--------------------------------------------------------------------------------
/src/blocks/Form/fields.tsx:
--------------------------------------------------------------------------------
1 | import { Checkbox } from "./Checkbox";
2 | import { Country } from "./Country";
3 | import { Email } from "./Email";
4 | import { Message } from "./Message";
5 | import { Number } from "./Number";
6 | import { Select } from "./Select";
7 | import { State } from "./State";
8 | import { Text } from "./Text";
9 | import { Textarea } from "./Textarea";
10 |
11 | export const fields = {
12 | checkbox: Checkbox,
13 | country: Country,
14 | email: Email,
15 | message: Message,
16 | number: Number,
17 | select: Select,
18 | state: State,
19 | text: Text,
20 | textarea: Textarea,
21 | };
22 |
--------------------------------------------------------------------------------
/src/fields/courierSettingsFields.ts:
--------------------------------------------------------------------------------
1 | import { type Field } from "payload";
2 |
3 | export const courierSettingsFields: Field[] = [
4 | { name: "label", type: "text", label: { en: "Label", pl: "Etykieta" }, localized: true, required: true },
5 | {
6 | name: "description",
7 | type: "text",
8 | label: { en: "Short description", pl: "Krótki opis" },
9 | localized: true,
10 | admin: {
11 | description: {
12 | en: "You can provide typical delivery time or any other information",
13 | pl: "Możesz podać typowy czas dostawy lub inne informacje",
14 | },
15 | },
16 | },
17 | ];
18 |
--------------------------------------------------------------------------------
/src/globals/(ecommerce)/Layout/ProductList/variants/filters/WithSidebar/components/MobileFiltersDialog.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Dialog } from "@headlessui/react";
4 | import { type ReactNode } from "react";
5 |
6 | import { useMobileFilters } from "../stores/MobileFiltersContext";
7 |
8 | export const MobileFiltersDialog = ({ children }: { children: ReactNode }) => {
9 | const { mobileFiltersOpen, setMobileFiltersOpen } = useMobileFilters();
10 | return (
11 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/src/stores/WishListStateStore/index.tsx:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 |
3 | type WishListState = {
4 | isOpen: boolean;
5 | toggleWishList: () => void;
6 | setWishListState: (isOpen: boolean) => void;
7 | };
8 |
9 | const useWishListStateStore = create((set) => ({
10 | isOpen: false,
11 | toggleWishList: () => set((state) => ({ isOpen: !state.isOpen })),
12 | setWishListState: (isOpen) => set({ isOpen }),
13 | }));
14 |
15 | export const useWishListState = () => {
16 | const { isOpen, toggleWishList, setWishListState } = useWishListStateStore();
17 |
18 | return { isOpen, toggleWishList, setWishListState };
19 | };
20 |
--------------------------------------------------------------------------------
/src/app/(frontend)/[locale]/(with-cart)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { type ReactNode } from "react";
2 |
3 | import { SynchronizeCart } from "@/components/(ecommerce)/Cart/SynchronizeCart";
4 | import { Cart } from "@/globals/(ecommerce)/Layout/Cart/Component";
5 | import { WishList } from "@/globals/(ecommerce)/Layout/WishList/Component";
6 | import { Header } from "@/globals/Header/Component";
7 |
8 | const CartLayout = ({ children }: { children: ReactNode }) => {
9 | return (
10 | <>
11 |
12 |
13 |
14 |
15 | {children}
16 | >
17 | );
18 | };
19 | export default CartLayout;
20 |
--------------------------------------------------------------------------------
/src/app/(payload)/api/[...slug]/route.ts:
--------------------------------------------------------------------------------
1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
3 | import config from '@payload-config'
4 | import '@payloadcms/next/css'
5 | import {
6 | REST_DELETE,
7 | REST_GET,
8 | REST_OPTIONS,
9 | REST_PATCH,
10 | REST_POST,
11 | REST_PUT,
12 | } from '@payloadcms/next/routes'
13 |
14 | export const GET = REST_GET(config)
15 | export const POST = REST_POST(config)
16 | export const DELETE = REST_DELETE(config)
17 | export const PATCH = REST_PATCH(config)
18 | export const PUT = REST_PUT(config)
19 | export const OPTIONS = REST_OPTIONS(config)
20 |
--------------------------------------------------------------------------------
/src/components/AdminColorPicker/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { FieldLabel, useField } from "@payloadcms/ui";
4 | import { type TextFieldClientComponent } from "payload";
5 |
6 | import { ColorPicker } from "../ui/colorPicker";
7 |
8 | export const AdminColorPicker: TextFieldClientComponent = ({ path, field }) => {
9 | const { value, setValue } = useField<{ value: string | undefined }>({ path });
10 |
11 | return (
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/src/globals/(ecommerce)/Layout/ClientPanel/Help/Component.tsx:
--------------------------------------------------------------------------------
1 | import { getLocale } from "next-intl/server";
2 |
3 | import RichText from "@/components/RichText";
4 | import { type Locale } from "@/i18n/config";
5 | import { getCachedGlobal } from "@/utilities/getGlobals";
6 |
7 | export const ClientHelp = async () => {
8 | const locale = (await getLocale()) as Locale;
9 | const { clientPanel } = await getCachedGlobal("shopLayout", locale, 1)();
10 | return (
11 |
12 |
{clientPanel.help?.title}
13 |
14 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/src/hooks/use-mobile.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | const MOBILE_BREAKPOINT = 768;
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(undefined);
7 |
8 | React.useEffect(() => {
9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
10 | const onChange = () => {
11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
12 | };
13 | mql.addEventListener("change", onChange);
14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
15 | return () => mql.removeEventListener("change", onChange);
16 | }, []);
17 |
18 | return !!isMobile;
19 | }
20 |
--------------------------------------------------------------------------------
/src/providers/index.tsx:
--------------------------------------------------------------------------------
1 | // import { getLocale } from "next-intl/server";
2 | import { type ReactNode } from "react";
3 |
4 | // import { HeaderThemeProvider } from "./HeaderTheme";
5 | // import { ThemeProvider } from "./Theme";
6 |
7 | // import { type Locale } from "@/i18n/config";
8 | // import { type ShopSetting } from "@/payload-types";
9 | // import { getCachedGlobal } from "@/utilities/getGlobals";
10 |
11 | export const Providers = async ({ children }: { children: ReactNode }) => {
12 | // const locale = (await getLocale()) as Locale;
13 | // const shopSettings: ShopSetting = await getCachedGlobal("shopSettings", locale, 1)();
14 | return children;
15 | };
16 |
--------------------------------------------------------------------------------
/src/components/(ecommerce)/AdminDashboard/components/views/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useSearchParams } from "next/navigation";
4 | import { type ReactNode } from "react";
5 |
6 | import { Overview } from "./Overview";
7 |
8 | export const AdminViews = () => {
9 | const searchParams = useSearchParams();
10 | const view = searchParams.get("view");
11 |
12 | let ActiveTabComponent: ReactNode | null = null;
13 |
14 | switch (view) {
15 | case "overview": {
16 | ActiveTabComponent = ;
17 | break;
18 | }
19 | default: {
20 | ActiveTabComponent = ;
21 | }
22 | }
23 | return ActiveTabComponent;
24 | };
25 |
--------------------------------------------------------------------------------
/src/components/(ecommerce)/PriceClient/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useLocale } from "next-intl";
3 |
4 | import { useCurrency } from "@/stores/Currency";
5 | import { formatPrice } from "@/utilities/formatPrices";
6 |
7 | export const PriceClient = ({
8 | pricing,
9 | }: {
10 | pricing: {
11 | value: number;
12 | currency: string;
13 | }[];
14 | }) => {
15 | const { currency } = useCurrency();
16 | const locale = useLocale();
17 | const price =
18 | pricing.length > 0
19 | ? (pricing.find((price) => price.currency === currency)?.value ?? pricing[0].value)
20 | : 0;
21 |
22 | return <>{formatPrice(price, currency, locale)}>;
23 | };
24 |
--------------------------------------------------------------------------------
/src/collections/(ecommerce)/Orders/components/VariantSelect/index.tsx:
--------------------------------------------------------------------------------
1 | import { FieldLabel } from "@payloadcms/ui";
2 | import { type TextFieldServerComponent } from "payload";
3 |
4 | import { VariantSelectClient } from "./VariantSelect.client";
5 |
6 | export type VariantsArr = {
7 | label: string | null | undefined;
8 | value: string | null | undefined;
9 | }[];
10 |
11 | export const VariantSelect: TextFieldServerComponent = async ({ path }) => {
12 | return (
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/src/globals/(ecommerce)/Layout/Cart/Component.tsx:
--------------------------------------------------------------------------------
1 | import { getLocale } from "next-intl/server";
2 | import { type ReactNode } from "react";
3 |
4 | import { type Locale } from "@/i18n/config";
5 | import { getCachedGlobal } from "@/utilities/getGlobals";
6 |
7 | import { SlideOver } from "./variants/SlideOver";
8 |
9 | export const Cart = async () => {
10 | const locale = (await getLocale()) as Locale;
11 | const { cartAndWishlist } = await getCachedGlobal("shopLayout", locale, 1)();
12 |
13 | let CartComponent: ReactNode = null;
14 | switch (cartAndWishlist.type) {
15 | case "slideOver":
16 | CartComponent = ;
17 | break;
18 | }
19 |
20 | return CartComponent;
21 | };
22 |
--------------------------------------------------------------------------------
/src/components/heros/LowImpact/index.tsx:
--------------------------------------------------------------------------------
1 | import RichText from "@/components/RichText";
2 |
3 | import type { Page } from "@/payload-types";
4 |
5 | type LowImpactHeroType =
6 | | {
7 | children?: React.ReactNode;
8 | richText?: never;
9 | }
10 | | (Omit & {
11 | children?: never;
12 | richText?: Page["hero"]["richText"];
13 | });
14 |
15 | export const LowImpactHero = ({ children, richText }: LowImpactHeroType) => {
16 | return (
17 |
18 |
19 | {children ?? (richText && )}
20 |
21 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/src/utilities/mergeOpenGraph.ts:
--------------------------------------------------------------------------------
1 | import { getServerSideURL } from "./getURL";
2 |
3 | import type { Metadata } from "next";
4 |
5 | const defaultOpenGraph: Metadata["openGraph"] = {
6 | type: "website",
7 | description: "An open-source website built with Payload and Next.js.",
8 | images: [
9 | {
10 | url: `${getServerSideURL()}/website-template-OG.webp`,
11 | },
12 | ],
13 | siteName: "Payload Ecommerce Template",
14 | title: "Payload Ecommerce Template",
15 | };
16 |
17 | export const mergeOpenGraph = (og?: Metadata["openGraph"]): Metadata["openGraph"] => {
18 | return {
19 | ...defaultOpenGraph,
20 | ...og,
21 | images: og?.images ?? defaultOpenGraph.images,
22 | };
23 | };
24 |
--------------------------------------------------------------------------------
/src/app/(frontend)/[locale]/reset-password/page.tsx:
--------------------------------------------------------------------------------
1 | import { ResetPasswordForm } from "@/components/ResetPasswordForm";
2 | import { redirect } from "@/i18n/routing";
3 |
4 | export const dynamic = "force-dynamic";
5 |
6 | const ResetPassword = async ({
7 | params,
8 | searchParams,
9 | }: {
10 | params: Promise<{ locale: string }>;
11 | searchParams: Promise<{ token?: string; collection: string }>;
12 | }) => {
13 | const { token, collection } = await searchParams;
14 | const { locale } = await params;
15 | if (!token) {
16 | return redirect({ href: "/", locale });
17 | }
18 | return ;
19 | };
20 | export default ResetPassword;
21 |
--------------------------------------------------------------------------------
/src/components/heros/RenderHero.tsx:
--------------------------------------------------------------------------------
1 | import { HighImpactHero } from "@/components/heros/HighImpact";
2 | import { LowImpactHero } from "@/components/heros/LowImpact";
3 | import { MediumImpactHero } from "@/components/heros/MediumImpact";
4 |
5 | import type { Page } from "@/payload-types";
6 |
7 | const heroes = {
8 | highImpact: HighImpactHero,
9 | lowImpact: LowImpactHero,
10 | mediumImpact: MediumImpactHero,
11 | };
12 |
13 | export const RenderHero = (props: Page["hero"]) => {
14 | const { type } = props || {};
15 |
16 | if (!type || type === "none") return null;
17 |
18 | const HeroToRender = heroes[type];
19 |
20 | if (!HeroToRender) return null;
21 |
22 | return ;
23 | };
24 |
--------------------------------------------------------------------------------
/src/globals/(ecommerce)/Layout/WishList/Component.tsx:
--------------------------------------------------------------------------------
1 | import { getLocale } from "next-intl/server";
2 | import { type ReactNode } from "react";
3 |
4 | import { type Locale } from "@/i18n/config";
5 | import { getCachedGlobal } from "@/utilities/getGlobals";
6 |
7 | import { SlideOver } from "./variants/SlideOver";
8 |
9 | export const WishList = async () => {
10 | const locale = (await getLocale()) as Locale;
11 | const { cartAndWishlist } = await getCachedGlobal("shopLayout", locale, 1)();
12 |
13 | let WishListComponent: ReactNode = null;
14 | switch (cartAndWishlist.type) {
15 | case "slideOver":
16 | WishListComponent = ;
17 | break;
18 | }
19 |
20 | return WishListComponent;
21 | };
22 |
--------------------------------------------------------------------------------
/src/fields/linkGroup.ts:
--------------------------------------------------------------------------------
1 | import deepMerge from "@/utilities/deepMerge";
2 |
3 | import { link, type LinkAppearances } from "./link";
4 |
5 | import type { ArrayField, Field } from "payload";
6 |
7 | type LinkGroupType = (options?: {
8 | appearances?: LinkAppearances[] | false;
9 | overrides?: Partial;
10 | }) => Field;
11 |
12 | export const linkGroup: LinkGroupType = ({ appearances, overrides = {} } = {}) => {
13 | const generatedLinkGroup: Field = {
14 | name: "links",
15 | type: "array",
16 | fields: [
17 | link({
18 | appearances,
19 | }),
20 | ],
21 | admin: {
22 | initCollapsed: true,
23 | },
24 | };
25 |
26 | return deepMerge(generatedLinkGroup, overrides);
27 | };
28 |
--------------------------------------------------------------------------------
/src/components/Emails/OrderStatusEmail/index.tsx:
--------------------------------------------------------------------------------
1 | import { Html } from "@react-email/components";
2 | import { type ReactNode } from "react";
3 |
4 | import { type Locale } from "@/i18n/config";
5 | import { type Order } from "@/payload-types";
6 | import { getCachedGlobal } from "@/utilities/getGlobals";
7 |
8 | import { Default } from "./variants/Default";
9 |
10 | export const OrderStatusEmail = async ({ order, locale }: { order: Order; locale: Locale }) => {
11 | const { messages } = await getCachedGlobal("emailMessages", locale, 1)();
12 |
13 | let Email: ReactNode = ;
14 |
15 | switch (messages.template) {
16 | case "default":
17 | Email = ;
18 | }
19 | return Email;
20 | };
21 |
--------------------------------------------------------------------------------
/src/fields/slug/formatSlug.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import type { FieldHook } from "payload";
3 |
4 | export const formatSlug = (val: string): string =>
5 | val
6 | .replace(/ /g, "-")
7 | .replace(/[^\w-]+/g, "")
8 | .toLowerCase();
9 |
10 | export const formatSlugHook =
11 | (fallback: string): FieldHook =>
12 | ({ data, operation, value }) => {
13 | if (typeof value === "string") {
14 | return formatSlug(value);
15 | }
16 |
17 | if (operation === "create" || !data?.slug) {
18 | const fallbackData = data?.[fallback] || data?.[fallback];
19 |
20 | if (fallbackData && typeof fallbackData === "string") {
21 | return formatSlug(fallbackData);
22 | }
23 | }
24 |
25 | return value;
26 | };
27 |
--------------------------------------------------------------------------------
/src/components/Emails/WelcomeEmail/index.tsx:
--------------------------------------------------------------------------------
1 | import { Html } from "@react-email/components";
2 | import { type ReactNode } from "react";
3 |
4 | import { type Locale } from "@/i18n/config";
5 | import { type Customer } from "@/payload-types";
6 | import { getCachedGlobal } from "@/utilities/getGlobals";
7 |
8 | import { Default } from "./variants/Default";
9 |
10 | export const WelcomeEmail = async ({ customer, locale }: { customer: Customer; locale: Locale }) => {
11 | const { messages } = await getCachedGlobal("emailMessages", locale, 1)();
12 |
13 | let Email: ReactNode = ;
14 |
15 | switch (messages.template) {
16 | case "default":
17 | Email = ;
18 | }
19 | return Email;
20 | };
21 |
--------------------------------------------------------------------------------
/src/hooks/formatSlug.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import type { FieldHook } from "payload";
3 |
4 | const format = (val: string): string =>
5 | val
6 | .replace(/ /g, "-")
7 | .replace(/[^\w-]+/g, "")
8 | .toLowerCase();
9 |
10 | const formatSlug =
11 | (fallback: string): FieldHook =>
12 | ({ data, operation, originalDoc, value }) => {
13 | if (typeof value === "string") {
14 | return format(value);
15 | }
16 |
17 | if (operation === "create") {
18 | const fallbackData = data?.[fallback] || originalDoc?.[fallback];
19 |
20 | if (fallbackData && typeof fallbackData === "string") {
21 | return format(fallbackData);
22 | }
23 | }
24 |
25 | return value;
26 | };
27 |
28 | export default formatSlug;
29 |
--------------------------------------------------------------------------------
/src/blocks/Code/config.ts:
--------------------------------------------------------------------------------
1 | import type { Block } from "payload";
2 |
3 | export const Code: Block = {
4 | slug: "code",
5 | interfaceName: "CodeBlock",
6 | fields: [
7 | {
8 | name: "language",
9 | type: "select",
10 | defaultValue: "typescript",
11 | options: [
12 | {
13 | label: "Typescript",
14 | value: "typescript",
15 | },
16 | {
17 | label: "Javascript",
18 | value: "javascript",
19 | },
20 | {
21 | label: "CSS",
22 | value: "css",
23 | },
24 | ],
25 | },
26 | {
27 | name: "code",
28 | type: "code",
29 | label: false,
30 | required: true,
31 | localized: true,
32 | },
33 | ],
34 | };
35 |
--------------------------------------------------------------------------------
/src/components/Media/index.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import React, { Fragment } from "react";
3 |
4 | import { ImageMedia } from "./ImageMedia";
5 | import { VideoMedia } from "./VideoMedia";
6 |
7 | import type { Props } from "./types";
8 |
9 | export const Media = (props: Props) => {
10 | const { className, htmlElement = "div", resource } = props;
11 |
12 | const isVideo = typeof resource === "object" && resource?.mimeType?.includes("video");
13 | const Tag = (htmlElement as any) || Fragment;
14 |
15 | return (
16 |
23 | {isVideo ? : }
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/src/globals/(ecommerce)/Layout/ProductDetails/types/index.ts:
--------------------------------------------------------------------------------
1 | import { type Media } from "@/payload-types";
2 |
3 | export type FilledVariant = {
4 | color:
5 | | {
6 | label: string;
7 | slug: string;
8 | colorValue?: string | null;
9 | id?: string | null;
10 | }
11 | | undefined;
12 | size:
13 | | {
14 | label: string;
15 | slug: string;
16 | id?: string | null;
17 | }
18 | | undefined;
19 | slug: string | null | undefined;
20 | stock: number;
21 | image: Media | null | undefined;
22 | pricing:
23 | | {
24 | value: number;
25 | currency: "USD" | "EUR" | "GBP" | "PLN";
26 | id?: string | null;
27 | }[]
28 | | null
29 | | undefined;
30 | };
31 |
--------------------------------------------------------------------------------
/src/globals/(ecommerce)/Layout/Checkout/Component.tsx:
--------------------------------------------------------------------------------
1 | import { notFound } from "next/navigation";
2 | import { type ReactNode } from "react";
3 |
4 | import { type Locale } from "@/i18n/config";
5 | import { getCachedGlobal } from "@/utilities/getGlobals";
6 |
7 | import { OneStepWithSummary } from "./variants/OneStepWithSummary";
8 |
9 | export const Checkout = async ({ locale }: { locale: Locale }) => {
10 | const { checkout } = await getCachedGlobal("shopLayout", locale, 1)();
11 |
12 | let CheckoutComponent: ReactNode = null;
13 | switch (checkout.type) {
14 | case "OneStepWithSummary":
15 | CheckoutComponent = ;
16 | break;
17 | }
18 |
19 | if (!CheckoutComponent) {
20 | notFound();
21 | }
22 |
23 | return CheckoutComponent;
24 | };
25 |
--------------------------------------------------------------------------------
/src/lib/getTotalWeight.ts:
--------------------------------------------------------------------------------
1 | import { type Cart } from "@/stores/CartStore/types";
2 |
3 | import { type FilledProduct } from "./getFilledProducts";
4 |
5 | export const getTotalWeight = (filledProducts: FilledProduct[], cart: Cart) =>
6 | filledProducts.reduce((acc, product) => {
7 | if (product.enableVariantWeights && product.variants) {
8 | const variantWeight = product.variants
9 | .filter((variant) =>
10 | cart.some(
11 | (cartProduct) =>
12 | cartProduct.id === product.id && cartProduct.choosenVariantSlug === variant.variantSlug,
13 | ),
14 | )
15 | .reduce((varAcc, variant) => varAcc + (variant.weight ?? 0), 0);
16 |
17 | return acc + variantWeight;
18 | }
19 |
20 | return acc + (product.weight ?? 0);
21 | }, 0);
22 |
--------------------------------------------------------------------------------
/src/schemas/ResetPasswordFormSchema.ts:
--------------------------------------------------------------------------------
1 | import { useTranslations } from "next-intl";
2 | import { z } from "zod";
3 |
4 | export type ResetPasswordFormData = {
5 | newPassword: string;
6 | confirmPassword: string;
7 | };
8 |
9 | export const useResetPasswordForm = () => {
10 | const t = useTranslations("ResetPasswordForm.errors");
11 |
12 | const ResetPasswordForm = z
13 | .object({
14 | newPassword: z.string().nonempty(t("password-length")).min(8, t("password-length")),
15 | confirmPassword: z.string().nonempty(t("password-length")).min(8, t("password-length")),
16 | })
17 | .refine((data) => data.newPassword === data.confirmPassword, {
18 | message: t("passwords-mismatch"),
19 | path: ["confirmPassword"],
20 | });
21 |
22 | return { ResetPasswordForm };
23 | };
24 |
--------------------------------------------------------------------------------
/src/utilities/getRedirects.ts:
--------------------------------------------------------------------------------
1 | import { unstable_cache } from "next/cache";
2 | import { getPayload } from "payload";
3 |
4 | import configPromise from "@payload-config";
5 |
6 | export async function getRedirects(depth = 1) {
7 | const payload = await getPayload({ config: configPromise });
8 |
9 | const { docs: redirects } = await payload.find({
10 | collection: "redirects",
11 | depth,
12 | limit: 0,
13 | pagination: false,
14 | });
15 |
16 | return redirects;
17 | }
18 |
19 | /**
20 | * Returns a unstable_cache function mapped with the cache tag for 'redirects'.
21 | *
22 | * Cache all redirects together to avoid multiple fetches.
23 | */
24 | export const getCachedRedirects = () =>
25 | unstable_cache(async () => getRedirects(), ["redirects"], {
26 | tags: ["redirects"],
27 | });
28 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as LabelPrimitive from "@radix-ui/react-label";
4 | import { type VariantProps, cva } from "class-variance-authority";
5 | import * as React from "react";
6 |
7 | import { cn } from "src/utilities/cn";
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
11 | );
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef & VariantProps
16 | >(({ className, ...props }, ref) => (
17 |
18 | ));
19 | Label.displayName = LabelPrimitive.Root.displayName;
20 |
21 | export { Label };
22 |
--------------------------------------------------------------------------------
/src/components/Logo/Logo.tsx:
--------------------------------------------------------------------------------
1 | import { clsx } from "clsx";
2 |
3 | type Props = {
4 | className?: string;
5 | loading?: "lazy" | "eager";
6 | priority?: "auto" | "high" | "low";
7 | };
8 |
9 | export const Logo = (props: Props) => {
10 | const { loading: loadingFromProps, priority: priorityFromProps, className } = props;
11 |
12 | const loading = loadingFromProps ?? "lazy";
13 | const priority = priorityFromProps ?? "low";
14 |
15 | return (
16 | /* eslint-disable @next/next/no-img-element */
17 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/src/components/Media/types.ts:
--------------------------------------------------------------------------------
1 | import type { Media as MediaType } from "@/payload-types";
2 | import type { StaticImageData } from "next/image";
3 | import type { ElementType, Ref } from "react";
4 |
5 | export type Props = {
6 | alt?: string;
7 | className?: string;
8 | fill?: boolean; // for NextImage only
9 | htmlElement?: ElementType | null;
10 | imgClassName?: string;
11 | onClick?: () => void;
12 | onLoad?: () => void;
13 | loading?: "lazy" | "eager"; // for NextImage only
14 | priority?: boolean; // for NextImage only
15 | ref?: Ref;
16 | resource?: MediaType | string | number; // for Payload media
17 | size?: string; // for NextImage only
18 | src?: StaticImageData; // for static media
19 | videoClassName?: string;
20 | placeholder?: "blur" | "empty";
21 | };
22 |
--------------------------------------------------------------------------------
/src/globals/(ecommerce)/Layout/ProductList/variants/filters/WithSidebar/components/MobileFunnelFiltersButton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { FunnelIcon } from "@heroicons/react/20/solid";
4 | import { useTranslations } from "next-intl";
5 |
6 | import { useMobileFilters } from "../stores/MobileFiltersContext";
7 |
8 | export const MobileFunnelFiltersButton = () => {
9 | const { setMobileFiltersOpen } = useMobileFilters();
10 | const t = useTranslations("ProductList");
11 | return (
12 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/src/app/(frontend)/[locale]/(with-cart)/account/orders-data/page.tsx:
--------------------------------------------------------------------------------
1 | import { revalidateTag } from "next/cache";
2 | import { setRequestLocale } from "next-intl/server";
3 |
4 | import { OrdersData } from "@/globals/(ecommerce)/Layout/ClientPanel/OrdersData/Component";
5 | import { type Locale } from "@/i18n/config";
6 | import { getCustomer } from "@/utilities/getCustomer";
7 |
8 | async function updateCustomerData() {
9 | "use server";
10 | revalidateTag("user-auth");
11 | }
12 |
13 | const OrdersDataPage = async ({ params }: { params: Promise<{ locale: Locale }> }) => {
14 | const user = await getCustomer();
15 | const { locale } = await params;
16 | setRequestLocale(locale);
17 | if (!user) return null;
18 | return ;
19 | };
20 | export default OrdersDataPage;
21 |
--------------------------------------------------------------------------------
/src/components/(ecommerce)/LogoutButton/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import axios from "axios";
4 | import { type ReactNode } from "react";
5 |
6 | import { Button } from "@/components/ui/button";
7 | import { useRouter } from "@/i18n/routing";
8 |
9 | export const LogoutButton = ({
10 | className,
11 | children,
12 | ...props
13 | }: {
14 | children: ReactNode;
15 | className?: string;
16 | [key: string]: unknown;
17 | }) => {
18 | const router = useRouter();
19 | const handleLogout = async () => {
20 | try {
21 | await axios.post("/api/customers/logout");
22 | router.refresh();
23 | } catch (error) {
24 | console.log(error);
25 | }
26 | };
27 |
28 | return (
29 |
32 | );
33 | };
34 |
--------------------------------------------------------------------------------
/src/blocks/CallToAction/config.ts:
--------------------------------------------------------------------------------
1 | import { defaultLexical } from "@/fields/defaultLexical";
2 | import { linkGroup } from "@/fields/linkGroup";
3 | import { marginFields, paddingFields } from "@/fields/spacingFields";
4 |
5 | import type { Block } from "payload";
6 |
7 | export const CallToAction: Block = {
8 | slug: "cta",
9 | interfaceName: "CallToActionBlock",
10 | fields: [
11 | {
12 | name: "richText",
13 | type: "richText",
14 | editor: defaultLexical,
15 | localized: true,
16 | label: false,
17 | },
18 | linkGroup({
19 | appearances: ["default", "outline"],
20 | overrides: {
21 | maxRows: 2,
22 | },
23 | }),
24 | marginFields,
25 | paddingFields,
26 | ],
27 | labels: {
28 | plural: "Calls to Action",
29 | singular: "Call to Action",
30 | },
31 | };
32 |
--------------------------------------------------------------------------------
/src/app/(payload)/admin/[[...segments]]/page.tsx:
--------------------------------------------------------------------------------
1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
3 | import type { Metadata } from 'next'
4 |
5 | import config from '@payload-config'
6 | import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
7 | import { importMap } from '../importMap'
8 |
9 | type Args = {
10 | params: Promise<{
11 | segments: string[]
12 | }>
13 | searchParams: Promise<{
14 | [key: string]: string | string[]
15 | }>
16 | }
17 |
18 | export const generateMetadata = ({ params, searchParams }: Args): Promise =>
19 | generatePageMetadata({ config, params, searchParams })
20 |
21 | const Page = ({ params, searchParams }: Args) =>
22 | RootPage({ config, params, searchParams, importMap })
23 |
24 | export default Page
25 |
--------------------------------------------------------------------------------
/src/components/LocaleSwitch/LocaleSwitch.tsx:
--------------------------------------------------------------------------------
1 | import { useLocale, useTranslations } from "next-intl";
2 | import { Suspense } from "react";
3 |
4 | import { SelectItem } from "@/components/ui/select";
5 | import { routing } from "@/i18n/routing";
6 |
7 | import { LocaleSwitchSelect } from "./LocaleSwitchSelect";
8 |
9 | export function LocaleSwitch() {
10 | const t = useTranslations("LocaleSwitch");
11 | const locale = useLocale();
12 |
13 | return (
14 | //TODO; better fallback
15 |
16 |
17 | {routing.locales.map((cur) => (
18 |
19 | {t("locale", { locale: cur })}
20 |
21 | ))}
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "src/utilities/cn";
4 |
5 | export type TextareaProps = {} & React.TextareaHTMLAttributes;
6 |
7 | const Textarea = React.forwardRef(({ className, ...props }, ref) => {
8 | return (
9 |
17 | );
18 | });
19 | Textarea.displayName = "Textarea";
20 |
21 | export { Textarea };
22 |
--------------------------------------------------------------------------------
/src/globals/(ecommerce)/Layout/ProductList/variants/filters/WithSidebar/components/MobileFiltersCloseButton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { XMarkIcon } from "@heroicons/react/24/outline";
4 | import { useTranslations } from "next-intl";
5 |
6 | import { useMobileFilters } from "../stores/MobileFiltersContext";
7 |
8 | export const MobileFiltersCloseButton = () => {
9 | const { setMobileFiltersOpen } = useMobileFilters();
10 | const t = useTranslations("ProductList");
11 | return (
12 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Next.js: debug full stack",
9 | "type": "node",
10 | "request": "launch",
11 | "program": "${workspaceFolder}/node_modules/next/dist/bin/next",
12 | "runtimeArgs": ["--inspect"],
13 | "skipFiles": ["/**"],
14 | "serverReadyAction": {
15 | "action": "debugWithChrome",
16 | "killOnServerStop": true,
17 | "pattern": "- Local:.+(https?://.+)",
18 | "uriFormat": "%s",
19 | "webRoot": "${workspaceFolder}"
20 | },
21 | "cwd": "${workspaceFolder}"
22 | }
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/src/collections/(ecommerce)/Orders/components/OrderTotalPriceField/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { NumberField, useField, useFormFields } from "@payloadcms/ui";
4 | import { type NumberFieldClientComponent } from "payload";
5 | import { useEffect } from "react";
6 |
7 | export const OrderTotalPriceField: NumberFieldClientComponent = (props) => {
8 | const { path } = props;
9 | const { setValue } = useField({ path });
10 |
11 | const totalPrice = useFormFields((context) => {
12 | return Object.entries(context[0])
13 | .filter(([key]) => key.includes(".priceTotal"))
14 | .map(([_, value]) => value.value as number)
15 | .reduce((acc, curr) => acc + curr, 0);
16 | });
17 |
18 | useEffect(() => {
19 | setValue(totalPrice);
20 | }, [totalPrice, setValue]);
21 |
22 | return ;
23 | };
24 |
--------------------------------------------------------------------------------
/src/components/AdminNavbar/NavWrapper/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useNav } from "@payloadcms/ui";
3 |
4 | import type { ReactNode } from "react";
5 |
6 | export const NavWrapper = (props: { baseClass?: string; children: ReactNode }) => {
7 | const { baseClass, children } = props;
8 |
9 | const { hydrated, navOpen, navRef, shouldAnimate } = useNav();
10 |
11 | return (
12 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/src/app/(payload)/admin/[[...segments]]/not-found.tsx:
--------------------------------------------------------------------------------
1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
3 | import type { Metadata } from 'next'
4 |
5 | import config from '@payload-config'
6 | import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
7 | import { importMap } from '../importMap'
8 |
9 | type Args = {
10 | params: Promise<{
11 | segments: string[]
12 | }>
13 | searchParams: Promise<{
14 | [key: string]: string | string[]
15 | }>
16 | }
17 |
18 | export const generateMetadata = ({ params, searchParams }: Args): Promise =>
19 | generatePageMetadata({ config, params, searchParams })
20 |
21 | const NotFound = ({ params, searchParams }: Args) =>
22 | NotFoundPage({ config, params, searchParams, importMap })
23 |
24 | export default NotFound
25 |
--------------------------------------------------------------------------------
/src/globals/Header/Component.client.tsx:
--------------------------------------------------------------------------------
1 | import { type ReactNode } from "react";
2 |
3 | import { DefaultHeader } from "./variants/DefaultHeader";
4 | import { FloatingHeader } from "./variants/FloatingHeader";
5 |
6 | import type { Header } from "@/payload-types";
7 |
8 | type HeaderClientProps = {
9 | data: Header;
10 | disableCart?: boolean;
11 | };
12 |
13 | export const HeaderClient = ({ data, disableCart }: HeaderClientProps) => {
14 | let header: ReactNode = null;
15 |
16 | switch (data.type) {
17 | case "default":
18 | header = ;
19 | break;
20 | case "floating":
21 | header = ;
22 | break;
23 | default:
24 | header = ;
25 | break;
26 | }
27 |
28 | return header;
29 | };
30 |
--------------------------------------------------------------------------------
/src/app/(frontend)/[locale]/(without-cart)/register/page.tsx:
--------------------------------------------------------------------------------
1 | import { RegisterPageWithoutOAuth } from "@/components/(ecommerce)/RegisterPage/WithoutOAuth";
2 | import { type Locale } from "@/i18n/config";
3 | import { redirect } from "@/i18n/routing";
4 | import { getCustomer } from "@/utilities/getCustomer";
5 | import { getCachedGlobal } from "@/utilities/getGlobals";
6 |
7 | export const dynamic = "force-dynamic";
8 |
9 | const RegisterPage = async ({ params }: { params: Promise<{ locale: Locale }> }) => {
10 | const user = await getCustomer();
11 | const { locale } = await params;
12 | if (user) {
13 | return redirect({ locale: locale, href: "/account" });
14 | }
15 | const shopSettings = await getCachedGlobal("shopSettings", locale, 1)();
16 |
17 | return shopSettings.enableOAuth ? <>> : ;
18 | };
19 |
20 | export default RegisterPage;
21 |
--------------------------------------------------------------------------------
/src/globals/(ecommerce)/Layout/ClientPanel/Orders/Component.tsx:
--------------------------------------------------------------------------------
1 | import { notFound } from "next/navigation";
2 | import { getLocale } from "next-intl/server";
3 | import { type ReactNode } from "react";
4 |
5 | import { type Locale } from "@/i18n/config";
6 | import { getCachedGlobal } from "@/utilities/getGlobals";
7 |
8 | import { WithSidebarOrders } from "../variants/WithSidebar/components/WithSidebarOrders";
9 |
10 | export const Orders = async () => {
11 | const locale = (await getLocale()) as Locale;
12 | const { clientPanel } = await getCachedGlobal("shopLayout", locale, 1)();
13 |
14 | let OrdersComponent: ReactNode = null;
15 | switch (clientPanel.type) {
16 | case "withSidebar":
17 | OrdersComponent = ;
18 | break;
19 | }
20 |
21 | if (!OrdersComponent) {
22 | notFound();
23 | }
24 |
25 | return OrdersComponent;
26 | };
27 |
--------------------------------------------------------------------------------
/src/utilities/formatDateTime.ts:
--------------------------------------------------------------------------------
1 | export const formatDateTime = (timestamp: string, format?: "EU" | "US"): string => {
2 | const now = new Date();
3 | let date = now;
4 | if (timestamp) date = new Date(timestamp);
5 | const months = date.getMonth();
6 | const days = date.getDate();
7 | // const hours = date.getHours();
8 | // const minutes = date.getMinutes();
9 | // const seconds = date.getSeconds();
10 |
11 | const MM = months + 1 < 10 ? `0${months + 1}` : months + 1;
12 | const DD = days < 10 ? `0${days}` : days;
13 | const YYYY = date.getFullYear();
14 | // const AMPM = hours < 12 ? 'AM' : 'PM';
15 | // const HH = hours > 12 ? hours - 12 : hours;
16 | // const MinMin = (minutes < 10) ? `0${minutes}` : minutes;
17 | // const SS = (seconds < 10) ? `0${seconds}` : seconds;
18 |
19 | return format === "EU" ? `${DD}/${MM}/${YYYY}` : `${MM}/${DD}/${YYYY}`;
20 | };
21 |
--------------------------------------------------------------------------------
/src/lib/couriers/labels/getInpostLabel.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { getLocale } from "next-intl/server";
3 |
4 | import { type Locale } from "@/i18n/config";
5 | import { getCachedGlobal } from "@/utilities/getGlobals";
6 |
7 | export const getInpostLabel = async (
8 | packageID: string,
9 | courierSlug: "inpost-pickup" | "inpost-courier" | "inpost-courier-cod",
10 | ) => {
11 | const locale = (await getLocale()) as Locale;
12 | const inpostSettings = await getCachedGlobal(courierSlug, locale, 1)();
13 | const { APIUrl, shipXAPIKey } = inpostSettings;
14 |
15 | const { data }: { data: ArrayBuffer } = await axios.get(
16 | `${APIUrl}/v1/shipments/${packageID}/label?type=A6`,
17 | {
18 | headers: {
19 | Authorization: `Bearer ${shipXAPIKey}`,
20 | },
21 | responseType: "arraybuffer",
22 | },
23 | );
24 |
25 | return data;
26 | };
27 |
--------------------------------------------------------------------------------
/src/app/(frontend)/[locale]/(with-cart)/account/layout.tsx:
--------------------------------------------------------------------------------
1 | import { setRequestLocale } from "next-intl/server";
2 | import { type ReactNode } from "react";
3 |
4 | import { ClientPanel } from "@/globals/(ecommerce)/Layout/ClientPanel/Component";
5 | import { type Locale } from "@/i18n/config";
6 | import { redirect } from "@/i18n/routing";
7 | import { getCustomer } from "@/utilities/getCustomer";
8 |
9 | export const dynamic = "force-dynamic";
10 |
11 | const AccountPage = async ({
12 | params,
13 | children,
14 | }: {
15 | params: Promise<{ locale: Locale }>;
16 | children: ReactNode;
17 | }) => {
18 | const { locale } = await params;
19 | setRequestLocale(locale);
20 | const user = await getCustomer();
21 |
22 | if (!user) {
23 | return redirect({ locale, href: "/login" });
24 | }
25 |
26 | return {children};
27 | };
28 | export default AccountPage;
29 |
--------------------------------------------------------------------------------
/src/collections/(ecommerce)/Orders/components/OrderTotalWithShippingField/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { NumberField, useField, useFormFields } from "@payloadcms/ui";
4 | import { type NumberFieldClientComponent } from "payload";
5 | import { useEffect } from "react";
6 |
7 | export const OrderTotalWithShippingField: NumberFieldClientComponent = (props) => {
8 | const { path } = props;
9 | const { setValue } = useField({ path });
10 |
11 | const totalPrice = useFormFields(([fields]) => {
12 | return fields["orderDetails.total"].value as number;
13 | });
14 | const shippingPrice = useFormFields(([fields]) => {
15 | return fields["orderDetails.shippingCost"].value as number;
16 | });
17 |
18 | useEffect(() => {
19 | setValue(totalPrice + shippingPrice);
20 | }, [totalPrice, shippingPrice, setValue]);
21 |
22 | return ;
23 | };
24 |
--------------------------------------------------------------------------------
/src/stores/Currency/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { create } from "zustand";
3 |
4 | import canUseDOM from "@/utilities/canUseDOM";
5 |
6 | import { type Currency } from "./types";
7 |
8 | type CurrencyState = {
9 | currency: Currency;
10 | setCurrency: (currency: Currency) => void;
11 | };
12 |
13 | const getInitialCurrency = (): Currency => {
14 | if (canUseDOM) {
15 | const stored = window.localStorage.getItem("currency") as Currency | null;
16 | if (stored) return stored;
17 | }
18 | return "USD";
19 | };
20 |
21 | const useCurrencyStore = create((set) => ({
22 | currency: getInitialCurrency(),
23 | setCurrency: (currencyToSet: Currency) => {
24 | if (canUseDOM) {
25 | window.localStorage.setItem("currency", currencyToSet);
26 | }
27 | set({ currency: currencyToSet });
28 | },
29 | }));
30 |
31 | export const useCurrency = () => useCurrencyStore();
32 |
--------------------------------------------------------------------------------
/src/utilities/getURL.ts:
--------------------------------------------------------------------------------
1 | import canUseDOM from "./canUseDOM";
2 |
3 | export const getServerSideURL = () => {
4 | let url = process.env.NEXT_PUBLIC_SERVER_URL;
5 |
6 | if (!url && process.env.VERCEL_PROJECT_PRODUCTION_URL) {
7 | return `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`;
8 | }
9 |
10 | if (!url) {
11 | url = "http://localhost:3000";
12 | }
13 |
14 | return url;
15 | };
16 |
17 | export const getClientSideURL = () => {
18 | if (canUseDOM) {
19 | const protocol = window.location.protocol;
20 | const domain = window.location.hostname;
21 | const port = window.location.port;
22 |
23 | return `${protocol}//${domain}${port ? `:${port}` : ""}`;
24 | }
25 |
26 | if (process.env.VERCEL_PROJECT_PRODUCTION_URL) {
27 | return `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`;
28 | }
29 |
30 | return process.env.NEXT_PUBLIC_SERVER_URL || "";
31 | };
32 |
--------------------------------------------------------------------------------
/src/globals/(ecommerce)/Layout/ClientPanel/Component.tsx:
--------------------------------------------------------------------------------
1 | import { notFound } from "next/navigation";
2 | import { getLocale } from "next-intl/server";
3 | import { type ReactNode } from "react";
4 |
5 | import { type Locale } from "@/i18n/config";
6 | import { getCachedGlobal } from "@/utilities/getGlobals";
7 |
8 | import { WithSidebar } from "./variants/WithSidebar";
9 |
10 | export const ClientPanel = async ({ children }: { children: ReactNode }) => {
11 | const locale = (await getLocale()) as Locale;
12 | const { clientPanel } = await getCachedGlobal("shopLayout", locale, 1)();
13 |
14 | let ClientPanelComponent: ReactNode = null;
15 | switch (clientPanel.type) {
16 | case "withSidebar":
17 | ClientPanelComponent = {children};
18 | break;
19 | }
20 |
21 | if (!ClientPanelComponent) {
22 | notFound();
23 | }
24 |
25 | return ClientPanelComponent;
26 | };
27 |
--------------------------------------------------------------------------------
/src/app/(frontend)/next/verify-email/route.ts:
--------------------------------------------------------------------------------
1 | import { getLocale } from "next-intl/server";
2 | import { getPayload } from "payload";
3 |
4 | import config from "@payload-config";
5 | export async function GET(req: Request) {
6 | const payload = await getPayload({ config });
7 | try {
8 | const url = new URL(req.url);
9 | const token = url.searchParams.get("token");
10 |
11 | if (!token) {
12 | return Response.json({ message: "Verification token is required" }, { status: 400 });
13 | }
14 |
15 | await payload.verifyEmail({
16 | collection: "customers",
17 | token: token,
18 | });
19 |
20 | const locale = await getLocale();
21 |
22 | return Response.redirect(`${process.env.NEXT_PUBLIC_SERVER_URL}/${locale}/login?verified=true`);
23 | } catch (error) {
24 | console.log(error);
25 | return Response.json({ message: "Internal server error" }, { status: 500 });
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/collections/Categories.ts:
--------------------------------------------------------------------------------
1 | import { anyone } from "@/access/anyone";
2 | import { authenticated } from "@/access/authenticated";
3 |
4 | import type { CollectionConfig } from "payload";
5 |
6 | export const Categories: CollectionConfig = {
7 | slug: "categories",
8 | labels: {
9 | plural: {
10 | en: "Posts Categories",
11 | pl: "Kategorie postów",
12 | },
13 | singular: {
14 | en: "Post Category",
15 | pl: "Kategoria postów",
16 | },
17 | },
18 | access: {
19 | create: authenticated,
20 | delete: authenticated,
21 | read: anyone,
22 | update: authenticated,
23 | },
24 | admin: {
25 | useAsTitle: "title",
26 | group: {
27 | en: "Page Settings",
28 | pl: "Ustawienia strony",
29 | },
30 | },
31 | fields: [
32 | {
33 | name: "title",
34 | type: "text",
35 | required: true,
36 | localized: true,
37 | },
38 | ],
39 | };
40 |
--------------------------------------------------------------------------------
/src/blocks/Code/CopyButton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { CopyIcon } from "@payloadcms/ui/icons/Copy";
4 | import { useState } from "react";
5 |
6 | import { Button } from "@/components/ui/button";
7 |
8 | export function CopyButton({ code }: { code: string }) {
9 | const [text, setText] = useState("Copy");
10 |
11 | function updateCopyStatus() {
12 | if (text === "Copy") {
13 | setText(() => "Copied!");
14 | setTimeout(() => {
15 | setText(() => "Copy");
16 | }, 1000);
17 | }
18 | }
19 |
20 | return (
21 |
22 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/src/blocks/Form/config.ts:
--------------------------------------------------------------------------------
1 | import { noBlocksLexical } from "@/fields/noBlocksLexical";
2 |
3 | import type { Block } from "payload";
4 |
5 | export const FormBlock: Block = {
6 | slug: "formBlock",
7 | interfaceName: "FormBlock",
8 | fields: [
9 | {
10 | name: "form",
11 | type: "relationship",
12 | relationTo: "forms",
13 | required: true,
14 | },
15 | {
16 | name: "enableIntro",
17 | type: "checkbox",
18 | label: "Enable Intro Content",
19 | },
20 | {
21 | name: "introContent",
22 | type: "richText",
23 | admin: {
24 | condition: (_, { enableIntro }) => Boolean(enableIntro),
25 | },
26 | localized: true,
27 | editor: noBlocksLexical,
28 | label: "Intro Content",
29 | },
30 | ],
31 | graphQL: {
32 | singularName: "FormBlock",
33 | },
34 | labels: {
35 | plural: "Form Blocks",
36 | singular: "Form Block",
37 | },
38 | };
39 |
--------------------------------------------------------------------------------
/src/app/(payload)/layout.tsx:
--------------------------------------------------------------------------------
1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
3 | import config from '@payload-config'
4 | import '@payloadcms/next/css'
5 | import type { ServerFunctionClient } from 'payload'
6 | import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
7 | import React from 'react'
8 |
9 | import { importMap } from './admin/importMap.js'
10 | import './custom.scss'
11 |
12 | type Args = {
13 | children: React.ReactNode
14 | }
15 |
16 | const serverFunction: ServerFunctionClient = async function (args) {
17 | 'use server'
18 | return handleServerFunctions({
19 | ...args,
20 | config,
21 | importMap,
22 | })
23 | }
24 |
25 | const Layout = ({ children }: Args) => (
26 |
27 | {children}
28 |
29 | )
30 |
31 | export default Layout
32 |
--------------------------------------------------------------------------------
/src/blocks/Banner/Component.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import RichText from "@/components/RichText";
4 | import { cn } from "src/utilities/cn";
5 |
6 | import type { BannerBlock as BannerBlockProps } from "src/payload-types";
7 |
8 | type Props = {
9 | className?: string;
10 | } & BannerBlockProps;
11 |
12 | export const BannerBlock = ({ className, content, style }: Props) => {
13 | return (
14 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/src/utilities/getDocument.ts:
--------------------------------------------------------------------------------
1 | import { unstable_cache } from "next/cache";
2 | import { getPayload } from "payload";
3 |
4 | import configPromise from "@payload-config";
5 |
6 | import type { Config } from "@/payload-types";
7 |
8 | type Collection = keyof Config["collections"];
9 |
10 | async function getDocument(collection: Collection, slug: string, depth = 0) {
11 | const payload = await getPayload({ config: configPromise });
12 |
13 | const page = await payload.find({
14 | collection,
15 | depth,
16 | where: {
17 | slug: {
18 | equals: slug,
19 | },
20 | },
21 | });
22 |
23 | return page.docs[0];
24 | }
25 |
26 | /**
27 | * Returns a unstable_cache function mapped with the cache tag for the slug
28 | */
29 | export const getCachedDocument = (collection: Collection, slug: string) =>
30 | unstable_cache(async () => getDocument(collection, slug), [collection, slug], {
31 | tags: [`${collection}_${slug}`],
32 | });
33 |
--------------------------------------------------------------------------------
/src/collections/Administrators/index.ts:
--------------------------------------------------------------------------------
1 | import { authenticated } from "@/access/authenticated";
2 |
3 | import type { CollectionConfig } from "payload";
4 |
5 | export const Administrators: CollectionConfig = {
6 | slug: "administrators",
7 | labels: {
8 | singular: {
9 | en: "Administrator",
10 | pl: "Administrator",
11 | },
12 | plural: {
13 | en: "Administrators",
14 | pl: "Administratorzy",
15 | },
16 | },
17 | access: {
18 | admin: authenticated,
19 | create: authenticated,
20 | delete: authenticated,
21 | read: authenticated,
22 | update: authenticated,
23 | },
24 | admin: {
25 | defaultColumns: ["name", "email"],
26 | useAsTitle: "name",
27 | group: {
28 | en: "Page Settings",
29 | pl: "Ustawienia strony",
30 | },
31 | },
32 | auth: true,
33 | fields: [
34 | {
35 | name: "name",
36 | type: "text",
37 | },
38 | ],
39 | timestamps: true,
40 | };
41 |
--------------------------------------------------------------------------------
/src/utilities/deepMerge.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | // @ts-nocheck
3 |
4 | /**
5 | * Simple object check.
6 | * @param item
7 | * @returns {boolean}
8 | */
9 | export function isObject(item: unknown): boolean {
10 | return item && typeof item === "object" && !Array.isArray(item);
11 | }
12 |
13 | /**
14 | * Deep merge two objects.
15 | * @param target
16 | * @param ...sources
17 | */
18 | export default function deepMerge(target: T, source: R): T {
19 | const output = { ...target };
20 | if (isObject(target) && isObject(source)) {
21 | Object.keys(source).forEach((key) => {
22 | if (isObject(source[key])) {
23 | if (!(key in target)) {
24 | Object.assign(output, { [key]: source[key] });
25 | } else {
26 | output[key] = deepMerge(target[key], source[key]);
27 | }
28 | } else {
29 | Object.assign(output, { [key]: source[key] });
30 | }
31 | });
32 | }
33 |
34 | return output;
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/(ecommerce)/AdminDashboardNavLink/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useTranslation } from "@payloadcms/ui";
3 | import { ChartNoAxesCombined } from "lucide-react";
4 | import Link from "next/link";
5 | import { usePathname } from "next/navigation";
6 |
7 | import {
8 | type CustomTranslationsObject,
9 | type CustomTranslationsKeys,
10 | } from "@/admin/translations/custom-translations";
11 |
12 | export const AdminDashboardNavLink = () => {
13 | const { t } = useTranslation();
14 |
15 | const pathname = usePathname();
16 |
17 | return (
18 |
22 |
23 | {pathname === "/admin" && }
24 | {t("adminDashboard:linkTitle")}
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/src/fields/freeShippingField.ts:
--------------------------------------------------------------------------------
1 | import { type Field } from "payload";
2 |
3 | import { currencyField } from "./currencyField";
4 |
5 | export const freeShippingField: Field = {
6 | name: "freeShipping",
7 | type: "array",
8 | label: {
9 | en: "Free shipping from",
10 | pl: "Darmowa dostawa od",
11 | },
12 | labels: {
13 | singular: {
14 | en: "Price",
15 | pl: "Cena",
16 | },
17 | plural: {
18 | en: "Prices",
19 | pl: "Ceny",
20 | },
21 | },
22 | admin: {
23 | components: {
24 | RowLabel: "@/components/(ecommerce)/RowLabels/PriceRowLabel#PriceRowLabel",
25 | },
26 | },
27 | fields: [
28 | {
29 | type: "row",
30 | fields: [
31 | {
32 | name: "value",
33 | type: "number",
34 | label: {
35 | en: "Price",
36 | pl: "Cena",
37 | },
38 | required: true,
39 | },
40 | currencyField,
41 | ],
42 | },
43 | ],
44 | };
45 |
--------------------------------------------------------------------------------
/src/globals/(ecommerce)/Layout/ProductList/variants/filters/WithSidebar/components/SortSelect.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useRouter, useSearchParams } from "next/navigation";
3 | import { type ReactNode } from "react";
4 |
5 | import { Select } from "@/components/ui/select";
6 |
7 | export const SortSelect = ({ children, defaultValue }: { children: ReactNode; defaultValue: string }) => {
8 | const searchParams = useSearchParams();
9 | const router = useRouter();
10 |
11 | const handleSortingOptions = (value: string) => {
12 | const currentParams = new URLSearchParams(searchParams?.toString());
13 |
14 | if (!value || value === "most-popular") {
15 | currentParams.delete("sortBy");
16 | } else {
17 | currentParams.set("sortBy", value);
18 | }
19 |
20 | router.push(`?${currentParams.toString()}`);
21 | };
22 |
23 | return (
24 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/src/blocks/RelatedPosts/Component.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import RichText from "@/components/RichText";
3 | import { cn } from "@/utilities/cn";
4 |
5 | import { Card } from "../../components/Card";
6 |
7 | import type { Post } from "@/payload-types";
8 |
9 | export type RelatedPostsProps = {
10 | className?: string;
11 | docs?: Post[];
12 | introContent?: any;
13 | };
14 |
15 | export const RelatedPosts = (props: RelatedPostsProps) => {
16 | const { className, docs, introContent } = props;
17 |
18 | return (
19 |
20 | {introContent &&
}
21 |
22 |
23 | {docs?.map((doc, index) => {
24 | if (typeof doc === "string") return null;
25 |
26 | return ;
27 | })}
28 |
29 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/blocks/Banner/config.ts:
--------------------------------------------------------------------------------
1 | import { FixedToolbarFeature, InlineToolbarFeature, lexicalEditor } from "@payloadcms/richtext-lexical";
2 |
3 | import type { Block } from "payload";
4 |
5 | export const Banner: Block = {
6 | slug: "banner",
7 | fields: [
8 | {
9 | name: "style",
10 | type: "select",
11 | defaultValue: "info",
12 | options: [
13 | { label: "Info", value: "info" },
14 | { label: "Warning", value: "warning" },
15 | { label: "Error", value: "error" },
16 | { label: "Success", value: "success" },
17 | ],
18 | required: true,
19 | },
20 | {
21 | name: "content",
22 | type: "richText",
23 | editor: lexicalEditor({
24 | features: ({ rootFeatures }) => {
25 | return [...rootFeatures, FixedToolbarFeature(), InlineToolbarFeature()];
26 | },
27 | }),
28 | label: false,
29 | localized: true,
30 | required: true,
31 | },
32 | ],
33 | interfaceName: "BannerBlock",
34 | };
35 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "src/utilities/cn";
4 |
5 | export type InputProps = {} & React.InputHTMLAttributes;
6 |
7 | const Input = React.forwardRef(({ type, className, ...props }, ref) => {
8 | return (
9 |
18 | );
19 | });
20 | Input.displayName = "Input";
21 |
22 | export { Input };
23 |
--------------------------------------------------------------------------------
/src/lib/getTotal.ts:
--------------------------------------------------------------------------------
1 | import { type FilledProduct } from "./getFilledProducts";
2 |
3 | type Total = Record;
4 |
5 | export const getTotal = (filledProducts: FilledProduct[]) => {
6 | const total = filledProducts.reduce((acc, product) => {
7 | if (!product) return acc;
8 | if (!product.enableVariantPrices) {
9 | product.pricing?.forEach((price) => {
10 | acc[price.currency] = (acc[price.currency] ?? 0) + price.value * (product.quantity ?? 1);
11 | });
12 | } else if (product.enableVariantPrices && product.enableVariants) {
13 | product.variant?.pricing?.forEach((price) => {
14 | acc[price.currency] = (acc[price.currency] ?? 0) + price.value * (product.quantity ?? 1);
15 | });
16 | }
17 | return acc;
18 | }, {});
19 |
20 | const totalFormatted =
21 | total &&
22 | Object.entries(total).map(([currency, value]) => ({
23 | currency,
24 | value: parseFloat(value.toFixed(2)),
25 | }));
26 |
27 | return totalFormatted;
28 | };
29 |
--------------------------------------------------------------------------------
/src/app/(frontend)/[locale]/(without-cart)/login/page.tsx:
--------------------------------------------------------------------------------
1 | import { LoginPageWithoutOAuth } from "@/components/(ecommerce)/LoginPage/WithoutOAuth";
2 | import { type Locale } from "@/i18n/config";
3 | import { redirect } from "@/i18n/routing";
4 | import { getCustomer } from "@/utilities/getCustomer";
5 | import { getCachedGlobal } from "@/utilities/getGlobals";
6 |
7 | export const dynamic = "force-dynamic";
8 |
9 | const LoginPage = async ({
10 | params,
11 | searchParams,
12 | }: {
13 | params: Promise<{ locale: Locale }>;
14 | searchParams: Promise<{ verified?: string }>;
15 | }) => {
16 | const user = await getCustomer();
17 | const { locale } = await params;
18 | const { verified } = await searchParams;
19 | if (user?.id) {
20 | return redirect({ locale: locale, href: "/account/orders" });
21 | }
22 | const shopSettings = await getCachedGlobal("shopSettings", locale, 1)();
23 |
24 | return shopSettings.enableOAuth ? <>> : ;
25 | };
26 | export default LoginPage;
27 |
--------------------------------------------------------------------------------
/src/components/CollectionArchive/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Card, type CardPostData } from "@/components/Card";
4 | import { cn } from "src/utilities/cn";
5 |
6 | export type Props = {
7 | posts: CardPostData[];
8 | };
9 |
10 | export const CollectionArchive = (props: Props) => {
11 | const { posts } = props;
12 |
13 | return (
14 |
15 |
16 |
17 | {posts?.map((result, index) => {
18 | if (typeof result === "object" && result !== null) {
19 | return (
20 |
21 |
22 |
23 | );
24 | }
25 |
26 | return null;
27 | })}
28 |
29 |
30 |
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright 2025 Mandala Software House
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/src/blocks/Form/Number/index.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import { Input } from "@/components/ui/input";
3 | import { Label } from "@/components/ui/label";
4 |
5 | import { Error } from "../Error";
6 | import { Width } from "../Width";
7 |
8 | import type { TextField } from "@payloadcms/plugin-form-builder/types";
9 | import type { FieldErrorsImpl, FieldValues, UseFormRegister } from "react-hook-form";
10 | export const Number = ({
11 | name,
12 | defaultValue,
13 | errors,
14 | label,
15 | register,
16 | required: requiredFromProps,
17 | width,
18 | }: TextField & {
19 | errors: Partial>>;
20 | register: UseFormRegister;
21 | }) => {
22 | return (
23 |
24 |
25 |
31 | {requiredFromProps && errors[name] && }
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/src/blocks/Form/Text/index.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import { Input } from "@/components/ui/input";
3 | import { Label } from "@/components/ui/label";
4 |
5 | import { Error } from "../Error";
6 | import { Width } from "../Width";
7 |
8 | import type { TextField } from "@payloadcms/plugin-form-builder/types";
9 | import type { FieldErrorsImpl, FieldValues, UseFormRegister } from "react-hook-form";
10 |
11 | export const Text = ({
12 | name,
13 | defaultValue,
14 | errors,
15 | label,
16 | register,
17 | required: requiredFromProps,
18 | width,
19 | }: TextField & {
20 | errors: Partial>>;
21 | register: UseFormRegister;
22 | }) => {
23 | return (
24 |
25 |
26 |
32 | {requiredFromProps && errors[name] && }
33 |
34 | );
35 | };
36 |
--------------------------------------------------------------------------------
/src/blocks/globals.ts:
--------------------------------------------------------------------------------
1 | import { cn } from "@/utilities/cn";
2 |
3 | export const spacingTopClasses = {
4 | none: "mt-0",
5 | small: "mt-8",
6 | medium: "mt-16",
7 | large: "mt-24",
8 | };
9 |
10 | export const spacingBottomClasses = {
11 | none: "mb-0",
12 | small: "mb-8",
13 | medium: "mb-16",
14 | large: "mb-24",
15 | };
16 |
17 | export const paddingTopClasses = {
18 | none: "pt-0",
19 | small: "pt-8",
20 | medium: "pt-16",
21 | large: "pt-24",
22 | };
23 |
24 | export const paddingBottomClasses = {
25 | none: "pb-0",
26 | small: "pb-8",
27 | medium: "pb-16",
28 | large: "pb-24",
29 | };
30 |
31 | export const getCenteringClasses = (alignment: "left" | "right" | "full" | "center" = "center") => {
32 | return cn(
33 | alignment === "left" || alignment === "right"
34 | ? "max-w-sm-half md:max-w-md-half lg:max-w-lg-half xl:max-w-xl-half 2xl:max-w-2xl-half"
35 | : "",
36 | alignment === "left" ? "ml-0 pl-0" : "",
37 | alignment === "right" ? "mr-0 pr-0" : "",
38 | alignment === "full" ? "max-w-none mx-0 px-0" : "",
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/src/fields/defaultLexical.ts:
--------------------------------------------------------------------------------
1 | import { LinkFeature, lexicalEditor } from "@payloadcms/richtext-lexical";
2 | import { type Config } from "payload";
3 |
4 | export const defaultLexical: Config["editor"] = lexicalEditor({
5 | features: ({ defaultFeatures }) => {
6 | return [
7 | ...defaultFeatures,
8 | LinkFeature({
9 | enabledCollections: ["pages", "posts"],
10 | fields: ({ defaultFields }) => {
11 | const defaultFieldsWithoutUrl = defaultFields.filter((field) => {
12 | if ("name" in field && field.name === "url") return false;
13 | return true;
14 | });
15 |
16 | return [
17 | ...defaultFieldsWithoutUrl,
18 | {
19 | name: "url",
20 | type: "text",
21 | admin: {
22 | condition: ({ linkType }) => linkType !== "internal",
23 | },
24 | label: ({ t }) => t("fields:enterURL"),
25 | required: true,
26 | },
27 | ];
28 | },
29 | }),
30 | ];
31 | },
32 | });
33 |
--------------------------------------------------------------------------------
/src/schemas/changePasswordModalForm.schema.ts:
--------------------------------------------------------------------------------
1 | import { useTranslations } from "next-intl";
2 | import { z } from "zod";
3 |
4 | export type ChangePasswordModalFormData = {
5 | oldPassword: string;
6 | newPassword: string;
7 | confirmPassword: string;
8 | };
9 |
10 | export const useChangePasswordModalForm = () => {
11 | const t = useTranslations("Account.settings.password-form.errors");
12 |
13 | const ChangePasswordModalForm = z
14 | .object({
15 | oldPassword: z.string().nonempty(t("password-length")),
16 | newPassword: z.string().nonempty(t("password-length")).min(8, t("password-length")),
17 | confirmPassword: z.string().nonempty(t("password-length")).min(8, t("password-length")),
18 | })
19 | .refine((data) => data.newPassword === data.confirmPassword, {
20 | message: t("passwords-mismatch"),
21 | path: ["confirmPassword"],
22 | })
23 | .refine((data) => data.oldPassword !== data.newPassword, {
24 | message: t("password-same"),
25 | path: ["newPassword"],
26 | });
27 |
28 | return { ChangePasswordModalForm };
29 | };
30 |
--------------------------------------------------------------------------------
/src/globals/(ecommerce)/Layout/Checkout/variants/OneStepWithSummary/index.tsx:
--------------------------------------------------------------------------------
1 | import { getTranslations } from "next-intl/server";
2 |
3 | import { type Locale } from "@/i18n/config";
4 | import { getCustomer } from "@/utilities/getCustomer";
5 | import { getCachedGlobal } from "@/utilities/getGlobals";
6 |
7 | import { CheckoutForm } from "./components/CheckoutForm";
8 |
9 | export const OneStepWithSummary = async ({ locale }: { locale: Locale }) => {
10 | const user = await getCustomer();
11 |
12 | const { geowidgetToken } = await getCachedGlobal("inpost-pickup", locale, 1)();
13 |
14 | const t = await getTranslations("CheckoutFormServer");
15 |
16 | return (
17 |
18 |
19 |
{t("checkout")}
20 |
21 |
22 |
23 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/src/utilities/formatAuthors.ts:
--------------------------------------------------------------------------------
1 | import { type Post } from "@/payload-types";
2 |
3 | /**
4 | * Formats an array of populatedAuthors from Posts into a prettified string.
5 | * @param authors - The populatedAuthors array from a Post.
6 | * @returns A prettified string of authors.
7 | * @example
8 | *
9 | * [Author1, Author2] becomes 'Author1 and Author2'
10 | * [Author1, Author2, Author3] becomes 'Author1, Author2, and Author3'
11 | *
12 | */
13 | export const formatAuthors = (authors: NonNullable[number]>[]) => {
14 | // Ensure we don't have any authors without a name
15 | const filteredAuthors = authors.filter((author) => Boolean(author.name));
16 |
17 | if (filteredAuthors.length === 0) return "";
18 | if (filteredAuthors.length === 1) return filteredAuthors[0].name;
19 | if (filteredAuthors.length === 2) return `${filteredAuthors[0].name} and ${filteredAuthors[1].name}`;
20 |
21 | return `${filteredAuthors
22 | .slice(0, -1)
23 | .map((author) => author?.name)
24 | .join(", ")} and ${filteredAuthors[authors.length - 1].name}`;
25 | };
26 |
--------------------------------------------------------------------------------
/src/blocks/Form/Email/index.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import { Input } from "@/components/ui/input";
3 | import { Label } from "@/components/ui/label";
4 |
5 | import { Error } from "../Error";
6 | import { Width } from "../Width";
7 |
8 | import type { EmailField } from "@payloadcms/plugin-form-builder/types";
9 | import type { FieldErrorsImpl, FieldValues, UseFormRegister } from "react-hook-form";
10 |
11 | export const Email = ({
12 | name,
13 | defaultValue,
14 | errors,
15 | label,
16 | register,
17 | required: requiredFromProps,
18 | width,
19 | }: EmailField & {
20 | errors: Partial>>;
21 | register: UseFormRegister;
22 | }) => {
23 | return (
24 |
25 |
26 |
32 |
33 | {requiredFromProps && errors[name] && }
34 |
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/src/globals/Footer/config.ts:
--------------------------------------------------------------------------------
1 | import { link } from "@/fields/link";
2 | import { revalidateGlobal } from "@/hooks/revalidateGlobal";
3 |
4 | import type { GlobalConfig } from "payload";
5 |
6 | export const Footer: GlobalConfig = {
7 | slug: "footer",
8 | access: {
9 | read: () => true,
10 | },
11 | label: {
12 | en: "Footer",
13 | pl: "Stopka",
14 | },
15 | admin: {
16 | group: {
17 | en: "Page Settings",
18 | pl: "Ustawienia strony",
19 | },
20 | },
21 | fields: [
22 | {
23 | name: "attribution",
24 | type: "richText",
25 | label: "Attribution",
26 | localized: true,
27 | },
28 | {
29 | name: "navItems",
30 | type: "array",
31 | fields: [
32 | link({
33 | appearances: false,
34 | }),
35 | ],
36 | maxRows: 6,
37 | admin: {
38 | initCollapsed: true,
39 | components: {
40 | RowLabel: "@/globals/Footer/RowLabel#RowLabel",
41 | },
42 | },
43 | },
44 | ],
45 | hooks: {
46 | afterChange: [revalidateGlobal],
47 | },
48 | };
49 |
--------------------------------------------------------------------------------
/src/utilities/getOrderProducts.ts:
--------------------------------------------------------------------------------
1 | import { getPayload } from "payload";
2 |
3 | import { type Locale } from "@/i18n/config";
4 | import { type Order } from "@/payload-types";
5 | import config from "@/payload.config";
6 |
7 | export const getOrderProducts = async (orderProducts: Order["products"] | null, locale: Locale) => {
8 | try {
9 | const payload = await getPayload({ config });
10 | console.log(orderProducts);
11 | return (
12 | orderProducts &&
13 | (await Promise.all(
14 | orderProducts.map(async (product) => {
15 | const filledProduct = await payload.findByID({
16 | collection: "products",
17 | id:
18 | typeof product.product === "string"
19 | ? product.product
20 | : (product.product?.id ?? product.id ?? ""),
21 | locale,
22 | });
23 | return {
24 | ...product,
25 | ...filledProduct,
26 | };
27 | }),
28 | ))
29 | );
30 | } catch (error) {
31 | console.error(error);
32 | return [];
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/src/blocks/Accordion/config.ts:
--------------------------------------------------------------------------------
1 | import { noBlocksLexical } from "@/fields/noBlocksLexical";
2 | import { marginFields, paddingFields } from "@/fields/spacingFields";
3 |
4 | import type { Block, Field } from "payload";
5 |
6 | const faqFields: Field[] = [
7 | {
8 | name: "title",
9 | type: "text",
10 | localized: true,
11 | required: true,
12 | },
13 | {
14 | name: "content",
15 | type: "richText",
16 | localized: true,
17 | editor: noBlocksLexical,
18 | required: true,
19 | },
20 | ];
21 |
22 | export const Accordion: Block = {
23 | slug: "accordion",
24 | interfaceName: "AccordionBlock",
25 | imageURL: "/blocksThumbnails/accordion.png",
26 | imageAltText: "Accordion",
27 | fields: [
28 | {
29 | name: "title",
30 | type: "richText",
31 | localized: true,
32 | editor: noBlocksLexical,
33 | },
34 | {
35 | name: "items",
36 | type: "array",
37 | admin: {
38 | initCollapsed: true,
39 | },
40 | required: true,
41 | fields: faqFields,
42 | },
43 | marginFields,
44 | paddingFields,
45 | ],
46 | };
47 |
--------------------------------------------------------------------------------
/src/components/AdminNavbar/getNavPrefs.ts:
--------------------------------------------------------------------------------
1 | import { cache } from "react";
2 |
3 | import type { NavPreferences, Payload, TypedUser } from "payload";
4 |
5 | export const getNavPrefs = cache(
6 | async ({ payload, user }: { payload: Payload; user?: TypedUser }): Promise =>
7 | user
8 | ? payload
9 | .find({
10 | collection: "payload-preferences",
11 | depth: 0,
12 | limit: 1,
13 | user,
14 | where: {
15 | and: [
16 | {
17 | key: {
18 | equals: "nav",
19 | },
20 | },
21 | {
22 | "user.relationTo": {
23 | equals: user.collection,
24 | },
25 | },
26 | {
27 | "user.value": {
28 | equals: user.id,
29 | },
30 | },
31 | ],
32 | },
33 | })
34 | ?.then((res) => res?.docs?.[0]?.value as NavPreferences)
35 | : null,
36 | );
37 |
--------------------------------------------------------------------------------
/src/utilities/getGlobals.ts:
--------------------------------------------------------------------------------
1 | import { unstable_cache } from "next/cache";
2 | import { type DataFromGlobalSlug, getPayload } from "payload";
3 |
4 | import { type Locale } from "@/i18n/config";
5 | import configPromise from "@payload-config";
6 |
7 | import type { Config } from "@/payload-types";
8 |
9 | type Global = keyof Config["globals"];
10 |
11 | async function getGlobal(slug: Global, depth = 5, locale: Locale) {
12 | const payload = await getPayload({ config: configPromise });
13 |
14 | const global = await payload.findGlobal({
15 | slug,
16 | locale,
17 | depth,
18 | });
19 |
20 | return global;
21 | }
22 |
23 | /**
24 | * Returns a unstable_cache function mapped with the cache tag for the slug
25 | */
26 | export const getCachedGlobal = (
27 | slug: T,
28 | locale: Locale,
29 | depth?: number,
30 | ): (() => Promise>) =>
31 | unstable_cache(
32 | async (): Promise> => {
33 | return (await getGlobal(slug, depth, locale)) as DataFromGlobalSlug;
34 | },
35 | [slug],
36 | {
37 | tags: [`global_${slug}`],
38 | },
39 | );
40 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | import { withPayload } from "@payloadcms/next/withPayload";
2 | import createNextIntlPlugin from "next-intl/plugin";
3 | import { withPlausibleProxy } from "next-plausible";
4 |
5 | import redirects from "./redirects.js";
6 |
7 | const NEXT_PUBLIC_SERVER_URL = process.env.VERCEL_PROJECT_PRODUCTION_URL
8 | ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
9 | : undefined || process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:3000";
10 |
11 | const withNextIntl = createNextIntlPlugin();
12 |
13 | /** @type {import('next').NextConfig} */
14 | const nextConfig = {
15 | output: "standalone",
16 | images: {
17 | remotePatterns: [
18 | ...[NEXT_PUBLIC_SERVER_URL /* 'https://example.com' */].map((item) => {
19 | const url = new URL(item);
20 |
21 | return {
22 | hostname: url.hostname,
23 | protocol: url.protocol.replace(":", ""),
24 | };
25 | }),
26 | ],
27 | },
28 | reactStrictMode: true,
29 | redirects,
30 | experimental: {
31 | reactCompiler: true,
32 | viewTransition: true,
33 | },
34 | };
35 |
36 | export default withNextIntl(withPayload(nextConfig));
37 |
--------------------------------------------------------------------------------
/src/app/(frontend)/[locale]/(with-cart)/product/[slug]/page.tsx:
--------------------------------------------------------------------------------
1 | import { notFound } from "next/navigation";
2 | import { getLocale } from "next-intl/server";
3 | import { getPayload } from "payload";
4 |
5 | import { ProductDetails } from "@/globals/(ecommerce)/Layout/ProductDetails/Component";
6 | import { type Locale } from "@/i18n/config";
7 | import config from "@payload-config";
8 |
9 | const ProductPage = async ({
10 | params,
11 | searchParams,
12 | }: {
13 | params: Promise<{ slug: string }>;
14 | searchParams: Promise>;
15 | }) => {
16 | const payload = await getPayload({ config });
17 | const locale = (await getLocale()) as Locale;
18 | const { slug } = await params;
19 | const { docs } = await payload.find({
20 | collection: "products",
21 | depth: 2,
22 | locale,
23 | where: {
24 | slug: {
25 | equals: slug,
26 | },
27 | },
28 | });
29 | const { variant } = await searchParams;
30 |
31 | if (docs.length === 0) {
32 | notFound();
33 | }
34 |
35 | return ;
36 | };
37 |
38 | export default ProductPage;
39 |
--------------------------------------------------------------------------------
/src/blocks/Form/Textarea/index.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import { Label } from "@/components/ui/label";
3 | import { Textarea as TextAreaComponent } from "@/components/ui/textarea";
4 |
5 | import { Error } from "../Error";
6 | import { Width } from "../Width";
7 |
8 | import type { TextField } from "@payloadcms/plugin-form-builder/types";
9 | import type { FieldErrorsImpl, FieldValues, UseFormRegister } from "react-hook-form";
10 |
11 | export const Textarea = ({
12 | name,
13 | defaultValue,
14 | errors,
15 | label,
16 | register,
17 | required: requiredFromProps,
18 | rows = 3,
19 | width,
20 | }: TextField & {
21 | errors: Partial>>;
22 | register: UseFormRegister;
23 | rows?: number;
24 | }) => {
25 | return (
26 |
27 |
28 |
29 |
35 |
36 | {requiredFromProps && errors[name] && }
37 |
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/src/blocks/Form/buildInitialFormState.tsx:
--------------------------------------------------------------------------------
1 | import type { FormFieldBlock } from "@payloadcms/plugin-form-builder/types";
2 |
3 | export const buildInitialFormState = (fields: FormFieldBlock[]) => {
4 | return fields?.reduce((initialSchema, field) => {
5 | if (field.blockType === "checkbox") {
6 | return {
7 | ...initialSchema,
8 | [field.name]: field.defaultValue,
9 | };
10 | }
11 | if (field.blockType === "country") {
12 | return {
13 | ...initialSchema,
14 | [field.name]: "",
15 | };
16 | }
17 | if (field.blockType === "email") {
18 | return {
19 | ...initialSchema,
20 | [field.name]: "",
21 | };
22 | }
23 | if (field.blockType === "text") {
24 | return {
25 | ...initialSchema,
26 | [field.name]: "",
27 | };
28 | }
29 | if (field.blockType === "select") {
30 | return {
31 | ...initialSchema,
32 | [field.name]: "",
33 | };
34 | }
35 | if (field.blockType === "state") {
36 | return {
37 | ...initialSchema,
38 | [field.name]: "",
39 | };
40 | }
41 | }, {});
42 | };
43 |
--------------------------------------------------------------------------------
/src/collections/(ecommerce)/Orders/components/ProductTotalPriceField/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { NumberField, useField, useFormFields } from "@payloadcms/ui";
4 | import { type NumberFieldClientComponent } from "payload";
5 | import { useCallback, useEffect } from "react";
6 |
7 | export const ProductTotalPriceField: NumberFieldClientComponent = (props) => {
8 | const { path } = props;
9 | const { setValue } = useField({ path });
10 |
11 | const unitPricePath = path.replace("priceTotal", "price");
12 | const unitPriceValue = useFormFields(([fields]) => {
13 | return fields[unitPricePath]?.value as number;
14 | });
15 |
16 | const quantityPath = path.replace("priceTotal", "quantity");
17 | const quantityValue = useFormFields(([fields]) => {
18 | return fields[quantityPath]?.value as number;
19 | });
20 |
21 | const handleUpdate = useCallback(() => {
22 | setValue(unitPriceValue * quantityValue);
23 | }, [unitPriceValue, quantityValue, setValue]);
24 |
25 | useEffect(() => {
26 | handleUpdate();
27 | }, [unitPriceValue, quantityValue, handleUpdate]);
28 |
29 | return ;
30 | };
31 |
--------------------------------------------------------------------------------
/src/schemas/registerForm.schema.ts:
--------------------------------------------------------------------------------
1 | import { useTranslations } from "next-intl";
2 | import { z } from "zod";
3 |
4 | export type RegisterFormData = {
5 | email: string;
6 | password: string;
7 | confirmPassword: string;
8 | };
9 |
10 | export const RegisterFormSchemaServer = z
11 | .object({
12 | email: z.string().nonempty().email(),
13 | password: z.string().nonempty().min(8),
14 | confirmPassword: z.string().nonempty(),
15 | })
16 | .refine((data) => data.password === data.confirmPassword, {
17 | path: ["confirmPassword"],
18 | });
19 |
20 | export const useRegisterFormSchema = () => {
21 | const t = useTranslations("RegisterForm.errors");
22 |
23 | const RegisterFormSchema = z
24 | .object({
25 | email: z.string().nonempty(t("email-empty")).email(t("email")),
26 | password: z.string().nonempty(t("password")).min(8, t("password-length")),
27 | confirmPassword: z.string().nonempty(t("password")),
28 | })
29 | .refine((data) => data.password === data.confirmPassword, {
30 | message: t("passwords-mismatch"),
31 | path: ["confirmPassword"],
32 | });
33 |
34 | return { RegisterFormSchema };
35 | };
36 |
--------------------------------------------------------------------------------
/src/blocks/Code/Component.client.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Highlight, themes } from "prism-react-renderer";
4 |
5 | import { CopyButton } from "./CopyButton";
6 |
7 | type Props = {
8 | code: string;
9 | language?: string;
10 | };
11 |
12 | export const Code = ({ code, language = "" }: Props) => {
13 | if (!code) return null;
14 |
15 | return (
16 |
17 | {({ getLineProps, getTokenProps, tokens }) => (
18 |
19 | {tokens.map((line, i) => (
20 |
21 | {i + 1}
22 |
23 | {line.map((token, key) => (
24 |
25 | ))}
26 |
27 |
28 | ))}
29 |
30 |
31 | )}
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/src/collections/(ecommerce)/Customers/hooks/sendWelcomeEmail.ts:
--------------------------------------------------------------------------------
1 | import { render } from "@react-email/components";
2 | import { getLocale, getTranslations } from "next-intl/server";
3 |
4 | import { WelcomeEmail } from "@/components/Emails/WelcomeEmail";
5 | import { type Locale } from "@/i18n/config";
6 | import { type Customer } from "@/payload-types";
7 | import { sendEmail } from "@/utilities/nodemailer";
8 |
9 | import type { CollectionAfterChangeHook } from "payload";
10 |
11 | export const sendWelcomeEmail: CollectionAfterChangeHook = async ({ previousDoc, doc }) => {
12 | if (previousDoc._verified === false && doc._verified === true) {
13 | try {
14 | const locale = (await getLocale()) as Locale;
15 |
16 | const html = await render(
17 | await WelcomeEmail({
18 | customer: doc,
19 | locale,
20 | }),
21 | );
22 |
23 | const t = await getTranslations({ locale, namespace: "Emails.welcome" });
24 |
25 | const res = await sendEmail({ to: doc.email, subject: t("subject"), html });
26 | console.log(res);
27 | } catch (error) {
28 | console.log(error);
29 | }
30 | }
31 |
32 | return doc;
33 | };
34 |
--------------------------------------------------------------------------------
/src/stores/Currency/CurrencySelector/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
4 |
5 | import { useCurrency } from "..";
6 | import { type Currency } from "../types";
7 |
8 | export const CurrencySelector = ({ currencyOptions }: { currencyOptions: string[] }) => {
9 | const { setCurrency, currency } = useCurrency();
10 |
11 | const onCurrencyChange = (currencyToSet: Currency) => {
12 | if (currencyToSet !== currency) {
13 | setCurrency(currencyToSet);
14 | }
15 | };
16 |
17 | return (
18 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
4 | import { Check } from "lucide-react";
5 | import * as React from "react";
6 |
7 | import { cn } from "src/utilities/cn";
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
22 |
23 |
24 |
25 | ));
26 | Checkbox.displayName = CheckboxPrimitive.Root.displayName;
27 |
28 | export { Checkbox };
29 |
--------------------------------------------------------------------------------
/src/globals/(ecommerce)/Layout/ClientPanel/variants/WithSidebar/index.tsx:
--------------------------------------------------------------------------------
1 | import { LogOutIcon } from "lucide-react";
2 | import { useTranslations } from "next-intl";
3 | import { type ReactNode } from "react";
4 |
5 | import { LogoutButton } from "@/components/(ecommerce)/LogoutButton";
6 |
7 | import { AsideMenu } from "./components/AsideMenu";
8 |
9 | export const WithSidebar = ({ children }: { children: ReactNode }) => {
10 | const t = useTranslations("Account.wrapper");
11 | return (
12 |
13 |
14 |
{t("my-account")}
15 |
16 |
17 | {t("logout")}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | {children}
26 |
27 |
28 |
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "esModuleInterop": true,
5 | "target": "ES2022",
6 | "lib": [
7 | "DOM",
8 | "DOM.Iterable",
9 | "ES2022"
10 | ],
11 | "allowJs": true,
12 | "skipLibCheck": true,
13 | "strict": false,
14 | "strictNullChecks": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "noEmit": true,
17 | "incremental": true,
18 | "jsx": "preserve",
19 | "module": "esnext",
20 | "moduleResolution": "bundler",
21 | "resolveJsonModule": true,
22 | "sourceMap": true,
23 | "isolatedModules": true,
24 | "plugins": [
25 | {
26 | "name": "next"
27 | }
28 | ],
29 | "paths": {
30 | "@payload-config": [
31 | "./src/payload.config.ts"
32 | ],
33 | "react": [
34 | "./node_modules/@types/react"
35 | ],
36 | "@/*": [
37 | "./src/*"
38 | ],
39 | }
40 | },
41 | "include": [
42 | "**/*.ts",
43 | "**/*.tsx",
44 | ".next/types/**/*.ts",
45 | "redirects.js",
46 | "next.config.mjs",
47 | "next-sitemap.config.cjs"
48 | ],
49 | "exclude": [
50 | "node_modules"
51 | ]
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/search/Component.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useRouter } from "next/navigation";
3 | import { useState, useEffect } from "react";
4 |
5 | import { Input } from "@/components/ui/input";
6 | import { Label } from "@/components/ui/label";
7 | import { useDebounce } from "@/utilities/useDebounce";
8 |
9 | export const Search = () => {
10 | const [value, setValue] = useState("");
11 | const router = useRouter();
12 |
13 | const debouncedValue = useDebounce(value);
14 |
15 | useEffect(() => {
16 | router.push(`/search${debouncedValue ? `?q=${debouncedValue}` : ""}`);
17 | }, [debouncedValue, router]);
18 |
19 | return (
20 |
21 |
40 |
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/src/providers/HeaderTheme/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { createContext, useCallback, useContext, useState } from "react";
4 |
5 | import canUseDOM from "@/utilities/canUseDOM";
6 |
7 | import type { Theme } from "@/providers/Theme/types";
8 |
9 | export type ContextType = {
10 | headerTheme?: Theme | null;
11 | setHeaderTheme: (theme: Theme | null) => void;
12 | };
13 |
14 | const initialContext: ContextType = {
15 | headerTheme: undefined,
16 | setHeaderTheme: () => null,
17 | };
18 |
19 | const HeaderThemeContext = createContext(initialContext);
20 |
21 | export const HeaderThemeProvider = ({ children }: { children: React.ReactNode }) => {
22 | const [headerTheme, setThemeState] = useState(
23 | canUseDOM ? (document.documentElement.getAttribute("data-theme") as Theme) : undefined,
24 | );
25 |
26 | const setHeaderTheme = useCallback((themeToSet: Theme | null) => {
27 | setThemeState(themeToSet);
28 | }, []);
29 |
30 | return (
31 |
32 | {children}
33 |
34 | );
35 | };
36 |
37 | export const useHeaderTheme = (): ContextType => useContext(HeaderThemeContext);
38 |
--------------------------------------------------------------------------------
/src/utilities/getCustomer.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { unstable_cache } from "next/cache";
4 | import { headers as getHeaders, cookies } from "next/headers";
5 | import { getPayload } from "payload";
6 |
7 | import config from "@payload-config";
8 |
9 | export const getCustomer = async () => {
10 | const headers = await getHeaders();
11 | const cookieStore = await cookies();
12 | const cookieString = cookieStore.toString();
13 |
14 | const customer = await unstable_cache(
15 | async () => {
16 | try {
17 | const payload = await getPayload({ config });
18 |
19 | const { user } = await payload.auth({
20 | headers,
21 | });
22 |
23 | // console.log("User on login:", user);
24 | // console.log("Cookies on login:", cookieStore.toString());
25 |
26 | if (!user || user.collection !== "customers") {
27 | return null;
28 | }
29 |
30 | return user;
31 | } catch (error) {
32 | console.error("Auth error:", error);
33 | return null;
34 | }
35 | },
36 | ["user-auth", cookieString],
37 | {
38 | revalidate: 1,
39 | tags: ["user-auth"],
40 | },
41 | )();
42 |
43 | return customer ?? undefined;
44 | };
45 |
--------------------------------------------------------------------------------
/src/app/(frontend)/next/seed/route.ts:
--------------------------------------------------------------------------------
1 | import { headers } from "next/headers";
2 | import { createLocalReq, getPayload } from "payload";
3 |
4 | import { seed } from "@/endpoints/seed";
5 | import config from "@payload-config";
6 |
7 | export const maxDuration = 240; // This function can run for a maximum of 240 seconds
8 |
9 | export async function POST(): Promise {
10 | const payload = await getPayload({ config });
11 | const requestHeaders = await headers();
12 |
13 | // Authenticate by passing request headers
14 | const { user } = await payload.auth({ headers: requestHeaders });
15 |
16 | if (!user) {
17 | return new Response("Action forbidden.", { status: 403 });
18 | }
19 |
20 | try {
21 | // Create a Payload request object to pass to the Local API for transactions
22 | // At this point you should pass in a user, locale, and any other context you need for the Local API
23 | const payloadReq = await createLocalReq({ user }, payload);
24 |
25 | await seed({ req: payloadReq });
26 |
27 | return Response.json({ success: true });
28 | } catch (e) {
29 | payload.logger.error({ err: e as Error, message: "Error seeding data" });
30 | return new Response("Error seeding data.", { status: 500 });
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/search/fieldOverrides.ts:
--------------------------------------------------------------------------------
1 | import { type Field } from "payload";
2 |
3 | export const searchFields: Field[] = [
4 | {
5 | name: "slug",
6 | type: "text",
7 | index: true,
8 | admin: {
9 | readOnly: true,
10 | },
11 | },
12 | {
13 | name: "meta",
14 | label: "Meta",
15 | type: "group",
16 | index: true,
17 | admin: {
18 | readOnly: true,
19 | },
20 | fields: [
21 | {
22 | type: "text",
23 | name: "title",
24 | label: "Title",
25 | },
26 | {
27 | type: "text",
28 | name: "description",
29 | label: "Description",
30 | },
31 | {
32 | name: "image",
33 | label: "Image",
34 | type: "upload",
35 | relationTo: "media",
36 | },
37 | ],
38 | },
39 | {
40 | label: "Categories",
41 | name: "categories",
42 | type: "array",
43 | admin: {
44 | readOnly: true,
45 | },
46 | fields: [
47 | {
48 | name: "relationTo",
49 | type: "text",
50 | },
51 | {
52 | name: "id",
53 | type: "text",
54 | },
55 | {
56 | name: "title",
57 | type: "text",
58 | },
59 | ],
60 | },
61 | ];
62 |
--------------------------------------------------------------------------------
/src/collections/(ecommerce)/Orders/hooks/generateID.ts:
--------------------------------------------------------------------------------
1 | import { getPayload, type CollectionBeforeValidateHook } from "payload";
2 |
3 | import { type Order } from "@/payload-types";
4 | import config from "@payload-config";
5 |
6 | export const generateID: CollectionBeforeValidateHook = async ({ data }) => {
7 | if (data && !data.id) {
8 | const payload = await getPayload({ config });
9 | let attempts = 0;
10 | let uniqueFound = false;
11 |
12 | while (!uniqueFound && attempts < 5) {
13 | const lastOrder = await payload.find({
14 | collection: "orders",
15 | sort: "-id",
16 | limit: 1,
17 | });
18 |
19 | const lastID = lastOrder.docs[0]?.id ?? "000000000";
20 | const newID = (parseInt(lastID) + 1).toString().padStart(8, "0");
21 |
22 | const existing = await payload.find({
23 | collection: "orders",
24 | where: {
25 | id: { equals: newID },
26 | },
27 | });
28 |
29 | if (existing.docs.length === 0) {
30 | uniqueFound = true;
31 | return { ...data, id: newID };
32 | }
33 |
34 | attempts++;
35 | }
36 |
37 | if (!uniqueFound) {
38 | throw new Error("Could not generate unique ID");
39 | }
40 | }
41 |
42 | return data;
43 | };
44 |
--------------------------------------------------------------------------------
/src/collections/Posts/hooks/populateAuthors.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import { type Administrator } from "@/payload-types";
3 |
4 | import type { CollectionAfterReadHook } from "payload";
5 |
6 | // The `user` collection has access control locked so that users are not publicly accessible
7 | // This means that we need to populate the authors manually here to protect user privacy
8 | // GraphQL will not return mutated user data that differs from the underlying schema
9 | // So we use an alternative `populatedAuthors` field to populate the user data, hidden from the admin UI
10 | export const populateAuthors: CollectionAfterReadHook = async ({ doc, req, req: { payload } }) => {
11 | if (doc?.authors) {
12 | const authorDocs: Administrator[] = [];
13 |
14 | for (const author of doc.authors) {
15 | const authorDoc = await payload.findByID({
16 | id: typeof author === "object" ? author?.id : author,
17 | collection: "administrators",
18 | depth: 0,
19 | req,
20 | });
21 |
22 | if (authorDoc) {
23 | authorDocs.push(authorDoc);
24 | }
25 | }
26 |
27 | doc.populatedAuthors = authorDocs.map((authorDoc) => ({
28 | id: authorDoc.id,
29 | name: authorDoc.name,
30 | }));
31 | }
32 |
33 | return doc;
34 | };
35 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "npm.packageManager": "pnpm",
3 | "editor.defaultFormatter": "esbenp.prettier-vscode",
4 | "[typescript]": {
5 | "editor.defaultFormatter": "esbenp.prettier-vscode",
6 | "editor.formatOnSave": true,
7 | "editor.codeActionsOnSave": {
8 | "source.fixAll.eslint": "explicit"
9 | }
10 | },
11 | "[typescriptreact]": {
12 | "editor.defaultFormatter": "esbenp.prettier-vscode",
13 | "editor.formatOnSave": true,
14 | "editor.codeActionsOnSave": {
15 | "source.fixAll.eslint": "explicit"
16 | }
17 | },
18 | "[javascript]": {
19 | "editor.defaultFormatter": "esbenp.prettier-vscode",
20 | "editor.formatOnSave": true,
21 | "editor.codeActionsOnSave": {
22 | "source.fixAll.eslint": "explicit"
23 | }
24 | },
25 | "[json]": {
26 | "editor.defaultFormatter": "esbenp.prettier-vscode",
27 | "editor.formatOnSave": true
28 | },
29 | "[jsonc]": {
30 | "editor.defaultFormatter": "esbenp.prettier-vscode",
31 | "editor.formatOnSave": true
32 | },
33 | "editor.formatOnSaveMode": "file",
34 | "typescript.tsdk": "node_modules/typescript/lib",
35 | "[javascript][typescript][typescriptreact]": {
36 | "editor.codeActionsOnSave": {
37 | "source.fixAll.eslint": "explicit"
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/Media/VideoMedia/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useRef } from "react";
4 |
5 | import { getClientSideURL } from "@/utilities/getURL";
6 | import { cn } from "src/utilities/cn";
7 |
8 | import type { Props as MediaProps } from "../types";
9 |
10 | export const VideoMedia = (props: MediaProps) => {
11 | const { onClick, resource, videoClassName } = props;
12 |
13 | const videoRef = useRef(null);
14 | // const [showFallback] = useState()
15 |
16 | useEffect(() => {
17 | const { current: video } = videoRef;
18 | if (video) {
19 | video.addEventListener("suspend", () => {
20 | // setShowFallback(true);
21 | // console.warn('Video was suspended, rendering fallback image.')
22 | });
23 | }
24 | }, []);
25 |
26 | if (resource && typeof resource === "object") {
27 | const { filename } = resource;
28 |
29 | return (
30 |
42 | );
43 | }
44 |
45 | return null;
46 | };
47 |
--------------------------------------------------------------------------------
/src/collections/(ecommerce)/ProductReviews/index.ts:
--------------------------------------------------------------------------------
1 | import { type CollectionConfig } from "payload";
2 |
3 | export const ProductReviews: CollectionConfig = {
4 | slug: "productReviews",
5 | access: {},
6 | admin: {
7 | group: {
8 | en: "Products",
9 | pl: "Produkty",
10 | },
11 | },
12 | labels: {
13 | singular: {
14 | en: "Product Review",
15 | pl: "Opinia o produkcie",
16 | },
17 | plural: {
18 | en: "Product Reviews",
19 | pl: "Opinie o produktach",
20 | },
21 | },
22 | fields: [
23 | {
24 | name: "product",
25 | type: "relationship",
26 | relationTo: "products",
27 | required: true,
28 | },
29 | {
30 | name: "author",
31 | label: {
32 | pl: "Autor opinii",
33 | en: "Review author",
34 | },
35 | type: "relationship",
36 | relationTo: "customers",
37 | required: true,
38 | },
39 | {
40 | name: "rating",
41 | label: {
42 | pl: "Ocena",
43 | en: "Rating",
44 | },
45 | type: "number",
46 | required: true,
47 | max: 5,
48 | min: 1,
49 | },
50 | {
51 | name: "review",
52 | label: {
53 | pl: "Treść opinii",
54 | en: "Review content",
55 | },
56 | type: "richText",
57 | },
58 | ],
59 | };
60 |
--------------------------------------------------------------------------------
/src/utilities/generatePreviewPath.ts:
--------------------------------------------------------------------------------
1 | import { type PayloadRequest, type CollectionSlug } from "payload";
2 |
3 | import { type Locale } from "@/i18n/config";
4 |
5 | const collectionPrefixMap: Partial> = {
6 | posts: "/posts",
7 | pages: "",
8 | };
9 |
10 | type Props = {
11 | collection: keyof typeof collectionPrefixMap;
12 | slug: string;
13 | req: PayloadRequest;
14 | };
15 |
16 | export const generatePreviewPath1 = ({ collection, slug, req }: Props) => {
17 | const locale = req.query.locale as Locale;
18 | const path = `${collectionPrefixMap[collection]}/${slug}`;
19 |
20 | const params = {
21 | locale,
22 | slug,
23 | collection,
24 | path,
25 | };
26 |
27 | const encodedParams = new URLSearchParams();
28 |
29 | Object.entries(params).forEach(([key, value]) => {
30 | encodedParams.append(key, value);
31 | });
32 |
33 | const isProduction =
34 | process.env.NODE_ENV === "production" || Boolean(process.env.VERCEL_PROJECT_PRODUCTION_URL);
35 | const protocol = isProduction ? "https:" : req.protocol;
36 |
37 | const url = `${protocol}//${req.host}/next/preview?${encodedParams.toString()}`;
38 |
39 | return url;
40 | };
41 |
42 | export const generatePreviewPath = ({ path, locale }) =>
43 | `/next/preview?path=${encodeURIComponent(path as string)}&locale=${locale}`;
44 |
--------------------------------------------------------------------------------
/src/collections/(ecommerce)/Orders/hooks/sendStatusEmail.ts:
--------------------------------------------------------------------------------
1 | import { render } from "@react-email/components";
2 | import { getLocale, getTranslations } from "next-intl/server";
3 |
4 | import { OrderStatusEmail } from "@/components/Emails/OrderStatusEmail";
5 | import { type Locale } from "@/i18n/config";
6 | import { type Order } from "@/payload-types";
7 | import { sendEmail } from "@/utilities/nodemailer";
8 |
9 | import type { FieldHook } from "payload";
10 |
11 | export const sendStatusEmail: FieldHook = async ({
12 | operation,
13 | value,
14 | originalDoc,
15 | }) => {
16 | if (operation !== "update" || !originalDoc || !value) return value;
17 |
18 | const disabledStatuses: Order["orderDetails"]["status"][] = ["cancelled", "completed", "pending"];
19 |
20 | try {
21 | const order = originalDoc;
22 | const locale = (await getLocale()) as Locale;
23 | const t = await getTranslations("Order");
24 |
25 | if (!disabledStatuses.includes(value)) {
26 | const html = await render(await OrderStatusEmail({ locale, order }));
27 |
28 | await sendEmail({
29 | html,
30 | subject: t(`${order.orderDetails.status}.title`),
31 | to: order.shippingAddress.email,
32 | });
33 | }
34 | } catch (error) {
35 | console.log(error);
36 | }
37 | return value;
38 | };
39 |
--------------------------------------------------------------------------------
/src/components/(ecommerce)/AdminDashboard/components/OverviewCard/index.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from "@payloadcms/ui";
2 | import { type ReactNode } from "react";
3 |
4 | import {
5 | type CustomTranslationsKeys,
6 | type CustomTranslationsObject,
7 | } from "@/admin/translations/custom-translations";
8 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
9 |
10 | export const OverviewCard = ({
11 | label,
12 | value,
13 | percentage,
14 | icon,
15 | }: {
16 | label: string;
17 | value: string | number;
18 | percentage: number;
19 | icon: ReactNode;
20 | }) => {
21 | const { t } = useTranslation();
22 | return (
23 |
24 |
25 | {label}
26 | {icon}
27 |
28 |
29 | {value}
30 |
31 | +{percentage}% {t("adminDashboard:from-last-month")}
32 |
33 |
34 |
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/src/components/AdminResetPassword/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useTranslation } from "@payloadcms/ui";
3 | import axios from "axios";
4 | import { useState } from "react";
5 |
6 | import {
7 | type CustomTranslationsKeys,
8 | type CustomTranslationsObject,
9 | } from "@/admin/translations/custom-translations";
10 |
11 | export const AdminResetPassword = () => {
12 | const [message, setMessage] = useState("");
13 | const handleResetPassword = async () => {
14 | setMessage("");
15 | const emailInput = document.getElementById("field-email") as HTMLInputElement;
16 | if (emailInput) {
17 | const email = emailInput.value;
18 | try {
19 | const res = await axios.post("/next/reset-password", { email, collection: "administrators" });
20 | console.log(res);
21 | if (res.status === 200) {
22 | setMessage(t("custom:resetPasswordSuccess"));
23 | }
24 | } catch {
25 | setMessage(t("custom:resetPasswordError"));
26 | }
27 | }
28 | };
29 | const { t } = useTranslation();
30 | return (
31 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as PopoverPrimitive from "@radix-ui/react-popover";
4 | import * as React from "react";
5 |
6 | import { cn } from "@/utilities/cn";
7 |
8 | const Popover = PopoverPrimitive.Root;
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger;
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ));
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName;
30 |
31 | export { Popover, PopoverTrigger, PopoverContent };
32 |
--------------------------------------------------------------------------------
/src/components/search/beforeSync.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import { type BeforeSync, type DocToSync } from "@payloadcms/plugin-search/types";
3 |
4 | export const beforeSyncWithSearch: BeforeSync = async ({ originalDoc, searchDoc, payload }) => {
5 | const {
6 | doc: { relationTo: collection },
7 | } = searchDoc;
8 |
9 | const { slug, id, categories, title, meta, excerpt } = originalDoc;
10 |
11 | const modifiedDoc: DocToSync = {
12 | ...searchDoc,
13 | slug,
14 | meta: {
15 | ...meta,
16 | title: meta?.title || title,
17 | image: meta?.image?.id || meta?.image,
18 | description: meta?.description,
19 | },
20 | categories: [],
21 | };
22 |
23 | if (categories && Array.isArray(categories) && categories.length > 0) {
24 | // get full categories and keep a flattened copy of their most important properties
25 | try {
26 | const mappedCategories = categories.map((category) => {
27 | const { id, title } = category;
28 |
29 | return {
30 | relationTo: "categories",
31 | id,
32 | title,
33 | };
34 | });
35 |
36 | modifiedDoc.categories = mappedCategories;
37 | } catch (err) {
38 | console.error(
39 | `Failed. Category not found when syncing collection '${collection}' with id: '${id}' to search.`,
40 | );
41 | }
42 | }
43 |
44 | return modifiedDoc;
45 | };
46 |
--------------------------------------------------------------------------------
/src/providers/Theme/InitTheme/index.tsx:
--------------------------------------------------------------------------------
1 | import Script from "next/script";
2 |
3 | import { defaultTheme, themeLocalStorageKey } from "../ThemeSelector/types";
4 |
5 | export const InitTheme = () => {
6 | return (
7 | // eslint-disable-next-line
8 |
48 | );
49 | };
50 |
--------------------------------------------------------------------------------
/src/collections/(ecommerce)/Orders/components/ProductNameField/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useField, useFormFields, useLocale } from "@payloadcms/ui";
3 | import axios from "axios";
4 | import { type TextFieldClientComponent } from "payload";
5 | import { stringify } from "qs-esm";
6 | import { useCallback, useEffect } from "react";
7 |
8 | import { type Product } from "@/payload-types";
9 |
10 | export const ProductNameField: TextFieldClientComponent = ({ path }) => {
11 | const { setValue } = useField({ path });
12 | const locale = useLocale();
13 |
14 | const productFieldPath = path.replace("productName", "product");
15 | const productID = useFormFields(([fields]) => {
16 | return fields[productFieldPath].value as string;
17 | });
18 |
19 | const query = stringify(
20 | {
21 | select: {
22 | title: true,
23 | },
24 | },
25 | { addQueryPrefix: true },
26 | );
27 |
28 | const fetchProduct = useCallback(async () => {
29 | try {
30 | const { data } = await axios.get(`/api/products/${productID}${query}&locale=${locale.code}`, {
31 | withCredentials: true,
32 | });
33 | setValue(data.title);
34 | } catch (error) {
35 | console.log(error);
36 | }
37 | }, [productID, locale.code, query, setValue]);
38 |
39 | useEffect(() => {
40 | void fetchProduct();
41 | }, [fetchProduct]);
42 |
43 | return null;
44 | };
45 |
--------------------------------------------------------------------------------
/src/collections/Posts/hooks/revalidatePost.ts:
--------------------------------------------------------------------------------
1 | import { revalidatePath, revalidateTag } from "next/cache";
2 |
3 | import type { Post } from "@/payload-types";
4 | import type { CollectionAfterChangeHook, CollectionAfterDeleteHook } from "payload";
5 |
6 | export const revalidatePost: CollectionAfterChangeHook = ({
7 | doc,
8 | previousDoc,
9 | req: { payload, context },
10 | }) => {
11 | if (!context.disableRevalidate) {
12 | if (doc._status === "published") {
13 | const path = `/posts/${doc.slug}`;
14 |
15 | payload.logger.info(`Revalidating post at path: ${path}`);
16 |
17 | revalidatePath(path);
18 | revalidateTag("posts-sitemap");
19 | }
20 |
21 | // If the post was previously published, we need to revalidate the old path
22 | if (previousDoc._status === "published" && doc._status !== "published") {
23 | const oldPath = `/posts/${previousDoc.slug}`;
24 |
25 | payload.logger.info(`Revalidating old post at path: ${oldPath}`);
26 |
27 | revalidatePath(oldPath);
28 | revalidateTag("posts-sitemap");
29 | }
30 | }
31 | return doc;
32 | };
33 |
34 | export const revalidateDelete: CollectionAfterDeleteHook = ({ doc, req: { context } }) => {
35 | if (!context.disableRevalidate) {
36 | const path = `/posts/${doc?.slug}`;
37 |
38 | revalidatePath(path);
39 | revalidateTag("posts-sitemap");
40 | }
41 |
42 | return doc;
43 | };
44 |
--------------------------------------------------------------------------------
/src/globals/(ecommerce)/Layout/ProductDetails/Component.tsx:
--------------------------------------------------------------------------------
1 | import { notFound } from "next/navigation";
2 | import { getLocale } from "next-intl/server";
3 | import { type ReactNode } from "react";
4 |
5 | import { type Locale } from "@/i18n/config";
6 | import { type Product } from "@/payload-types";
7 | import { getCachedGlobal } from "@/utilities/getGlobals";
8 |
9 | import { WithImageGalleryExpandableDetails } from "./variants/WithImageGalleryExpandableDetails";
10 |
11 | import { ProductBreadcrumbs } from "../../../../components/(ecommerce)/ProductBreadcrumbs";
12 |
13 | export const ProductDetails = async ({ variant, product }: { variant?: string; product: Product }) => {
14 | const locale = (await getLocale()) as Locale;
15 | const { productDetails } = await getCachedGlobal("shopLayout", locale, 1)();
16 |
17 | let ProductDetailsComponent: ReactNode = null;
18 | switch (productDetails.type) {
19 | case "WithImageGalleryExpandableDetails":
20 | ProductDetailsComponent = (
21 |
26 | );
27 | break;
28 | }
29 |
30 | if (!ProductDetailsComponent) {
31 | notFound();
32 | }
33 |
34 | return (
35 | <>
36 |
37 | {ProductDetailsComponent}
38 | >
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/src/utilities/getMeUser.ts:
--------------------------------------------------------------------------------
1 | import { cookies } from "next/headers";
2 |
3 | import { type Locale } from "@/i18n/config";
4 | import { redirect } from "@/i18n/routing";
5 |
6 | import { getClientSideURL } from "./getURL";
7 |
8 | import type { Administrator } from "@/payload-types";
9 |
10 | export const getMeUser = async (args?: {
11 | nullUserRedirect?: string;
12 | locale?: Locale;
13 | validUserRedirect?: string;
14 | }): Promise<{
15 | token: string;
16 | user: Administrator;
17 | }> => {
18 | const { nullUserRedirect, validUserRedirect, locale } = args ?? {};
19 | const cookieStore = await cookies();
20 | const token = cookieStore.get("payload-token")?.value;
21 |
22 | const meUserReq = await fetch(`${getClientSideURL()}/api/administrators/me`, {
23 | headers: {
24 | Authorization: `JWT ${token}`,
25 | },
26 | });
27 |
28 | const {
29 | user,
30 | }: {
31 | user: Administrator;
32 | } = (await meUserReq.json()) as { user: Administrator };
33 |
34 | if (validUserRedirect && meUserReq.ok && user) {
35 | return redirect({ locale: locale ?? "en", href: validUserRedirect });
36 | }
37 |
38 | if (nullUserRedirect && (!meUserReq.ok || !user)) {
39 | return redirect({ locale: locale ?? "en", href: nullUserRedirect });
40 | }
41 |
42 | // Token will exist here because if it doesn't the user will be redirected
43 | return {
44 | token: token!,
45 | user,
46 | };
47 | };
48 |
--------------------------------------------------------------------------------
/src/components/Emails/VerifyAccountEmail/index.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Html, Text } from "@react-email/components";
2 | import { getTranslations } from "next-intl/server";
3 | import * as React from "react";
4 |
5 | import { type Locale } from "@/i18n/config";
6 |
7 | export const VerifyAccountEmail = async ({
8 | url,
9 | locale,
10 | name,
11 | }: {
12 | url: string;
13 | locale: Locale;
14 | name: string;
15 | }) => {
16 | const t = await getTranslations({ locale, namespace: "Emails.verify-email" });
17 | return (
18 |
19 |
28 | {t("greeting", { name })},
29 |
30 |
39 | {t("body")}
40 |
41 |
53 |
54 | );
55 | };
56 |
--------------------------------------------------------------------------------
/src/utilities/generateMeta.ts:
--------------------------------------------------------------------------------
1 | import { getServerSideURL } from "./getURL";
2 | import { mergeOpenGraph } from "./mergeOpenGraph";
3 |
4 | import type { Media, Page, Post, Config } from "@/payload-types";
5 | import type { Metadata } from "next";
6 |
7 | const getImageURL = (image?: Media | Config["db"]["defaultIDType"] | null) => {
8 | const serverUrl = getServerSideURL();
9 |
10 | let url = serverUrl + "/website-template-OG.webp";
11 |
12 | if (image && typeof image === "object" && "url" in image) {
13 | const ogUrl = image.sizes?.og?.url;
14 |
15 | url = ogUrl ? serverUrl + ogUrl : serverUrl + image.url;
16 | }
17 |
18 | return url;
19 | };
20 |
21 | export const generateMeta = async (args: { doc: Partial | Partial }): Promise => {
22 | const { doc } = args || {};
23 |
24 | const ogImage = getImageURL(doc?.meta?.image);
25 |
26 | const title = doc?.meta?.title
27 | ? doc?.meta?.title + " | Payload Ecommerce Template"
28 | : "Payload Ecommerce Template";
29 |
30 | return {
31 | description: doc?.meta?.description,
32 | openGraph: mergeOpenGraph({
33 | description: doc?.meta?.description ?? "",
34 | images: ogImage
35 | ? [
36 | {
37 | url: ogImage,
38 | },
39 | ]
40 | : undefined,
41 | title,
42 | url: Array.isArray(doc?.slug) ? doc?.slug.join("/") : "/",
43 | }),
44 | title,
45 | };
46 | };
47 |
--------------------------------------------------------------------------------
/src/collections/(ecommerce)/ProductSubCategories/index.ts:
--------------------------------------------------------------------------------
1 | import { type CollectionConfig } from "payload";
2 |
3 | import { slugField } from "@/fields/slug";
4 |
5 | export const ProductSubCategories: CollectionConfig = {
6 | slug: "productSubCategories",
7 | admin: {
8 | useAsTitle: "title",
9 | group: {
10 | en: "Products",
11 | pl: "Produkty",
12 | },
13 | },
14 | labels: {
15 | singular: {
16 | en: "Product Subcategory",
17 | pl: "Podkategoria produktu",
18 | },
19 | plural: {
20 | en: "Product Subcategories",
21 | pl: "Podkategorie produktów",
22 | },
23 | },
24 | fields: [
25 | {
26 | name: "category",
27 | type: "relationship",
28 | relationTo: "productCategories",
29 | label: {
30 | en: "Parent category",
31 | pl: "Kategoria nadrzędna",
32 | },
33 | required: true,
34 | },
35 | {
36 | name: "title",
37 | label: {
38 | en: "Subcategory name",
39 | pl: "Nazwa podkategorii",
40 | },
41 | type: "text",
42 | required: true,
43 | localized: true,
44 | },
45 | ...slugField(),
46 | {
47 | name: "products",
48 | label: {
49 | en: "Products in this category",
50 | pl: "Produkty w tej kategorii",
51 | },
52 | type: "join",
53 | collection: "products",
54 | on: "categoriesArr.subcategories",
55 | },
56 | ],
57 | };
58 |
--------------------------------------------------------------------------------
/src/blocks/Form/Checkbox/index.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import {
3 | useFormContext,
4 | type FieldErrorsImpl,
5 | type FieldValues,
6 | type UseFormRegister,
7 | } from "react-hook-form";
8 |
9 | import { Checkbox as CheckboxUi } from "@/components/ui/checkbox";
10 | import { Label } from "@/components/ui/label";
11 |
12 | import { Error } from "../Error";
13 | import { Width } from "../Width";
14 |
15 | import type { CheckboxField } from "@payloadcms/plugin-form-builder/types";
16 |
17 | export const Checkbox = ({
18 | name,
19 | defaultValue,
20 | errors,
21 | label,
22 | register,
23 | required: requiredFromProps,
24 | width,
25 | }: CheckboxField & {
26 | errors: Partial>>;
27 | getValues: any;
28 | register: UseFormRegister;
29 | setValue: any;
30 | }) => {
31 | const props = register(name, { required: requiredFromProps });
32 | const { setValue } = useFormContext();
33 |
34 | return (
35 |
36 |
37 | {
42 | setValue(props.name, checked);
43 | }}
44 | />
45 |
46 |
47 | {requiredFromProps && errors[name] && }
48 |
49 | );
50 | };
51 |
--------------------------------------------------------------------------------
/src/blocks/CallToAction/Component.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | paddingBottomClasses,
3 | paddingTopClasses,
4 | spacingBottomClasses,
5 | spacingTopClasses,
6 | } from "@/blocks/globals";
7 | import { CMSLink } from "@/components/Link";
8 | import RichText from "@/components/RichText";
9 | import { cn } from "@/utilities/cn";
10 |
11 | import type { CallToActionBlock as CTABlockProps } from "@/payload-types";
12 |
13 | export const CallToActionBlock = ({
14 | links,
15 | richText,
16 | spacingTop,
17 | spacingBottom,
18 | paddingBottom,
19 | paddingTop,
20 | }: CTABlockProps) => {
21 | return (
22 |
31 |
32 |
33 | {richText && }
34 |
35 |
36 | {(links ?? []).map(({ link }, i) => {
37 | return ;
38 | })}
39 |
40 |
41 |
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/src/components/Emails/ResetPasswordEmail/index.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Html, Text } from "@react-email/components";
2 | import { getTranslations } from "next-intl/server";
3 | import * as React from "react";
4 |
5 | import { type Locale } from "@/i18n/config";
6 |
7 | export const ResetPasswordEmail = async ({
8 | url,
9 | locale,
10 | name,
11 | }: {
12 | url: string;
13 | locale: Locale;
14 | name: string;
15 | }) => {
16 | const t = await getTranslations({ locale, namespace: "Emails.reset-password" });
17 | console.log(name);
18 | return (
19 |
20 |
29 | {t("greeting", { name })},
30 |
31 |
40 | {t("message")}
41 |
42 |
54 |
55 | );
56 | };
57 |
--------------------------------------------------------------------------------
/src/utilities/nodemailer.ts:
--------------------------------------------------------------------------------
1 | import nodemailer from "nodemailer";
2 |
3 | import { getCachedGlobal } from "./getGlobals";
4 |
5 | type EmailPayload = {
6 | to: string;
7 | subject: string;
8 | html: string;
9 | };
10 |
11 | type EmailResponse = {
12 | success: boolean;
13 | messageId: string;
14 | };
15 |
16 | const createEmailTransporter = async () => {
17 | const { smtp } = await getCachedGlobal("emailMessages", "en", 1)();
18 |
19 | const { host, fromEmail, password, port, secure, user } = smtp ?? {};
20 |
21 | return {
22 | transporter: nodemailer.createTransport({
23 | host: host ?? process.env.SMTP_HOST,
24 | port: Number(port ?? 587),
25 | secure: secure ?? false,
26 | auth: { user: user ?? process.env.SMTP_USER, pass: password ?? process.env.SMTP_PASS },
27 | }),
28 | fromEmail: fromEmail ?? process.env.SMTP_USER,
29 | };
30 | };
31 |
32 | export const sendEmail = async ({ to, subject, html }: EmailPayload): Promise => {
33 | const { transporter, fromEmail } = await createEmailTransporter();
34 |
35 | try {
36 | const { messageId } = await transporter.sendMail({
37 | from: fromEmail,
38 | to,
39 | subject,
40 | html,
41 | });
42 |
43 | return { success: true, messageId };
44 | } catch (error) {
45 | const errorMessage = error instanceof Error ? error.message : "Unknown email error";
46 | throw new Error(`Failed to send email: ${errorMessage}`);
47 | }
48 | };
49 |
--------------------------------------------------------------------------------
/src/components/(ecommerce)/CurrencySelect/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { FieldLabel, Select, useField } from "@payloadcms/ui";
4 | import axios from "axios";
5 | import { type TextFieldClientComponent } from "payload";
6 | import { useEffect, useState } from "react";
7 |
8 | import { type ShopSetting } from "@/payload-types";
9 |
10 | export const CurrencySelect: TextFieldClientComponent = ({ path }) => {
11 | const { value, setValue } = useField({ path });
12 | const [options, setOptions] = useState<
13 | {
14 | label: string;
15 | value: string;
16 | }[]
17 | >([]);
18 |
19 | useEffect(() => {
20 | const fetchOptions = async () => {
21 | try {
22 | const { data } = await axios.get("/api/globals/shopSettings");
23 | setOptions(
24 | data.availableCurrencies.map((currency) => ({
25 | label: currency,
26 | value: currency,
27 | })),
28 | );
29 | } catch {
30 | setOptions([]);
31 | }
32 | };
33 | void fetchOptions();
34 | }, []);
35 |
36 | return (
37 |
38 |
39 |
48 | );
49 | };
50 |
--------------------------------------------------------------------------------
/src/providers/Theme/ThemeSelector/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 |
5 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
6 |
7 | import { themeLocalStorageKey, type Theme } from "./types";
8 |
9 | import { useTheme } from "..";
10 |
11 | export const ThemeSelector = () => {
12 | const { setTheme } = useTheme();
13 | // Lazy initialization - only runs once
14 | const [value, setValue] = useState(() => {
15 | if (typeof window !== 'undefined') {
16 | return window.localStorage.getItem(themeLocalStorageKey) ?? "auto";
17 | }
18 | return "auto";
19 | });
20 |
21 | const onThemeChange = (themeToSet: Theme & "auto") => {
22 | if (themeToSet === "auto") {
23 | setTheme(null);
24 | setValue("auto");
25 | } else {
26 | setTheme(themeToSet);
27 | setValue(themeToSet);
28 | }
29 | };
30 |
31 | return (
32 |
45 | );
46 | };
47 |
--------------------------------------------------------------------------------
/src/collections/Pages/hooks/revalidatePage.ts:
--------------------------------------------------------------------------------
1 | import { revalidatePath, revalidateTag } from "next/cache";
2 |
3 | import type { Page } from "@/payload-types";
4 | import type { CollectionAfterChangeHook, CollectionAfterDeleteHook } from "payload";
5 |
6 | export const revalidatePage: CollectionAfterChangeHook = ({
7 | doc,
8 | previousDoc,
9 | req: { payload, context },
10 | }) => {
11 | if (!context.disableRevalidate) {
12 | if (doc._status === "published") {
13 | const path = doc.slug === "home" ? "/" : `/${doc.slug}`;
14 |
15 | payload.logger.info(`Revalidating page at path: ${path}`);
16 |
17 | revalidatePath(path);
18 | revalidateTag("pages-sitemap");
19 | }
20 |
21 | // If the page was previously published, we need to revalidate the old path
22 | if (previousDoc?._status === "published" && doc._status !== "published") {
23 | const oldPath = previousDoc.slug === "home" ? "/" : `/${previousDoc.slug}`;
24 |
25 | payload.logger.info(`Revalidating old page at path: ${oldPath}`);
26 |
27 | revalidatePath(oldPath);
28 | revalidateTag("pages-sitemap");
29 | }
30 | }
31 | return doc;
32 | };
33 |
34 | export const revalidateDelete: CollectionAfterDeleteHook = ({ doc, req: { context } }) => {
35 | if (!context.disableRevalidate) {
36 | const path = doc?.slug === "home" ? "/" : `/${doc?.slug}`;
37 | revalidatePath(path);
38 | revalidateTag("pages-sitemap");
39 | }
40 |
41 | return doc;
42 | };
43 |
--------------------------------------------------------------------------------
/src/collections/(ecommerce)/Customers/hooks/createTokenAndSendEmail.ts:
--------------------------------------------------------------------------------
1 | import { render } from "@react-email/components";
2 | import { getLocale, getTranslations } from "next-intl/server";
3 |
4 | import { VerifyAccountEmail } from "@/components/Emails/VerifyAccountEmail";
5 | import { type Locale } from "@/i18n/config";
6 | import { sendEmail } from "@/utilities/nodemailer";
7 |
8 | import type { CollectionAfterOperationHook } from "payload";
9 |
10 | export const createTokenAndSendEmail: CollectionAfterOperationHook<"customers"> = async ({
11 | operation,
12 | result,
13 | req,
14 | }) => {
15 | const payload = req.payload;
16 | if (operation !== "create" || !result) return result;
17 |
18 | const user = await payload.findByID({
19 | collection: "customers",
20 | id: result.id,
21 | req,
22 | showHiddenFields: true,
23 | });
24 |
25 | try {
26 | const locale = (await getLocale()) as Locale;
27 |
28 | const html = await render(
29 | await VerifyAccountEmail({
30 | url: `${process.env.NEXT_PUBLIC_SERVER_URL}/next/verify-email?token=${user._verificationToken}`,
31 | locale,
32 | name: result.firstName ?? "Customer",
33 | }),
34 | );
35 |
36 | const t = await getTranslations({ locale, namespace: "Emails.verify-email" });
37 |
38 | const res = await sendEmail({ to: result.email, subject: t("subject"), html });
39 | console.log(res);
40 | } catch (error) {
41 | console.log(error);
42 | }
43 |
44 | return result;
45 | };
46 |
--------------------------------------------------------------------------------
/src/collections/(ecommerce)/ProductCategories/index.ts:
--------------------------------------------------------------------------------
1 | import { type CollectionConfig } from "payload";
2 |
3 | import { anyone } from "@/access/anyone";
4 | import { slugField } from "@/fields/slug";
5 |
6 | export const ProductCategories: CollectionConfig = {
7 | slug: "productCategories",
8 | admin: {
9 | useAsTitle: "title",
10 | group: {
11 | en: "Products",
12 | pl: "Produkty",
13 | },
14 | },
15 | labels: {
16 | singular: {
17 | en: "Product Category",
18 | pl: "Kateogria produktu",
19 | },
20 | plural: {
21 | en: "Product Categories",
22 | pl: "Kategorie produktów",
23 | },
24 | },
25 | access: {
26 | read: anyone,
27 | },
28 | fields: [
29 | {
30 | name: "title",
31 | label: {
32 | en: "Category name",
33 | pl: "Nazwa kategorii",
34 | },
35 | type: "text",
36 | required: true,
37 | localized: true,
38 | },
39 | ...slugField(),
40 | {
41 | name: "subcategories",
42 | label: {
43 | en: "Related subcategories",
44 | pl: "Powiązane podkategorie",
45 | },
46 | type: "join",
47 | collection: "productSubCategories",
48 | on: "category",
49 | },
50 | {
51 | name: "products",
52 | label: {
53 | en: "Products in this category",
54 | pl: "Produkty w tej kategorii",
55 | },
56 | type: "join",
57 | collection: "products",
58 | on: "categoriesArr.category",
59 | },
60 | ],
61 | };
62 |
--------------------------------------------------------------------------------
/src/collections/(ecommerce)/Orders/utils/getShippingLabel.ts:
--------------------------------------------------------------------------------
1 | import axios, { isAxiosError } from "axios";
2 | import { type SetStateAction } from "react";
3 |
4 | export const getShippingLabel = async ({
5 | setIsDownloading,
6 | setError,
7 | orderID,
8 | }: {
9 | setIsDownloading: (value: SetStateAction) => void;
10 | setError: (value: SetStateAction) => void;
11 | orderID: string;
12 | }) => {
13 | setIsDownloading(true);
14 | try {
15 | const response = await axios.get(`/next/printLabel?orderID=${orderID}`, {
16 | responseType: "blob",
17 | });
18 |
19 | const blob = new Blob([response.data], { type: "application/pdf" });
20 | const url = window.URL.createObjectURL(blob);
21 | const link = document.createElement("a");
22 | link.href = url;
23 | link.setAttribute("download", `${orderID}.pdf`);
24 | document.body.appendChild(link);
25 | link.click();
26 | link.remove();
27 | window.URL.revokeObjectURL(url);
28 | } catch (error) {
29 | if (isAxiosError(error) && error.response?.data instanceof Blob) {
30 | const text: string = await error.response.data.text();
31 | // eslint-disable-next-line
32 | const errorData = JSON.parse(text);
33 | console.log("Error:", errorData);
34 | setError((errorData as string) || "Error downloading file");
35 | } else {
36 | console.log("Unknown error:", error);
37 | setError("Unknown error occurred");
38 | }
39 | } finally {
40 | setIsDownloading(false);
41 | }
42 | };
43 |
--------------------------------------------------------------------------------
/src/components/heros/MediumImpact/index.tsx:
--------------------------------------------------------------------------------
1 | import { CMSLink } from "@/components/Link";
2 | import { Media } from "@/components/Media";
3 | import RichText from "@/components/RichText";
4 | import { cn } from "@/utilities/cn";
5 |
6 | import type { Page } from "@/payload-types";
7 |
8 | export const MediumImpactHero = ({ links, media, richText, reversed }: Page["hero"]) => {
9 | return (
10 |
16 |
17 | {richText &&
}
18 |
19 | {Array.isArray(links) && links.length > 0 && (
20 |
21 | {links.map(({ link }, i) => {
22 | return (
23 | -
24 |
25 |
26 | );
27 | })}
28 |
29 | )}
30 |
31 |
32 | {media && typeof media === "object" && (
33 |
34 |
35 | {media?.caption && (
36 |
37 |
38 |
39 | )}
40 |
41 | )}
42 |
43 |
44 | );
45 | };
46 |
--------------------------------------------------------------------------------
/src/lib/paywalls/getAutopayPaymentURL.ts:
--------------------------------------------------------------------------------
1 | import { createHash } from "crypto";
2 |
3 | import axios from "axios";
4 |
5 | import { type Paywall } from "@/payload-types";
6 | import { type Currency } from "@/stores/Currency/types";
7 |
8 | export const getAutopayPaymentURL = async ({
9 | total,
10 | autopay,
11 | orderID,
12 | currency,
13 | customerEmail,
14 | }: {
15 | total: number;
16 | autopay: Paywall["autopay"];
17 | orderID: string;
18 | currency: Currency;
19 | customerEmail: string;
20 | }) => {
21 | const serviceID = autopay?.serviceID ?? "";
22 | const hashKey = autopay?.hashKey ?? "";
23 | const endpoint = autopay?.endpoint ?? "";
24 | try {
25 | const data = {
26 | ServiceID: serviceID,
27 | OrderID: orderID,
28 | Amount: total.toString(),
29 | GatewayID: "0",
30 | Currency: currency,
31 | CustomerEmail: customerEmail,
32 | Hash: createHash("sha256")
33 | .update(`${serviceID}|${orderID}|${total.toString()}|0|${currency}|${customerEmail}|${hashKey}`)
34 | .digest("hex"),
35 | };
36 |
37 | const formData = new URLSearchParams(data);
38 |
39 | const { data: response } = await axios.post(endpoint, formData, {
40 | headers: {
41 | BmHeader: "pay-bm-continue-transaction-url",
42 | "Content-Type": "application/x-www-form-urlencoded",
43 | },
44 | });
45 |
46 | console.log(response);
47 |
48 | return response;
49 | } catch (error) {
50 | console.log(error);
51 | return null;
52 | }
53 | };
54 |
--------------------------------------------------------------------------------
/src/app/(frontend)/next/wishListProducts/route.ts:
--------------------------------------------------------------------------------
1 | import { getPayload } from "payload";
2 |
3 | import { type Locale } from "@/i18n/config";
4 | import { getFilledProducts } from "@/lib/getFilledProducts";
5 | import { type WishList } from "@/stores/WishlistStore/types";
6 | import config from "@payload-config";
7 |
8 | export async function POST(req: Request) {
9 | try {
10 | const payload = await getPayload({ config });
11 | const { wishlist, locale }: { wishlist: WishList | undefined; locale: Locale } = (await req.json()) as {
12 | wishlist: WishList | undefined;
13 | locale: Locale;
14 | };
15 | if (!wishlist) {
16 | return Response.json({ status: 200 });
17 | }
18 |
19 | const { docs: products } = await payload.find({
20 | collection: "products",
21 | where: {
22 | id: {
23 | in: wishlist.map((product) => product.id),
24 | },
25 | },
26 | locale,
27 | select: {
28 | title: true,
29 | price: true,
30 | images: true,
31 | variants: true,
32 | enableVariants: true,
33 | enableVariantPrices: true,
34 | colors: true,
35 | slug: true,
36 | sizes: true,
37 | pricing: true,
38 | },
39 | });
40 |
41 | const filledProducts = getFilledProducts(products, wishlist);
42 |
43 | return Response.json({ status: 200, filledProducts });
44 | } catch (error) {
45 | console.log(error);
46 | return Response.json({ status: 500, message: "Internal server error" });
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/blocks/Accordion/Component.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | spacingTopClasses,
5 | spacingBottomClasses,
6 | paddingBottomClasses,
7 | paddingTopClasses,
8 | } from "@/blocks/globals";
9 | import RichText from "@/components/RichText";
10 | import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
11 | import { cn } from "@/utilities/cn";
12 |
13 | import type { AccordionBlock as AccordionBlockProps } from "@/payload-types";
14 |
15 | export const AccordionBlock = ({
16 | spacingBottom,
17 | spacingTop,
18 | title,
19 | items,
20 | paddingBottom,
21 | paddingTop,
22 | }: AccordionBlockProps) => {
23 | return (
24 |
33 | {title && }
34 |
35 | {items.map((item, index) => (
36 |
37 | {item.title}
38 |
39 |
40 |
41 |
42 | ))}
43 |
44 |
45 | );
46 | };
47 |
--------------------------------------------------------------------------------
/src/app/(frontend)/(sitemaps)/posts-sitemap.xml/route.ts:
--------------------------------------------------------------------------------
1 | import { unstable_cache } from "next/cache";
2 | import { getServerSideSitemap } from "next-sitemap";
3 | import { getPayload } from "payload";
4 |
5 | import config from "@payload-config";
6 |
7 | const getPostsSitemap = unstable_cache(
8 | async () => {
9 | const payload = await getPayload({ config });
10 | const SITE_URL =
11 | process.env.NEXT_PUBLIC_SERVER_URL ||
12 | process.env.VERCEL_PROJECT_PRODUCTION_URL ||
13 | "https://example.com";
14 |
15 | const results = await payload.find({
16 | collection: "posts",
17 | overrideAccess: false,
18 | draft: false,
19 | depth: 0,
20 | limit: 1000,
21 | pagination: false,
22 | where: {
23 | _status: {
24 | equals: "published",
25 | },
26 | },
27 | select: {
28 | slug: true,
29 | updatedAt: true,
30 | },
31 | });
32 |
33 | const dateFallback = new Date().toISOString();
34 |
35 | const sitemap = results.docs
36 | ? results.docs
37 | .filter((post) => Boolean(post?.slug))
38 | .map((post) => ({
39 | loc: `${SITE_URL}/posts/${post?.slug}`,
40 | lastmod: post.updatedAt || dateFallback,
41 | }))
42 | : [];
43 |
44 | return sitemap;
45 | },
46 | ["posts-sitemap"],
47 | {
48 | tags: ["posts-sitemap"],
49 | },
50 | );
51 |
52 | export async function GET() {
53 | const sitemap = await getPostsSitemap();
54 |
55 | return getServerSideSitemap(sitemap);
56 | }
57 |
--------------------------------------------------------------------------------
/src/components/PageRange/index.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | const defaultLabels = {
3 | plural: "Docs",
4 | singular: "Doc",
5 | };
6 |
7 | const defaultCollectionLabels = {
8 | posts: {
9 | plural: "Posts",
10 | singular: "Post",
11 | },
12 | };
13 |
14 | export const PageRange = (props: {
15 | className?: string;
16 | collection?: string;
17 | collectionLabels?: {
18 | plural?: string;
19 | singular?: string;
20 | };
21 | currentPage?: number;
22 | limit?: number;
23 | totalDocs?: number;
24 | }) => {
25 | const {
26 | className,
27 | collection,
28 | collectionLabels: collectionLabelsFromProps,
29 | currentPage,
30 | limit,
31 | totalDocs,
32 | } = props;
33 |
34 | let indexStart = (currentPage ? currentPage - 1 : 1) * (limit || 1) + 1;
35 | if (totalDocs && indexStart > totalDocs) indexStart = 0;
36 |
37 | let indexEnd = (currentPage ?? 1) * (limit ?? 1);
38 | if (totalDocs && indexEnd > totalDocs) indexEnd = totalDocs;
39 |
40 | const { plural, singular } =
41 | collectionLabelsFromProps || defaultCollectionLabels[collection || ""] || defaultLabels || {};
42 |
43 | return (
44 |
45 | {(typeof totalDocs === "undefined" || totalDocs === 0) && "Search produced no results."}
46 | {typeof totalDocs !== "undefined" &&
47 | totalDocs > 0 &&
48 | `Showing ${indexStart}${indexStart > 0 ? ` - ${indexEnd}` : ""} of ${totalDocs} ${
49 | totalDocs > 1 ? plural : singular
50 | }`}
51 |
52 | );
53 | };
54 |
--------------------------------------------------------------------------------
/src/fields/noBlocksLexical.ts:
--------------------------------------------------------------------------------
1 | import { LinkFeature, lexicalEditor, BlocksFeature } from "@payloadcms/richtext-lexical";
2 | import { type Config } from "payload";
3 |
4 | // import {
5 | // BgColorFeature,
6 | // HighlightColorFeature,
7 | // TextColorFeature,
8 | // YoutubeFeature,
9 | // VimeoFeature,
10 | // } from "payloadcms-lexical-ext";
11 | import { Carousel } from "@/blocks/Carousel/config";
12 |
13 | export const noBlocksLexical: Config["editor"] = lexicalEditor({
14 | features: ({ defaultFeatures }) => {
15 | return [
16 | ...defaultFeatures,
17 | BlocksFeature({
18 | blocks: [Carousel],
19 | }),
20 | LinkFeature({
21 | enabledCollections: ["pages", "posts"],
22 | fields: ({ defaultFields }) => {
23 | const defaultFieldsWithoutUrl = defaultFields.filter((field) => {
24 | if ("name" in field && field.name === "url") return false;
25 | return true;
26 | });
27 |
28 | return [
29 | ...defaultFieldsWithoutUrl,
30 | {
31 | name: "url",
32 | type: "text",
33 | admin: {
34 | condition: ({ linkType }) => linkType !== "internal",
35 | },
36 | label: ({ t }) => t("fields:enterURL"),
37 | required: true,
38 | },
39 | ];
40 | },
41 | }),
42 | // TextColorFeature(),
43 | // HighlightColorFeature(),
44 | // BgColorFeature(),
45 |
46 | // YoutubeFeature(),
47 | // VimeoFeature(),
48 | ];
49 | },
50 | });
51 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:22-alpine AS base
2 | RUN npm install -g corepack@latest
3 | ENV COREPACK_INTEGRITY_KEYS=0
4 |
5 | FROM base AS deps
6 |
7 | RUN apk add --no-cache libc6-compat
8 | WORKDIR /app
9 |
10 | #COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
11 | #RUN if [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; else echo "Lockfile not found." && exit 1; fi
12 | COPY package.json pnpm-lock.yaml* ./
13 | RUN corepack enable pnpm && pnpm i --frozen-lockfile
14 |
15 |
16 | FROM base AS builder
17 | WORKDIR /app
18 |
19 | ARG PAYLOAD_SECRET=dummy_secret
20 | ARG DATABASE_URI
21 | ENV DATABASE_URI=${DATABASE_URI}
22 | # Don't uncomment- ENV PAYLOAD_SECRET=${PAYLOAD_SECRET}
23 |
24 | COPY --from=deps /app/node_modules ./node_modules
25 | COPY . .
26 |
27 | #RUN if [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; else echo "Lockfile not found." && exit 1; fi
28 | RUN corepack enable pnpm && pnpm run build
29 |
30 | FROM base AS runner
31 | WORKDIR /app
32 |
33 | ENV NODE_ENV=production
34 | ENV NEXT_TELEMETRY_DISABLED=1
35 |
36 | RUN addgroup --system --gid 1001 nodejs
37 | RUN adduser --system --uid 1001 nextjs
38 |
39 | COPY --from=builder /app/public ./public
40 | RUN mkdir -p .next
41 | COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
42 | COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
43 |
44 | RUN chown -R nextjs:nodejs /app
45 | # RUN chown -R nextjs:nodejs ./public
46 |
47 | USER nextjs
48 |
49 | EXPOSE 3000
50 |
51 | ENV PORT=3000
52 | ENV HOSTNAME=0.0.0.0
53 |
54 | CMD ["node", "server.js"]
55 |
--------------------------------------------------------------------------------
/src/components/(ecommerce)/RegisterPage/WithoutOAuth/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Image from "next/image";
4 | import { useTranslations } from "next-intl";
5 |
6 | import { Link } from "@/i18n/routing";
7 |
8 | import { RegisterForm } from "./components/RegisterForm";
9 |
10 | export const RegisterPageWithoutOAuth = () => {
11 | const t = useTranslations("RegisterForm");
12 | return (
13 |
14 |
15 |
16 |
23 |
{t("title")}
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {t("have-account")}{" "}
33 |
34 | {t("login-now")}
35 |
36 |
37 |
38 |
39 |
40 | );
41 | };
42 |
--------------------------------------------------------------------------------
/src/components/heros/HighImpact/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useEffect } from "react";
3 |
4 | import { CMSLink } from "@/components/Link";
5 | import { Media } from "@/components/Media";
6 | import RichText from "@/components/RichText";
7 | import { useHeaderTheme } from "@/providers/HeaderTheme";
8 |
9 | import type { Page } from "@/payload-types";
10 |
11 | export const HighImpactHero = ({ links, media, richText }: Page["hero"]) => {
12 | const { setHeaderTheme } = useHeaderTheme();
13 |
14 | useEffect(() => {
15 | setHeaderTheme("dark");
16 | });
17 |
18 | return (
19 |
20 |
21 |
22 | {richText &&
}
23 | {Array.isArray(links) && links.length > 0 && (
24 |
25 | {links.map(({ link }, i) => {
26 | return (
27 | -
28 |
29 |
30 | );
31 | })}
32 |
33 | )}
34 |
35 |
36 |
37 | {media && typeof media === "object" && (
38 |
39 | )}
40 |
41 |
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/src/components/(ecommerce)/ListingBreadcrumbs/index.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "@/i18n/routing";
2 | import { type ProductCategory, type ProductSubCategory } from "@/payload-types";
3 |
4 | export const ListingBreadcrumbs = ({
5 | category,
6 | subcategory,
7 | }: {
8 | category: ProductCategory;
9 | subcategory?: ProductSubCategory;
10 | }) => {
11 | return (
12 | <>
13 |
40 | >
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/src/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
4 | import { Circle } from "lucide-react";
5 | import * as React from "react";
6 |
7 | import { cn } from "@/utilities/cn";
8 |
9 | const RadioGroup = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => {
13 | return ;
14 | });
15 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
16 |
17 | const RadioGroupItem = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => {
21 | return (
22 |
30 |
31 |
32 |
33 |
34 | );
35 | });
36 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
37 |
38 | export { RadioGroup, RadioGroupItem };
39 |
--------------------------------------------------------------------------------
/src/fields/courierFields.ts:
--------------------------------------------------------------------------------
1 | import { type Field } from "payload";
2 |
3 | import { countryPickerField } from "./countryPickerField";
4 | import { courierSettingsFields } from "./courierSettingsFields";
5 | import { freeShippingField } from "./freeShippingField";
6 | import { weightRangesField } from "./weightRangesField";
7 |
8 | export const courierFields: Field[] = [
9 | {
10 | name: "enabled",
11 | type: "checkbox",
12 | label: {
13 | en: "Enable this courier",
14 | pl: "Włącz tego kuriera",
15 | },
16 | },
17 | {
18 | name: "settings",
19 | label: {
20 | en: "Settings",
21 | pl: "Ustawienia",
22 | },
23 | type: "group",
24 |
25 | fields: courierSettingsFields,
26 | },
27 | {
28 | name: "deliveryZones",
29 | type: "array",
30 | label: {
31 | en: "Delivery zones",
32 | pl: "Strefy dostaw",
33 | },
34 | labels: {
35 | plural: {
36 | en: "Delivery zones",
37 | pl: "Strefy dostaw",
38 | },
39 | singular: {
40 | en: "Delivery zone",
41 | pl: "Strefa dostaw",
42 | },
43 | },
44 |
45 | fields: [countryPickerField, freeShippingField, weightRangesField],
46 | admin: {
47 | components: {
48 | RowLabel: "@/components/(ecommerce)/RowLabels/DeliveryZonesRowLabel#DeliveryZonesRowLabel",
49 | },
50 | },
51 | },
52 | {
53 | name: "icon",
54 | type: "upload",
55 | label: {
56 | en: "Icon",
57 | pl: "Ikona",
58 | },
59 | relationTo: "media",
60 | admin: {
61 | condition: (data) => Boolean(data.enabled),
62 | },
63 | },
64 | ];
65 |
--------------------------------------------------------------------------------
/src/utilities/getPriceRange.ts:
--------------------------------------------------------------------------------
1 | import { type Product } from "@/payload-types";
2 | import { type Currency } from "@/stores/Currency/types";
3 |
4 | export const getPriceRange = (variants: Product["variants"], enableVariantPrices: boolean) => {
5 | if (!variants || !enableVariantPrices) return null;
6 |
7 | const allPrices = variants.flatMap((variant) => variant.pricing ?? []);
8 |
9 | const groupedPrices = allPrices.reduce(
10 | (acc, currentPrice) => {
11 | const currency = currentPrice.currency as Currency;
12 |
13 | if (!acc[currency]) {
14 | acc[currency] = [];
15 | }
16 |
17 | acc[currency].push({
18 | ...currentPrice,
19 | currency: currentPrice.currency as Currency,
20 | id: currentPrice.id ?? undefined,
21 | });
22 | return acc;
23 | },
24 | {} as Record,
25 | );
26 |
27 | const priceRanges: { value: number; currency: Currency; id?: string }[][] = [];
28 | const minPrices: { value: number; currency: Currency; id?: string }[] = [];
29 | const maxPrices: { value: number; currency: Currency; id?: string }[] = [];
30 |
31 | for (const currency in groupedPrices) {
32 | const prices = groupedPrices[currency as Currency];
33 | const sortedPrices = prices.sort((a, b) => a.value - b.value);
34 |
35 | const minPrice = sortedPrices[0];
36 | const maxPrice = sortedPrices[sortedPrices.length - 1];
37 |
38 | minPrices.push(minPrice);
39 | maxPrices.push(maxPrice);
40 | }
41 |
42 | priceRanges.push(minPrices);
43 | priceRanges.push(maxPrices);
44 |
45 | return priceRanges;
46 | };
47 |
--------------------------------------------------------------------------------