chore: merges templates
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
DATABASE_URI=mongodb://127.0.0.1/payload-template-blank
|
||||
MONGODB_URI=mongodb://127.0.0.1/payload-template-blank
|
||||
PAYLOAD_SECRET=YOUR_SECRET_HERE
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
|
||||
payload:
|
||||
image: node:18-alpine
|
||||
ports:
|
||||
- '3000:3000'
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- .:/home/node/app
|
||||
- node_modules:/home/node/app/node_modules
|
||||
@@ -18,7 +19,7 @@ services:
|
||||
mongo:
|
||||
image: mongo:latest
|
||||
ports:
|
||||
- '27017:27017'
|
||||
- "27017:27017"
|
||||
command:
|
||||
- --storageEngine=wiredTiger
|
||||
volumes:
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nodemon.json",
|
||||
"ext": "ts",
|
||||
"exec": "ts-node src/server.ts -- -I",
|
||||
"stdin": false
|
||||
"exec": "ts-node src/server.ts"
|
||||
}
|
||||
|
||||
@@ -28,4 +28,4 @@
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,12 @@
|
||||
import path from 'path'
|
||||
|
||||
import { payloadCloud } from '@payloadcms/plugin-cloud'
|
||||
import { mongooseAdapter } from '@payloadcms/db-mongodb' // database-adapter-import
|
||||
import { webpackBundler } from '@payloadcms/bundler-webpack' // bundler-import
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical' // editor-import
|
||||
import { buildConfig } from 'payload/config'
|
||||
|
||||
import path from 'path'
|
||||
import Users from './collections/Users'
|
||||
import { payloadCloud } from '@payloadcms/plugin-cloud'
|
||||
|
||||
export default buildConfig({
|
||||
admin: {
|
||||
user: Users.slug,
|
||||
bundler: webpackBundler(), // bundler-config
|
||||
},
|
||||
editor: lexicalEditor({}), // editor-config
|
||||
collections: [Users],
|
||||
typescript: {
|
||||
outputFile: path.resolve(__dirname, 'payload-types.ts'),
|
||||
@@ -22,9 +15,4 @@ export default buildConfig({
|
||||
schemaOutputFile: path.resolve(__dirname, 'generated-schema.graphql'),
|
||||
},
|
||||
plugins: [payloadCloud()],
|
||||
// database-adapter-config-start
|
||||
db: mongooseAdapter({
|
||||
url: process.env.DATABASE_URI,
|
||||
}),
|
||||
// database-adapter-config-end
|
||||
})
|
||||
|
||||
@@ -13,6 +13,7 @@ const start = async () => {
|
||||
// Initialize Payload
|
||||
await payload.init({
|
||||
secret: process.env.PAYLOAD_SECRET,
|
||||
mongoURL: process.env.MONGODB_URI,
|
||||
express: app,
|
||||
onInit: async () => {
|
||||
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`)
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"strict": false,
|
||||
"esModuleInterop": true,
|
||||
@@ -10,13 +14,21 @@
|
||||
"rootDir": "./src",
|
||||
"jsx": "react",
|
||||
"paths": {
|
||||
"payload/generated-types": ["./src/payload-types.ts"]
|
||||
"payload/generated-types": [
|
||||
"./src/payload-types.ts",
|
||||
],
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist", "build"],
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"build",
|
||||
],
|
||||
"ts-node": {
|
||||
"transpileOnly": true,
|
||||
"swc": true
|
||||
"swc": true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3942,9 +3942,9 @@ graphql-type-json@^0.3.2:
|
||||
integrity sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg==
|
||||
|
||||
graphql@^16.6.0:
|
||||
version "16.7.1"
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.7.1.tgz#11475b74a7bff2aefd4691df52a0eca0abd9b642"
|
||||
integrity sha512-DRYR9tf+UGU0KOsMcKAlXeFfX89UiiIZ0dRU3mR0yJfu6OjZqUcp68NnFLnqQU5RexygFoDy1EW+ccOYcPfmHg==
|
||||
version "16.8.1"
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07"
|
||||
integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==
|
||||
|
||||
gzip-size@^6.0.0:
|
||||
version "6.0.0"
|
||||
@@ -6072,9 +6072,9 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
|
||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||
|
||||
postcss@^8.2.15, postcss@^8.4.21, postcss@^8.4.24:
|
||||
version "8.4.27"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057"
|
||||
integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==
|
||||
version "8.4.31"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
|
||||
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
|
||||
dependencies:
|
||||
nanoid "^3.3.6"
|
||||
picocolors "^1.0.0"
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
# Payload vars
|
||||
PORT=3000
|
||||
DATABASE_URI=mongodb://127.0.0.1/payload-template-ecommerce
|
||||
MONGODB_URI=mongodb://127.0.0.1/payload-template-ecommerce
|
||||
PAYLOAD_SECRET=712kjbkuh87234sflj98713b
|
||||
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
STRIPE_SECRET_KEY=
|
||||
PAYLOAD_PUBLIC_STRIPE_IS_TEST_KEY=true
|
||||
STRIPE_WEBHOOKS_SIGNING_SECRET=
|
||||
PAYLOAD_PUBLIC_DRAFT_SECRET=demo-draft-secret
|
||||
REVALIDATION_KEY=demo-revalation-key
|
||||
PAYLOAD_PUBLIC_DRAFT_SECRET=EXAMPLE_DRAFT_SECRET
|
||||
REVALIDATION_KEY=EXAMPLE_REVALIDATION_KEY
|
||||
|
||||
# Next.js vars
|
||||
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
|
||||
NEXT_PUBLIC_IS_LIVE=
|
||||
NEXT_PRIVATE_DRAFT_SECRET=demo-draft-secret
|
||||
NEXT_PRIVATE_REVALIDATION_KEY=demo-revalation-key
|
||||
NEXT_PRIVATE_DRAFT_SECRET=EXAMPLE_DRAFT_SECRET
|
||||
NEXT_PRIVATE_REVALIDATION_KEY=EXAMPLE_REVALIDATION_KEY
|
||||
|
||||
8
templates/ecommerce/.prettierrc.js
Normal file
8
templates/ecommerce/.prettierrc.js
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
parser: 'typescript',
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'avoid',
|
||||
}
|
||||
@@ -30,17 +30,17 @@ If you have not done so already, you need to have standalone copy of this repo o
|
||||
|
||||
#### Method 1 (recommended)
|
||||
|
||||
Go to Payload Cloud and [clone this template](https://payloadcms.com/new/clone/ecommerce). This will create a new repository on your GitHub account with this template's code which you can then clone to your own machine.
|
||||
Go to Payload Cloud and [clone this template](https://payloadcms.com/new/clone/ecommerce). This will create a new repository on your GitHub account with this template's code which you can then clone to your own machine.
|
||||
|
||||
#### Method 2
|
||||
|
||||
Use the `create-payload-app` CLI to clone this template directly to your machine:
|
||||
Use the `create-payload-app` CLI to clone this template directly to your machine:
|
||||
|
||||
npx create-payload-app my-project -t ecommerce
|
||||
|
||||
#### Method 3
|
||||
|
||||
Use the `git` CLI to clone this template directly to your machine:
|
||||
Use the `git` CLI to clone this template directly to your machine:
|
||||
|
||||
git clone -n --depth=1 --filter=tree:0 https://github.com/payloadcms/payload my-project && cd my-project && git sparse-checkout set --no-cone templates/ecommerce && git checkout && rm -rf .git && git init && git add . && git mv -f templates/ecommerce/{.,}* . && git add . && git commit -m "Initial commit"
|
||||
|
||||
@@ -59,7 +59,7 @@ The Payload config is tailored specifically to the needs of most e-commerce busi
|
||||
|
||||
### Collections
|
||||
|
||||
See the [Collections](https://payloadcms.com/docs/configuration/collections) docs for details on how to extend this functionality.
|
||||
See the [Collections](https://payloadcms.com/docs/configuration/collections) docs for details on how to extend this functionality.
|
||||
|
||||
- #### Users (Authentication)
|
||||
|
||||
@@ -145,7 +145,6 @@ Logged-in users can have their shopping carts saved to their profiles as they sh
|
||||
Payload itself handles no currency exchange. All payments are processed and billed using [Stripe](https://stripe.com). This means you must have access to a Stripe account via an API key, see [Connect Stripe](#connect-stripe) for how to get one. When you create a product in Payload that you wish to sell, it must be connected to a Stripe product by selecting one from the field in the product's sidebar, see [Products](#products) for more details. Once set, data is automatically synced between the two platforms in the following ways:
|
||||
|
||||
1. Stripe to Payload using [Stripe Webhooks](https://stripe.com/docs/webhooks):
|
||||
|
||||
- `product.created`
|
||||
- `product.updated`
|
||||
- `price.updated`
|
||||
@@ -229,10 +228,22 @@ Create unique product and page layouts for any type fo content using a powerful
|
||||
|
||||
Each block is fully designed and built into the front-end website that comes with this template. See [Website](#website) for more details.
|
||||
|
||||
## Draft Preview
|
||||
|
||||
All pages and products are draft-enabled so you can preview them before publishing them to your website. To do this, these collections use [Versions](https://payloadcms.com/docs/configuration/collections#versions) with `drafts` set to `true`. This means that when you create a new page or product, it will be saved as a draft and will not be visible on your website until you publish it. This also means that you can preview your draft before publishing it to your website. To do this, we automatically format a custom URL which redirects to your front-end to securely fetch the draft version of your content.
|
||||
|
||||
Since the front-end of this template is statically generated, this also means that pages and products will need to be regenerated as changes are made to published documents. To do this, we use an `afterChange` hook to regenerate the front-end when a document has changed and its `_status` is `published`.
|
||||
|
||||
For more details on how to extend this functionality, see the official [Draft Preview Example](https://github.com/payloadcms/payload/tree/master/examples/draft-preview).
|
||||
|
||||
## SEO
|
||||
|
||||
This template comes pre-configured with the official [Payload SEO Plugin](https://github.com/payloadcms/plugin-seo) for complete SEO control from the admin panel. All SEO data is fully integrated into the front-end website that comes with this template. See [Website](#website) for more details.
|
||||
|
||||
## Redirects
|
||||
|
||||
If you are migrating an existing site or moving content to a new URL, you can use the `redirects` collection to create a proper redirect from old URLs to new ones. This will ensure that proper request status codes are returned to search engines and that your users are not left with a broken link. This template comes pre-configured with the official [Payload Redirects Plugin](https://github.com/payloadcms/plugin-redirects) for complete redirect control from the admin panel. All redirects are fully integrated into the front-end website that comes with this template. See [Website](#website) for more details.
|
||||
|
||||
## Website
|
||||
|
||||
This template includes a beautifully designed, production-ready front-end built with the [Next.js App Router](https://nextjs.org), served right alongside your Payload app in a single Express server. This makes is so that you can deploy both apps simultaneously and host them together. If you prefer a different front-end framework, this pattern works for any framework that supports a custom server. If you prefer to host your website separately from Payload, you can easily [Eject](#eject) the front-end out from this template to swap in your own, or to use it as a standalone CMS. For more details, see the official [Custom Server Example](https://github.com/payloadcms/payload/tree/master/examples/custom-server).
|
||||
@@ -245,15 +256,20 @@ Core features:
|
||||
- [TypeScript](https://www.typescriptlang.org)
|
||||
- [React Hook Form](https://react-hook-form.com)
|
||||
- [Payload Admin Bar](https://github.com/payloadcms/payload-admin-bar)
|
||||
- Complete authentication flow
|
||||
- Fully built shopping cart
|
||||
- Complete checkout flow
|
||||
- Authentication
|
||||
- Publication workflow
|
||||
- Shopping cart
|
||||
- Checkout
|
||||
- Customer accounts
|
||||
- Dark mode
|
||||
- Pre-made layout building blocks
|
||||
- Complete SEO integration
|
||||
- Complete Stripe integration
|
||||
- Fully built paywall
|
||||
- SEO
|
||||
- Redirects
|
||||
- Paywall
|
||||
|
||||
### Cache
|
||||
|
||||
Although Next.js includes a robust set of caching strategies out of the box, Payload Cloud proxies and caches all files through Cloudflare using the [Official Cloud Plugin](https://github.com/payloadcms/plugin-cloud). This means that Next.js caching is not needed and is disabled by default. If you are hosting your app outside of Payload Cloud, you can easily reenable the Next.js caching mechanisms by removing the `no-store` directive from all fetch requests in `./src/app/_api` and then removing all instances of `export const dynamic = 'force-dynamic'` from pages files, such as `./src/app/(pages)/[slug]/page.tsx`. For more details, see the official [Next.js Caching Docs](https://nextjs.org/docs/app/building-your-application/caching).
|
||||
|
||||
### Eject
|
||||
|
||||
@@ -263,7 +279,7 @@ If you prefer another front-end framework or would like to use Payload as a stan
|
||||
|
||||
For more details on how setup a custom server, see the official [Custom Server Example](https://github.com/payloadcms/payload/tree/master/examples/custom-server).
|
||||
|
||||
## Development
|
||||
## Development
|
||||
|
||||
To spin up this example locally, follow the [Quick Start](#quick-start). Then [Connect Stripe](#connect-stripe) to enable payments, and [Seed](#seed) the database with a few products and pages.
|
||||
|
||||
|
||||
36
templates/ecommerce/csp.js
Normal file
36
templates/ecommerce/csp.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const policies = {
|
||||
'default-src': ["'self'"],
|
||||
'script-src': [
|
||||
"'self'",
|
||||
"'unsafe-inline'",
|
||||
"'unsafe-eval'",
|
||||
'https://checkout.stripe.com',
|
||||
'https://js.stripe.com',
|
||||
'https://maps.googleapis.com',
|
||||
],
|
||||
'child-src': ["'self'"],
|
||||
'style-src': ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
|
||||
'img-src': ["'self'", 'https://*.stripe.com', 'https://raw.githubusercontent.com'],
|
||||
'font-src': ["'self'"],
|
||||
'frame-src': [
|
||||
"'self'",
|
||||
'https://checkout.stripe.com',
|
||||
'https://js.stripe.com',
|
||||
'https://hooks.stripe.com',
|
||||
],
|
||||
'connect-src': [
|
||||
"'self'",
|
||||
'https://checkout.stripe.com',
|
||||
'https://api.stripe.com',
|
||||
'https://maps.googleapis.com',
|
||||
],
|
||||
}
|
||||
|
||||
module.exports = Object.entries(policies)
|
||||
.map(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
return `${key} ${value.join(' ')}`
|
||||
}
|
||||
return ''
|
||||
})
|
||||
.join('; ')
|
||||
@@ -1,10 +1,11 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
|
||||
payload:
|
||||
image: node:18-alpine
|
||||
ports:
|
||||
- '3000:3000'
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- .:/home/node/app
|
||||
- node_modules:/home/node/app/node_modules
|
||||
@@ -18,7 +19,7 @@ services:
|
||||
mongo:
|
||||
image: mongo:latest
|
||||
ports:
|
||||
- '27017:27017'
|
||||
- "27017:27017"
|
||||
command:
|
||||
- --storageEngine=wiredTiger
|
||||
volumes:
|
||||
|
||||
@@ -6,16 +6,16 @@ import path from 'path'
|
||||
// See `yarn eject` in `package.json` for the exact command
|
||||
// See `./README.md#eject` for more information
|
||||
|
||||
const files = ['./next.config.js', './next-env.d.ts']
|
||||
const files = ['./next.config.js', './next-env.d.ts', './redirects.js']
|
||||
const directories = ['./src/app']
|
||||
|
||||
const eject = async (): Promise<void> => {
|
||||
files.forEach((file) => {
|
||||
files.forEach(file => {
|
||||
fs.unlinkSync(path.join(__dirname, file))
|
||||
})
|
||||
|
||||
directories.forEach((directory) => {
|
||||
fs.rm(path.join(__dirname, directory), { recursive: true }, (err) => {
|
||||
directories.forEach(directory => {
|
||||
fs.rm(path.join(__dirname, directory), { recursive: true }, err => {
|
||||
if (err) throw err
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,33 +1,23 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
|
||||
const ContentSecurityPolicy = `
|
||||
default-src 'self';
|
||||
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://checkout.stripe.com https://js.stripe.com https://maps.googleapis.com;
|
||||
child-src 'self';
|
||||
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
|
||||
img-src 'self' https://*.stripe.com https://raw.githubusercontent.com;
|
||||
font-src 'self';
|
||||
frame-src 'self' https://checkout.stripe.com https://js.stripe.com https://hooks.stripe.com;
|
||||
connect-src 'self' https://checkout.stripe.com https://api.stripe.com https://maps.googleapis.com;
|
||||
`
|
||||
const ContentSecurityPolicy = require('./csp')
|
||||
const redirects = require('./redirects')
|
||||
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
swcMinify: true,
|
||||
images: {
|
||||
domains: ['localhost', process.env.NEXT_PUBLIC_SERVER_URL].filter(Boolean),
|
||||
// remotePatterns: [
|
||||
// {
|
||||
// protocol: 'https',
|
||||
// hostname: 'localhost',
|
||||
// port: '3000',
|
||||
// pathname: '/media/**',
|
||||
// },
|
||||
// ],
|
||||
domains: ['localhost', process.env.NEXT_PUBLIC_SERVER_URL]
|
||||
.filter(Boolean)
|
||||
.map(url => url.replace(/https?:\/\//, '')),
|
||||
},
|
||||
redirects,
|
||||
async headers() {
|
||||
const headers = []
|
||||
|
||||
// Prevent search engines from indexing the site if it is not live
|
||||
// This is useful for staging environments before they are ready to go live
|
||||
// To allow robots to crawl the site, use the `NEXT_PUBLIC_IS_LIVE` env variable
|
||||
// You may want to also use this variable to conditionally render any tracking scripts
|
||||
if (!process.env.NEXT_PUBLIC_IS_LIVE) {
|
||||
headers.push({
|
||||
headers: [
|
||||
@@ -40,12 +30,15 @@ const nextConfig = {
|
||||
})
|
||||
}
|
||||
|
||||
// Set the `Content-Security-Policy` header as a security measure to prevent XSS attacks
|
||||
// It works by explicitly whitelisting trusted sources of content for your website
|
||||
// This will block all inline scripts and styles except for those that are allowed
|
||||
headers.push({
|
||||
source: '/(.*)',
|
||||
headers: [
|
||||
{
|
||||
key: 'Content-Security-Policy',
|
||||
value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim(),
|
||||
value: ContentSecurityPolicy,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nodemon.json",
|
||||
"watch": ["server.ts"],
|
||||
"exec": "ts-node --project tsconfig.server.json src/server.ts -- -I",
|
||||
"exec": "ts-node --project tsconfig.server.json src/server.ts",
|
||||
"ext": "js ts",
|
||||
"stdin": false
|
||||
"ignore" : [
|
||||
"src/app"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -23,8 +23,9 @@
|
||||
"dependencies": {
|
||||
"@payloadcms/plugin-cloud": "^2.0.0",
|
||||
"@payloadcms/plugin-nested-docs": "^1.0.4",
|
||||
"@payloadcms/plugin-redirects": "^1.0.0",
|
||||
"@payloadcms/plugin-seo": "^1.0.10",
|
||||
"@payloadcms/plugin-stripe": "^0.0.13",
|
||||
"@payloadcms/plugin-stripe": "^0.0.14",
|
||||
"@stripe/react-stripe-js": "^1.16.3",
|
||||
"@stripe/stripe-js": "^1.46.0",
|
||||
"cross-env": "^7.0.3",
|
||||
|
||||
81
templates/ecommerce/redirects.js
Normal file
81
templates/ecommerce/redirects.js
Normal file
@@ -0,0 +1,81 @@
|
||||
// Note: This will not work in dev mode and will throw an error upon startup
|
||||
// This is because the Payload APIs are not yet running when the Next.js server starts
|
||||
// This is not a problem in production as Payload is booted up before building Next.js
|
||||
// For this reason the errors can be silently ignored in dev mode
|
||||
|
||||
module.exports = async () => {
|
||||
const internetExplorerRedirect = {
|
||||
source: '/:path((?!ie-incompatible.html$).*)', // all pages except the incompatibility page
|
||||
has: [
|
||||
{
|
||||
type: 'header',
|
||||
key: 'user-agent',
|
||||
value: '(.*Trident.*)', // all ie browsers
|
||||
},
|
||||
],
|
||||
permanent: false,
|
||||
destination: '/ie-incompatible.html',
|
||||
}
|
||||
|
||||
try {
|
||||
const redirectsRes = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_SERVER_URL}/api/redirects?limit=1000&depth=1`,
|
||||
)
|
||||
|
||||
const redirectsData = await redirectsRes.json()
|
||||
const { docs } = redirectsData
|
||||
|
||||
let dynamicRedirects = []
|
||||
|
||||
if (docs) {
|
||||
docs.forEach(doc => {
|
||||
const { from, to: { type, url, reference } = {} } = doc
|
||||
|
||||
let source = from
|
||||
.replace(process.env.NEXT_PUBLIC_SERVER_URL, '')
|
||||
.split('?')[0]
|
||||
.toLowerCase()
|
||||
|
||||
if (source.endsWith('/')) source = source.slice(0, -1) // a trailing slash will break this redirect
|
||||
|
||||
let destination = '/'
|
||||
|
||||
if (type === 'custom' && url) {
|
||||
destination = url.replace(process.env.NEXT_PUBLIC_SERVER_URL, '')
|
||||
}
|
||||
|
||||
if (
|
||||
type === 'reference' &&
|
||||
typeof reference.value === 'object' &&
|
||||
reference?.value?._status === 'published'
|
||||
) {
|
||||
destination = `${process.env.NEXT_PUBLIC_SERVER_URL}/${
|
||||
reference.relationTo !== 'pages' ? `${reference.relationTo}/` : ''
|
||||
}${reference.value.slug}`
|
||||
}
|
||||
|
||||
const redirect = {
|
||||
source,
|
||||
destination,
|
||||
permanent: true,
|
||||
}
|
||||
|
||||
if (source.startsWith('/') && destination && source !== destination) {
|
||||
return dynamicRedirects.push(redirect)
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
const redirects = [internetExplorerRedirect, ...dynamicRedirects]
|
||||
|
||||
return redirects
|
||||
} catch (error) {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
console.error(`Error configuring redirects: ${error}`) // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,14 @@ import { Blocks } from '../../_components/Blocks'
|
||||
import { Hero } from '../../_components/Hero'
|
||||
import { generateMeta } from '../../_utilities/generateMeta'
|
||||
|
||||
// Payload Cloud caches all files through Cloudflare, so we don't need Next.js to cache them as well
|
||||
// This means that we can turn off Next.js data caching and instead rely solely on the Cloudflare CDN
|
||||
// To do this, we include the `no-cache` header on the fetch requests used to get the data for this page
|
||||
// But we also need to force Next.js to dynamically render this page on each request for preview mode to work
|
||||
// See https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic
|
||||
// If you are not using Payload Cloud then this line can be removed, see `../../../README.md#cache`
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default async function Page({ params: { slug = 'home' } }) {
|
||||
const { isEnabled: isDraftMode } = draftMode()
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '../../../_css/common';
|
||||
@import "../../../_css/common";
|
||||
|
||||
.form {
|
||||
margin-bottom: var(--base);
|
||||
|
||||
@@ -143,7 +143,7 @@ const AccountForm: React.FC = () => {
|
||||
label="Confirm Password"
|
||||
required
|
||||
register={register}
|
||||
validate={(value) => value === password.current || 'The passwords do not match'}
|
||||
validate={value => value === password.current || 'The passwords do not match'}
|
||||
error={errors.passwordConfirm}
|
||||
/>
|
||||
</Fragment>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
color: var(--theme-warning-500);
|
||||
}
|
||||
|
||||
:global([data-theme='light']) {
|
||||
:global([data-theme="light"]) {
|
||||
.warning {
|
||||
color: var(--theme-error-500);
|
||||
}
|
||||
@@ -91,7 +91,7 @@
|
||||
padding: calc(var(--base) * 0.25) calc(var(--base) * 0.25);
|
||||
}
|
||||
|
||||
:global([data-theme='dark']) {
|
||||
:global([data-theme="dark"]) {
|
||||
.quantity {
|
||||
background-color: var(--theme-elevation-150);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import classes from './index.module.scss'
|
||||
export const CartPage: React.FC<{
|
||||
settings: Settings
|
||||
page: Page
|
||||
}> = (props) => {
|
||||
}> = props => {
|
||||
const { settings } = props
|
||||
const { productsPage } = settings || {}
|
||||
|
||||
@@ -121,7 +121,7 @@ export const CartPage: React.FC<{
|
||||
// fallback to empty string to avoid uncontrolled input error
|
||||
// this allows the user to user their backspace key to clear the input
|
||||
value={typeof quantity === 'number' ? quantity : ''}
|
||||
onChange={(e) => {
|
||||
onChange={e => {
|
||||
addItemToCart({
|
||||
product,
|
||||
quantity: Number(e.target.value),
|
||||
|
||||
@@ -15,6 +15,10 @@ import { CartPage } from './CartPage'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
// Force this page to be dynamic so that Next.js does not cache it
|
||||
// See the note in '../[slug]/page.tsx' about this
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default async function Cart() {
|
||||
let page: Page | null = null
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '../../../_css/common';
|
||||
@import "../../../_css/common";
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
|
||||
@@ -21,7 +21,7 @@ export const CheckoutForm: React.FC<{}> = () => {
|
||||
const { cart, cartTotal } = useCart()
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (e) => {
|
||||
async e => {
|
||||
e.preventDefault()
|
||||
setIsLoading(true)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '../../../_css/common';
|
||||
@import "../../../_css/common";
|
||||
|
||||
.items {
|
||||
margin-bottom: var(--base);
|
||||
@@ -27,7 +27,7 @@
|
||||
color: var(--theme-warning-500);
|
||||
}
|
||||
|
||||
:global([data-theme='light']) {
|
||||
:global([data-theme="light"]) {
|
||||
.warning {
|
||||
color: var(--theme-error-500);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ const stripe = loadStripe(apiKey)
|
||||
|
||||
export const CheckoutPage: React.FC<{
|
||||
settings: Settings
|
||||
}> = (props) => {
|
||||
}> = props => {
|
||||
const {
|
||||
settings: { productsPage },
|
||||
} = props
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '../../_css/common';
|
||||
@import "../../_css/common";
|
||||
|
||||
.checkoutPage {
|
||||
margin-bottom: var(--block-padding);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '../../../_css/common';
|
||||
@import "../../../_css/common";
|
||||
|
||||
.form {
|
||||
margin-bottom: var(--base);
|
||||
|
||||
@@ -101,7 +101,7 @@ const CreateAccountForm: React.FC = () => {
|
||||
label="Confirm Password"
|
||||
required
|
||||
register={register}
|
||||
validate={(value) => value === password.current || 'The passwords do not match'}
|
||||
validate={value => value === password.current || 'The passwords do not match'}
|
||||
error={errors.passwordConfirm}
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '../../_css/common';
|
||||
@import "../../_css/common";
|
||||
|
||||
.createAccount {
|
||||
margin-bottom: var(--block-padding);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '../../../_css/common';
|
||||
@import "../../../_css/common";
|
||||
|
||||
.form {
|
||||
margin-bottom: var(--base);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '../../_css/common';
|
||||
@import "../../_css/common";
|
||||
|
||||
.login {
|
||||
margin-bottom: var(--block-padding);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useAuth } from '../../../_providers/Auth'
|
||||
|
||||
export const LogoutPage: React.FC<{
|
||||
settings: Settings
|
||||
}> = (props) => {
|
||||
}> = props => {
|
||||
const { settings } = props
|
||||
const { productsPage } = settings || {}
|
||||
const { logout } = useAuth()
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
color: var(--theme-warning-500);
|
||||
}
|
||||
|
||||
:global([data-theme='light']) {
|
||||
:global([data-theme="light"]) {
|
||||
.warning {
|
||||
color: var(--theme-error-500);
|
||||
}
|
||||
@@ -120,3 +120,4 @@
|
||||
flex-wrap: wrap;
|
||||
gap: calc(var(--base) / 2);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ export default async function Order({ params: { id } }) {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `JWT ${token}`,
|
||||
},
|
||||
})?.then(async (res) => {
|
||||
})?.then(async res => {
|
||||
if (!res.ok) notFound()
|
||||
const json = await res.json()
|
||||
if ('error' in json && json.error) notFound()
|
||||
|
||||
@@ -31,14 +31,14 @@ export default async function Orders() {
|
||||
},
|
||||
cache: 'no-store',
|
||||
})
|
||||
?.then(async (res) => {
|
||||
?.then(async res => {
|
||||
if (!res.ok) notFound()
|
||||
const json = await res.json()
|
||||
if ('error' in json && json.error) notFound()
|
||||
if ('errors' in json && json.errors) notFound()
|
||||
return json
|
||||
})
|
||||
?.then((json) => json.docs)
|
||||
?.then(json => json.docs)
|
||||
} catch (error) {
|
||||
// when deploying this template on Payload Cloud, this page needs to build before the APIs are live
|
||||
// so swallow the error here and simply render the page with fallback data where necessary
|
||||
|
||||
@@ -11,6 +11,10 @@ import { PaywallBlocks } from '../../../_components/PaywallBlocks'
|
||||
import { ProductHero } from '../../../_heros/Product'
|
||||
import { generateMeta } from '../../../_utilities/generateMeta'
|
||||
|
||||
// Force this page to be dynamic so that Next.js does not cache it
|
||||
// See the note in '../../../[slug]/page.tsx' about this
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default async function Product({ params: { slug } }) {
|
||||
const { isEnabled: isDraftMode } = draftMode()
|
||||
|
||||
@@ -30,13 +34,54 @@ export default async function Product({ params: { slug } }) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const { layout } = product
|
||||
const { layout, relatedProducts } = product
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ProductHero product={product} />
|
||||
<Blocks blocks={layout} />
|
||||
{product?.enablePaywall && <PaywallBlocks productSlug={slug as string} disableTopPadding />}
|
||||
<Blocks
|
||||
disableTopPadding
|
||||
blocks={[
|
||||
{
|
||||
blockType: 'relatedProducts',
|
||||
blockName: 'Related Product',
|
||||
relationTo: 'products',
|
||||
introContent: [
|
||||
{
|
||||
type: 'h4',
|
||||
children: [
|
||||
{
|
||||
text: 'Related Products',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'p',
|
||||
children: [
|
||||
{
|
||||
text: 'The products displayed here are individually selected for this page. Admins can select any number of related products to display here and the layout will adjust accordingly. Alternatively, you could swap this out for the "Archive" block to automatically populate products by category complete with pagination. To manage related posts, ',
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
url: `/admin/collections/products/${product.id}`,
|
||||
children: [
|
||||
{
|
||||
text: 'navigate to the admin dashboard',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
docs: relatedProducts,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '../../../_css/common';
|
||||
@import "../../../_css/common";
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
@@ -20,3 +20,4 @@
|
||||
.message {
|
||||
margin-bottom: var(--base);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '../../_css/common';
|
||||
@import "../../_css/common";
|
||||
|
||||
.recoverPassword {
|
||||
margin-bottom: var(--block-padding);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '../../../_css/common';
|
||||
@import "../../../_css/common";
|
||||
|
||||
.form {
|
||||
width: 66.66%;
|
||||
|
||||
@@ -44,6 +44,7 @@ export const fetchDoc = async <T>(args: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token?.value && draft ? { Authorization: `JWT ${token.value}` } : {}),
|
||||
},
|
||||
cache: 'no-store',
|
||||
next: { tags: [`${collection}_${slug}`] },
|
||||
body: JSON.stringify({
|
||||
query: queryMap[collection].query,
|
||||
@@ -53,8 +54,8 @@ export const fetchDoc = async <T>(args: {
|
||||
},
|
||||
}),
|
||||
})
|
||||
?.then((res) => res.json())
|
||||
?.then((res) => {
|
||||
?.then(res => res.json())
|
||||
?.then(res => {
|
||||
if (res.errors) throw new Error(res?.errors?.[0]?.message ?? 'Error fetching doc')
|
||||
return res?.data?.[queryMap[collection].key]?.docs?.[0]
|
||||
})
|
||||
|
||||
@@ -40,13 +40,14 @@ export const fetchDocs = async <T>(
|
||||
'Content-Type': 'application/json',
|
||||
...(token?.value && draft ? { Authorization: `JWT ${token.value}` } : {}),
|
||||
},
|
||||
cache: 'no-store',
|
||||
next: { tags: [collection] },
|
||||
body: JSON.stringify({
|
||||
query: queryMap[collection].query,
|
||||
}),
|
||||
})
|
||||
?.then((res) => res.json())
|
||||
?.then((res) => {
|
||||
?.then(res => res.json())
|
||||
?.then(res => {
|
||||
if (res.errors) throw new Error(res?.errors?.[0]?.message ?? 'Error fetching docs')
|
||||
|
||||
return res?.data?.[queryMap[collection].key]?.docs
|
||||
|
||||
@@ -9,15 +9,16 @@ export async function fetchSettings(): Promise<Settings> {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
cache: 'no-store',
|
||||
body: JSON.stringify({
|
||||
query: SETTINGS_QUERY,
|
||||
}),
|
||||
})
|
||||
?.then((res) => {
|
||||
?.then(res => {
|
||||
if (!res.ok) throw new Error('Error fetching doc')
|
||||
return res.json()
|
||||
})
|
||||
?.then((res) => {
|
||||
?.then(res => {
|
||||
if (res?.errors) throw new Error(res?.errors[0]?.message || 'Error fetching settings')
|
||||
return res.data?.Settings
|
||||
})
|
||||
@@ -33,15 +34,16 @@ export async function fetchHeader(): Promise<Header> {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
cache: 'no-store',
|
||||
body: JSON.stringify({
|
||||
query: HEADER_QUERY,
|
||||
}),
|
||||
})
|
||||
?.then((res) => {
|
||||
?.then(res => {
|
||||
if (!res.ok) throw new Error('Error fetching doc')
|
||||
return res.json()
|
||||
})
|
||||
?.then((res) => {
|
||||
?.then(res => {
|
||||
if (res?.errors) throw new Error(res?.errors[0]?.message || 'Error fetching header')
|
||||
return res.data?.Header
|
||||
})
|
||||
@@ -61,11 +63,11 @@ export async function fetchFooter(): Promise<Footer> {
|
||||
query: FOOTER_QUERY,
|
||||
}),
|
||||
})
|
||||
.then((res) => {
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error('Error fetching doc')
|
||||
return res.json()
|
||||
})
|
||||
?.then((res) => {
|
||||
?.then(res => {
|
||||
if (res?.errors) throw new Error(res?.errors[0]?.message || 'Error fetching footer')
|
||||
return res.data?.Footer
|
||||
})
|
||||
|
||||
@@ -21,6 +21,7 @@ export const getMe = async (args?: {
|
||||
Authorization: `JWT ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
cache: 'no-store',
|
||||
body: JSON.stringify({
|
||||
query: ME_QUERY,
|
||||
}),
|
||||
|
||||
@@ -11,7 +11,7 @@ export const ArchiveBlock: React.FC<
|
||||
ArchiveBlockProps & {
|
||||
id?: string
|
||||
}
|
||||
> = (props) => {
|
||||
> = props => {
|
||||
const {
|
||||
introContent,
|
||||
id,
|
||||
@@ -37,7 +37,7 @@ export const ArchiveBlock: React.FC<
|
||||
populatedDocsTotal={populatedDocsTotal}
|
||||
categories={categories}
|
||||
limit={limit}
|
||||
sort="-publishedDate"
|
||||
sort="-publishedOn"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ $spacer-h: calc(var(--block-padding) / 2);
|
||||
color: var(--theme-elevation-1000);
|
||||
}
|
||||
|
||||
:global([data-theme='dark']) {
|
||||
:global([data-theme="dark"]) {
|
||||
.callToAction {
|
||||
background-color: var(--theme-elevation-0);
|
||||
color: var(--theme-elevation-1000);
|
||||
@@ -22,7 +22,7 @@ $spacer-h: calc(var(--block-padding) / 2);
|
||||
color: var(--theme-elevation-0);
|
||||
}
|
||||
|
||||
:global([data-theme='dark']) {
|
||||
:global([data-theme="dark"]) {
|
||||
.invert {
|
||||
background-color: var(--theme-elevation-900);
|
||||
color: var(--theme-elevation-0);
|
||||
|
||||
@@ -13,7 +13,7 @@ export const ContentBlock: React.FC<
|
||||
Props & {
|
||||
id?: string
|
||||
}
|
||||
> = (props) => {
|
||||
> = props => {
|
||||
const { columns } = props
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
|
||||
.caption {
|
||||
color: var(--theme-elevation-500);
|
||||
margin-top: var(--base);
|
||||
margin-top: var(--base)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ type Props = Extract<Page['layout'][0], { blockType: 'mediaBlock' }> & {
|
||||
id?: string
|
||||
}
|
||||
|
||||
export const MediaBlock: React.FC<Props> = (props) => {
|
||||
export const MediaBlock: React.FC<Props> = props => {
|
||||
const { media, position = 'default', staticImage } = props
|
||||
|
||||
let caption
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
@import '../../_css/common';
|
||||
|
||||
.introContent {
|
||||
position: relative;
|
||||
margin-bottom: calc(var(--base) * 2);
|
||||
|
||||
@include mid-break {
|
||||
margin-bottom: var(--base);
|
||||
}
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
width: 100%;
|
||||
gap: var(--base) 40px;
|
||||
|
||||
@include mid-break {
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: calc(var(--base) / 2) var(--base);
|
||||
}
|
||||
}
|
||||
|
||||
.column {
|
||||
grid-column-end: span 12;
|
||||
|
||||
@include mid-break {
|
||||
grid-column-end: span 6;
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
grid-column-end: span 6;
|
||||
}
|
||||
}
|
||||
|
||||
.cols-half {
|
||||
grid-column-end: span 6;
|
||||
|
||||
@include mid-break {
|
||||
grid-column-end: span 6;
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
grid-column-end: span 6;
|
||||
}
|
||||
}
|
||||
|
||||
.cols-thirds {
|
||||
grid-column-end: span 3;
|
||||
|
||||
@include mid-break {
|
||||
grid-column-end: span 6;
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
grid-column-end: span 6;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import React from 'react'
|
||||
|
||||
import { Product } from '../../../payload/payload-types'
|
||||
import { Card } from '../../_components/Card'
|
||||
import { Gutter } from '../../_components/Gutter'
|
||||
import RichText from '../../_components/RichText'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export type RelatedProductsProps = {
|
||||
blockType: 'relatedProducts'
|
||||
blockName: string
|
||||
introContent?: any
|
||||
docs?: (string | Product)[]
|
||||
relationTo: 'products'
|
||||
}
|
||||
|
||||
export const RelatedProducts: React.FC<RelatedProductsProps> = props => {
|
||||
const { introContent, docs, relationTo } = props
|
||||
|
||||
return (
|
||||
<div className={classes.relatedProducts}>
|
||||
{introContent && (
|
||||
<Gutter className={classes.introContent}>
|
||||
<RichText content={introContent} />
|
||||
</Gutter>
|
||||
)}
|
||||
<Gutter>
|
||||
<div className={classes.grid}>
|
||||
{docs?.map((doc, index) => {
|
||||
if (typeof doc === 'string') return null
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={[
|
||||
classes.column,
|
||||
docs.length === 2 && classes['cols-half'],
|
||||
docs.length >= 3 && classes['cols-thirds'],
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
<Card relationTo={relationTo} doc={doc} showCategories />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Gutter>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -14,7 +14,7 @@ export const AddToCartButton: React.FC<{
|
||||
quantity?: number
|
||||
className?: string
|
||||
appearance?: Props['appearance']
|
||||
}> = (props) => {
|
||||
}> = props => {
|
||||
const { product, quantity = 1, className, appearance = 'primary' } = props
|
||||
|
||||
const { cart, addItemToCart, isProductInCart, hasInitializedCart } = useCart()
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
transition: opacity 150ms linear;
|
||||
}
|
||||
|
||||
:global([data-theme='dark']) {
|
||||
:global([data-theme="dark"]) {
|
||||
.adminBar {
|
||||
background-color: var(--theme-elevation-0);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ const Title: React.FC = () => <span>Dashboard</span>
|
||||
|
||||
export const AdminBar: React.FC<{
|
||||
adminBarProps?: PayloadAdminBarProps
|
||||
}> = (props) => {
|
||||
}> = props => {
|
||||
const { adminBarProps } = props || {}
|
||||
const segments = useSelectedLayoutSegments()
|
||||
const collection = segments?.[1] === 'products' ? 'products' : 'pages'
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
color: var(--theme-bg);
|
||||
}
|
||||
|
||||
:global([data-theme='dark']) {
|
||||
:global([data-theme="dark"]) {
|
||||
.invert {
|
||||
background-color: var(--theme-elevation-100);
|
||||
color: var(--theme-text);
|
||||
|
||||
@@ -9,7 +9,7 @@ type Props = {
|
||||
id?: string
|
||||
}
|
||||
|
||||
export const BackgroundColor: React.FC<Props> = (props) => {
|
||||
export const BackgroundColor: React.FC<Props> = props => {
|
||||
const { id, className, children, invert } = props
|
||||
|
||||
return (
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ArchiveBlock } from '../../_blocks/ArchiveBlock'
|
||||
import { CallToActionBlock } from '../../_blocks/CallToAction'
|
||||
import { ContentBlock } from '../../_blocks/Content'
|
||||
import { MediaBlock } from '../../_blocks/MediaBlock'
|
||||
import { RelatedProducts, type RelatedProductsProps } from '../../_blocks/RelatedProducts'
|
||||
import { toKebabCase } from '../../_utilities/toKebabCase'
|
||||
import { BackgroundColor } from '../BackgroundColor/index'
|
||||
import { VerticalPadding, VerticalPaddingOptions } from '../VerticalPadding/index'
|
||||
@@ -14,12 +15,13 @@ const blockComponents = {
|
||||
content: ContentBlock,
|
||||
mediaBlock: MediaBlock,
|
||||
archive: ArchiveBlock,
|
||||
relatedProducts: RelatedProducts,
|
||||
}
|
||||
|
||||
export const Blocks: React.FC<{
|
||||
blocks: Page['layout']
|
||||
blocks: (Page['layout'][0] | RelatedProductsProps)[]
|
||||
disableTopPadding?: boolean
|
||||
}> = (props) => {
|
||||
}> = props => {
|
||||
const { disableTopPadding, blocks } = props
|
||||
|
||||
const hasBlocks = blocks && Array.isArray(blocks) && blocks.length > 0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@import '../../_css/common';
|
||||
|
||||
.card {
|
||||
.card {
|
||||
border: 1px var(--theme-elevation-200) solid;
|
||||
border-radius: 4px;
|
||||
height: 100%;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import React, { Fragment, useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
@@ -39,7 +41,7 @@ export const Card: React.FC<{
|
||||
title?: string
|
||||
relationTo?: 'products'
|
||||
doc?: Product
|
||||
}> = (props) => {
|
||||
}> = props => {
|
||||
const {
|
||||
showCategories,
|
||||
title: titleFromProps,
|
||||
|
||||
@@ -9,7 +9,7 @@ import classes from './index.module.scss'
|
||||
|
||||
export const CartLink: React.FC<{
|
||||
className?: string
|
||||
}> = (props) => {
|
||||
}> = props => {
|
||||
const { className } = props
|
||||
const { cart } = useCart()
|
||||
const [length, setLength] = useState<number>()
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
left: 0;
|
||||
top: calc(var(--base) * -5);
|
||||
@include mid-break {
|
||||
top: calc(var(--base) * -2);
|
||||
top: calc(var(--base) * -2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ export type Props = {
|
||||
categories?: ArchiveBlockProps['categories']
|
||||
}
|
||||
|
||||
export const CollectionArchive: React.FC<Props> = (props) => {
|
||||
export const CollectionArchive: React.FC<Props> = props => {
|
||||
const {
|
||||
className,
|
||||
relationTo,
|
||||
@@ -51,7 +51,7 @@ export const CollectionArchive: React.FC<Props> = (props) => {
|
||||
|
||||
const [results, setResults] = useState<Result>({
|
||||
totalDocs: typeof populatedDocsTotal === 'number' ? populatedDocsTotal : 0,
|
||||
docs: populatedDocs?.map((doc) => doc.value) || [],
|
||||
docs: populatedDocs?.map(doc => doc.value) || [],
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
hasPrevPage: false,
|
||||
@@ -101,7 +101,7 @@ export const CollectionArchive: React.FC<Props> = (props) => {
|
||||
in:
|
||||
typeof catsFromProps === 'string'
|
||||
? [catsFromProps]
|
||||
: catsFromProps.map((cat) => cat.id).join(','),
|
||||
: catsFromProps.map(cat => cat.id).join(','),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
:global([data-theme='dark']) {
|
||||
:global([data-theme="dark"]) {
|
||||
.footer {
|
||||
background-color: var(--theme-elevation-50);
|
||||
color: var(--theme-elevation-1000);
|
||||
|
||||
@@ -41,10 +41,16 @@ export async function Footer() {
|
||||
return <CMSLink key={i} {...link} />
|
||||
})}
|
||||
<Link href="/admin">Admin</Link>
|
||||
<Link href="https://github.com/payloadcms/payload/tree/master/templates/ecommerce">
|
||||
<Link
|
||||
href="https://github.com/payloadcms/payload/tree/master/templates/ecommerce"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Source Code
|
||||
</Link>
|
||||
<Link href="https://github.com/payloadcms/payload">Payload</Link>
|
||||
<Link href="https://payloadcms.com" target="_blank" rel="noopener noreferrer">
|
||||
Payload
|
||||
</Link>
|
||||
</nav>
|
||||
</Gutter>
|
||||
</footer>
|
||||
|
||||
@@ -4,7 +4,7 @@ import classes from './index.module.scss'
|
||||
|
||||
export const HR: React.FC<{
|
||||
className?: string
|
||||
}> = (props) => {
|
||||
}> = props => {
|
||||
const { className } = props
|
||||
|
||||
return <hr className={[className, classes.hr].filter(Boolean).join(' ')} />
|
||||
|
||||
@@ -25,6 +25,10 @@ export const HeaderNav: React.FC<{ header: HeaderType }> = ({ header }) => {
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
{navItems.map(({ link }, i) => {
|
||||
return <CMSLink key={i} {...link} appearance="none" />
|
||||
})}
|
||||
<CartLink />
|
||||
{user && <Link href="/account">Account</Link>}
|
||||
{!user && (
|
||||
<React.Fragment>
|
||||
@@ -32,10 +36,6 @@ export const HeaderNav: React.FC<{ header: HeaderType }> = ({ header }) => {
|
||||
<Link href="/create-account">Create Account</Link>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{navItems.map(({ link }, i) => {
|
||||
return <CMSLink key={i} {...link} appearance="none" />
|
||||
})}
|
||||
<CartLink />
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
:global([data-theme='light']) {
|
||||
:global([data-theme="light"]) {
|
||||
.logo {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ const heroes = {
|
||||
lowImpact: LowImpactHero,
|
||||
}
|
||||
|
||||
export const Hero: React.FC<Page['hero']> = (props) => {
|
||||
export const Hero: React.FC<Page['hero']> = props => {
|
||||
const { type } = props || {}
|
||||
|
||||
if (!type || type === 'none') return null
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
height: calc(var(--base) * 2);
|
||||
line-height: calc(var(--base) * 2);
|
||||
padding: 0 calc(var(--base) / 2);
|
||||
font-size: inherit;
|
||||
|
||||
&:focus {
|
||||
border: none;
|
||||
@@ -31,7 +32,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
:global([data-theme='dark']) {
|
||||
.asterisk {
|
||||
color: var(--color-error-500);
|
||||
}
|
||||
|
||||
:global([data-theme="dark"]) {
|
||||
.input {
|
||||
background-color: var(--theme-elevation-150);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ type Props = {
|
||||
error: any
|
||||
type?: 'text' | 'number' | 'password' | 'email'
|
||||
validate?: (value: string) => boolean | string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export const Input: React.FC<Props> = ({
|
||||
@@ -21,11 +22,13 @@ export const Input: React.FC<Props> = ({
|
||||
error,
|
||||
type = 'text',
|
||||
validate,
|
||||
disabled,
|
||||
}) => {
|
||||
return (
|
||||
<div className={classes.inputWrap}>
|
||||
<label htmlFor="name" className={classes.label}>
|
||||
{`${label} ${required ? '*' : ''}`}
|
||||
{label}
|
||||
{required ? <span className={classes.asterisk}> *</span> : ''}
|
||||
</label>
|
||||
<input
|
||||
className={[classes.input, error && classes.error].filter(Boolean).join(' ')}
|
||||
@@ -42,6 +45,7 @@ export const Input: React.FC<Props> = ({
|
||||
}
|
||||
: {}),
|
||||
})}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{error && (
|
||||
<div className={classes.errorMessage}>
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
@import '../../_css/common';
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.75;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: .75;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
& > *:not(:last-child) {
|
||||
margin-bottom: var(--base);
|
||||
}
|
||||
& > *:not(:last-child) {
|
||||
margin-bottom: var(--base);
|
||||
}
|
||||
}
|
||||
|
||||
.shimmer {
|
||||
width: 100%;
|
||||
height: calc(var(--base) * 2.5); // same as input height `formInput`
|
||||
background-color: var(--theme-elevation-200);
|
||||
border-radius: 1px;
|
||||
opacity: 1;
|
||||
will-change: opacity;
|
||||
animation: shimmer 1s infinite;
|
||||
width: 100%;
|
||||
height: calc(var(--base) * 2.5); // same as input height `formInput`
|
||||
background-color: var(--theme-elevation-200);
|
||||
border-radius: 1px;
|
||||
opacity: 1;
|
||||
will-change: opacity;
|
||||
animation: shimmer 1s infinite;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import classes from './index.module.scss'
|
||||
export const LoadingShimmer: React.FC<{
|
||||
number?: number
|
||||
height?: number // in `base` units
|
||||
}> = (props) => {
|
||||
}> = props => {
|
||||
const arrayFromNumber = Array.from(Array(props.number || 1).keys())
|
||||
|
||||
return (
|
||||
|
||||
@@ -10,7 +10,7 @@ import classes from './index.module.scss'
|
||||
|
||||
const { breakpoints } = cssVariables
|
||||
|
||||
export const Image: React.FC<MediaProps> = (props) => {
|
||||
export const Image: React.FC<MediaProps> = props => {
|
||||
const {
|
||||
imgClassName,
|
||||
onClick,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Props as MediaProps } from '../types'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export const Video: React.FC<MediaProps> = (props) => {
|
||||
export const Video: React.FC<MediaProps> = props => {
|
||||
const { videoClassName, resource, onClick } = props
|
||||
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Image } from './Image'
|
||||
import { Props } from './types'
|
||||
import { Video } from './Video'
|
||||
|
||||
export const Media: React.FC<Props> = (props) => {
|
||||
export const Media: React.FC<Props> = props => {
|
||||
const { className, resource, htmlElement = 'div' } = props
|
||||
|
||||
const isVideo = typeof resource !== 'string' && resource?.mimeType?.includes('video')
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
color: var(--theme-success-900);
|
||||
}
|
||||
|
||||
:global([data-theme='dark']) {
|
||||
:global([data-theme="dark"]) {
|
||||
.default {
|
||||
background-color: var(--theme-elevation-900);
|
||||
color: var(--theme-elevation-100);
|
||||
|
||||
@@ -24,7 +24,7 @@ export const PageRange: React.FC<{
|
||||
singular?: string
|
||||
plural?: string
|
||||
}
|
||||
}> = (props) => {
|
||||
}> = props => {
|
||||
const {
|
||||
className,
|
||||
totalDocs,
|
||||
|
||||
@@ -9,7 +9,7 @@ export const Pagination: React.FC<{
|
||||
totalPages: number
|
||||
onClick: (page: number) => void
|
||||
className?: string
|
||||
}> = (props) => {
|
||||
}> = props => {
|
||||
const { page, totalPages, onClick, className } = props
|
||||
const hasNextPage = page < totalPages
|
||||
const hasPrevPage = page > 1
|
||||
|
||||
@@ -15,17 +15,21 @@ import { VerticalPadding } from '../VerticalPadding'
|
||||
export const PaywallBlocks: React.FC<{
|
||||
productSlug: string
|
||||
disableTopPadding?: boolean
|
||||
}> = (props) => {
|
||||
}> = props => {
|
||||
const { productSlug, disableTopPadding } = props
|
||||
const { user } = useAuth()
|
||||
|
||||
const [isLoading, setIsLoading] = React.useState(false)
|
||||
const [blocks, setBlocks] = React.useState<Page['layout']>()
|
||||
const hasInitialized = React.useRef(false)
|
||||
const isRequesting = React.useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!user || hasInitialized.current) return
|
||||
if (!user || hasInitialized.current || isRequesting.current) return
|
||||
hasInitialized.current = true
|
||||
isRequesting.current = true
|
||||
|
||||
const start = Date.now()
|
||||
|
||||
const getPaywallContent = async () => {
|
||||
setIsLoading(true)
|
||||
@@ -44,13 +48,20 @@ export const PaywallBlocks: React.FC<{
|
||||
},
|
||||
}),
|
||||
})
|
||||
?.then((res) => res.json())
|
||||
?.then((res) => res?.data?.Products.docs[0]?.paywall)
|
||||
?.then(res => res.json())
|
||||
?.then(res => res?.data?.Products.docs[0]?.paywall)
|
||||
|
||||
if (paywall) {
|
||||
setBlocks(paywall)
|
||||
}
|
||||
|
||||
// wait before setting `isLoading` to `false` to give the illusion of loading
|
||||
// this is to prevent a flash of the loading shimmer on fast networks
|
||||
const end = Date.now()
|
||||
if (end - start < 1000) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500 - (end - start)))
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
} catch (error) {
|
||||
console.error(error) // eslint-disable-line no-console
|
||||
|
||||
@@ -43,7 +43,7 @@ export const Price: React.FC<{
|
||||
product: Product
|
||||
quantity?: number
|
||||
button?: 'addToCart' | 'removeFromCart' | false
|
||||
}> = (props) => {
|
||||
}> = props => {
|
||||
const { product, product: { priceJSON } = {}, button = 'addToCart', quantity } = props
|
||||
|
||||
const [price, setPrice] = useState<{
|
||||
|
||||
@@ -8,7 +8,7 @@ import classes from './index.module.scss'
|
||||
export const RemoveFromCartButton: React.FC<{
|
||||
className?: string
|
||||
product: Product
|
||||
}> = (props) => {
|
||||
}> = props => {
|
||||
const { className, product } = props
|
||||
|
||||
const { deleteItemFromCart, isProductInCart } = useCart()
|
||||
|
||||
@@ -5,21 +5,22 @@ import { useSearchParams } from 'next/navigation'
|
||||
|
||||
import { Message } from '../Message'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export type Props = {
|
||||
params?: string[]
|
||||
message?: string
|
||||
className?: string
|
||||
onParams?: (paramValues: (string | string[])[]) => void
|
||||
onParams?: (paramValues: ((string | null | undefined) | string[])[]) => void
|
||||
}
|
||||
|
||||
export const RenderParamsComponent: React.FC<Props> = ({
|
||||
params = ['error', 'message', 'success'],
|
||||
message,
|
||||
params = ['error', 'warning', 'success', 'message'],
|
||||
className,
|
||||
onParams,
|
||||
}) => {
|
||||
const searchParams = useSearchParams()
|
||||
const paramValues = params.map((param) => searchParams.get(param)).filter(Boolean)
|
||||
const paramValues = params.map(param => searchParams?.get(param))
|
||||
|
||||
useEffect(() => {
|
||||
if (paramValues.length && onParams) {
|
||||
@@ -30,9 +31,19 @@ export const RenderParamsComponent: React.FC<Props> = ({
|
||||
if (paramValues.length) {
|
||||
return (
|
||||
<div className={className}>
|
||||
{paramValues.map((paramValue) => (
|
||||
<Message key={paramValue} message={(message || 'PARAM')?.replace('PARAM', paramValue)} />
|
||||
))}
|
||||
{paramValues.map((paramValue, index) => {
|
||||
if (!paramValue) return null
|
||||
|
||||
return (
|
||||
<Message
|
||||
className={classes.renderParams}
|
||||
key={paramValue}
|
||||
{...{
|
||||
[params[index]]: paramValue,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
@import '../../_css/common.scss';
|
||||
|
||||
.renderParams {
|
||||
margin-bottom: calc(var(--base) * 2);
|
||||
|
||||
@include mid-break {
|
||||
margin-bottom: var(--base);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { Props, RenderParamsComponent } from './Component'
|
||||
// To fix this, we wrap the component in a `Suspense` component
|
||||
// See https://nextjs.org/docs/messages/deopted-into-client-rendering for more info
|
||||
|
||||
export const RenderParams: React.FC<Props> = (props) => {
|
||||
export const RenderParams: React.FC<Props> = props => {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<RenderParamsComponent {...props} />
|
||||
|
||||
@@ -12,4 +12,4 @@
|
||||
|
||||
.bottom-medium {
|
||||
padding-bottom: calc(var(--block-padding) / 2);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
@use './queries.scss' as *;
|
||||
@use './colors.scss' as *;
|
||||
@use './type.scss' as *;
|
||||
@import './theme.scss';
|
||||
@import "./theme.scss";
|
||||
|
||||
:root {
|
||||
--base: 24px;
|
||||
@@ -32,8 +32,8 @@ html {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
opacity: 0;
|
||||
|
||||
&[data-theme='dark'],
|
||||
&[data-theme='light'] {
|
||||
&[data-theme=dark],
|
||||
&[data-theme=light] {
|
||||
opacity: initial;
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,7 @@ p {
|
||||
margin: var(--base) 0;
|
||||
|
||||
@include mid-break {
|
||||
margin: calc(var(--base) * 0.75) 0;
|
||||
margin: calc(var(--base) * .75) 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,12 +108,12 @@ a {
|
||||
color: currentColor;
|
||||
|
||||
&:focus {
|
||||
opacity: 0.8;
|
||||
opacity: .8;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
opacity: .7;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,3 +83,5 @@
|
||||
--color-error-900: rgb(51, 22, 24);
|
||||
--color-error-950: rgb(25, 11, 12);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
@forward './queries.scss';
|
||||
@forward './type.scss';
|
||||
@forward './type.scss';
|
||||
@@ -1,4 +1,4 @@
|
||||
[data-theme='light'] {
|
||||
[data-theme=light] {
|
||||
--theme-success-50: var(--color-success-50);
|
||||
--theme-success-100: var(--color-success-100);
|
||||
--theme-success-150: var(--color-success-150);
|
||||
@@ -117,7 +117,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme='dark'] {
|
||||
[data-theme=dark] {
|
||||
--theme-elevation-0: var(--color-base-1000);
|
||||
--theme-elevation-50: var(--color-base-950);
|
||||
--theme-elevation-100: var(--color-base-900);
|
||||
|
||||
@@ -27,8 +27,14 @@ export const PRODUCT = `
|
||||
${ARCHIVE_BLOCK}
|
||||
}
|
||||
priceJSON
|
||||
${META}
|
||||
enablePaywall
|
||||
relatedProducts {
|
||||
id
|
||||
slug
|
||||
title
|
||||
${META}
|
||||
}
|
||||
${META}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
display: flex;
|
||||
padding-top: var(--base);
|
||||
flex-wrap: wrap;
|
||||
margin: calc(var(--base) * -0.5);
|
||||
margin: calc(var(--base) * -.5);
|
||||
|
||||
& > * {
|
||||
margin: calc(var(--base) / 2);
|
||||
|
||||
@@ -8,7 +8,7 @@ import RichText from '../../_components/RichText'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export const MediumImpactHero: React.FC<Page['hero']> = (props) => {
|
||||
export const MediumImpactHero: React.FC<Page['hero']> = props => {
|
||||
const { richText, media, links } = props
|
||||
|
||||
return (
|
||||
|
||||
@@ -38,7 +38,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
// used to track the single event of logging in or logging out
|
||||
// useful for `useEffect` hooks that should only run once
|
||||
const [status, setStatus] = useState<undefined | 'loggedOut' | 'loggedIn'>()
|
||||
const create = useCallback<Create>(async (args) => {
|
||||
const create = useCallback<Create>(async args => {
|
||||
try {
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/users/create`, {
|
||||
method: 'POST',
|
||||
@@ -66,7 +66,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
}
|
||||
}, [])
|
||||
|
||||
const login = useCallback<Login>(async (args) => {
|
||||
const login = useCallback<Login>(async args => {
|
||||
try {
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/users/login`, {
|
||||
method: 'POST',
|
||||
@@ -142,7 +142,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
fetchMe()
|
||||
}, [])
|
||||
|
||||
const forgotPassword = useCallback<ForgotPassword>(async (args) => {
|
||||
const forgotPassword = useCallback<ForgotPassword>(async args => {
|
||||
try {
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/users/forgot-password`, {
|
||||
method: 'POST',
|
||||
@@ -167,7 +167,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
}
|
||||
}, [])
|
||||
|
||||
const resetPassword = useCallback<ResetPassword>(async (args) => {
|
||||
const resetPassword = useCallback<ResetPassword>(async args => {
|
||||
try {
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/users/reset-password`, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -32,7 +32,7 @@ const Context = createContext({} as CartContext)
|
||||
|
||||
export const useCart = () => useContext(Context)
|
||||
|
||||
const arrayHasItems = (array) => Array.isArray(array) && array.length > 0
|
||||
const arrayHasItems = array => Array.isArray(array) && array.length > 0
|
||||
|
||||
// Step 1: Check local storage for a cart
|
||||
// Step 2: If there is a cart, fetch the products and hydrate the cart
|
||||
@@ -41,7 +41,7 @@ const arrayHasItems = (array) => Array.isArray(array) && array.length > 0
|
||||
// Step 4B: Sync the cart to Payload and clear local storage
|
||||
// Step 5: If the user is logged out, sync the cart to local storage only
|
||||
|
||||
export const CartProvider = (props) => {
|
||||
export const CartProvider = props => {
|
||||
// const { setTimedNotification } = useNotifications();
|
||||
const { children } = props
|
||||
const { user, status: authStatus } = useAuth()
|
||||
@@ -138,7 +138,7 @@ export const CartProvider = (props) => {
|
||||
const flattenedCart = {
|
||||
...cart,
|
||||
items: cart?.items
|
||||
?.map((item) => {
|
||||
?.map(item => {
|
||||
if (!item?.product || typeof item?.product !== 'object') {
|
||||
return null
|
||||
}
|
||||
@@ -203,7 +203,7 @@ export const CartProvider = (props) => {
|
||||
)
|
||||
|
||||
// this method can be used to add new items AND update existing ones
|
||||
const addItemToCart = useCallback((incomingItem) => {
|
||||
const addItemToCart = useCallback(incomingItem => {
|
||||
dispatchCart({
|
||||
type: 'ADD_ITEM',
|
||||
payload: incomingItem,
|
||||
|
||||
@@ -36,7 +36,7 @@ export const ThemeSelector: React.FC = () => {
|
||||
<label htmlFor="theme">
|
||||
<select
|
||||
id="theme"
|
||||
onChange={(e) => onThemeChange(e.target.value as Theme & 'auto')}
|
||||
onChange={e => onThemeChange(e.target.value as Theme & 'auto')}
|
||||
ref={selectRef}
|
||||
className={classes.select}
|
||||
>
|
||||
|
||||
@@ -7,8 +7,15 @@ export async function GET(request: NextRequest): Promise<unknown> {
|
||||
const slug = request.nextUrl.searchParams.get('slug')
|
||||
const secret = request.nextUrl.searchParams.get('secret')
|
||||
|
||||
if (secret !== process.env.NEXT_PRIVATE_REVALIDATION_KEY) {
|
||||
return NextResponse.json({ revalidated: false, now: Date.now() })
|
||||
if (
|
||||
!secret ||
|
||||
secret !== process.env.NEXT_PRIVATE_REVALIDATION_KEY ||
|
||||
typeof collection !== 'string' ||
|
||||
typeof slug !== 'string'
|
||||
) {
|
||||
// Do not indicate that the revalidation key is incorrect in the response
|
||||
// This will protect this API route from being exploited
|
||||
return new Response('Invalid request', { status: 400 })
|
||||
}
|
||||
|
||||
if (typeof collection === 'string' && typeof slug === 'string') {
|
||||
|
||||
@@ -15,6 +15,8 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<InitTheme />
|
||||
<link rel="icon" href="/favicon.ico" sizes="32x32" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
</head>
|
||||
<body>
|
||||
<Providers>
|
||||
|
||||
@@ -19,7 +19,7 @@ export const updateUserPurchases: AfterChangeHook<Order> = async ({ doc, req, op
|
||||
id: orderedBy,
|
||||
data: {
|
||||
purchases: [
|
||||
...(user?.purchases?.map((purchase) =>
|
||||
...(user?.purchases?.map(purchase =>
|
||||
typeof purchase === 'string' ? purchase : purchase.id,
|
||||
) || []), // eslint-disable-line function-paren-newline
|
||||
...(doc?.items?.map(({ product }) =>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user