diff --git a/apps/cms-payload/.env.example b/apps/cms-payload/.env.example deleted file mode 100644 index f92052c377e..00000000000 --- a/apps/cms-payload/.env.example +++ /dev/null @@ -1,12 +0,0 @@ -DATABASE_URI=postgres://postgres:@127.0.0.1:5432/your-database-name -PAYLOAD_SECRET=YOUR_SECRET_HERE - -S3_BUCKET= -S3_ACCESS_KEY_ID= -S3_SECRET_ACCESS_KEY= -S3_REGION= -S3_ENDPOINT= - -NEXT_PUBLIC_SERVER_URL=http://localhost:3000 -CRON_SECRET=YOUR_CRON_SECRET_HERE -PREVIEW_SECRET=YOUR_SECRET_HERE diff --git a/apps/cms-payload/.gitignore b/apps/cms-payload/.gitignore deleted file mode 100644 index 3d53a40bcb8..00000000000 --- a/apps/cms-payload/.gitignore +++ /dev/null @@ -1,43 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz - -/.idea/* -!/.idea/runConfigurations - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts - -.env - -/media diff --git a/apps/cms-payload/README.md b/apps/cms-payload/README.md deleted file mode 100644 index 4a2dde69c19..00000000000 --- a/apps/cms-payload/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# Payload Blank Template - -This template comes configured with the bare minimum to get started on anything you need. - -## Quick start - -This template can be deployed directly from our Cloud hosting and it will setup MongoDB and cloud S3 object storage for media. - -## Quick Start - local setup - -To spin up this template locally, follow these steps: - -### Clone - -After you click the `Deploy` button above, you'll want to have standalone copy of this repo on your machine. If you've already cloned this repo, skip to [Development](#development). - -### Development - -1. First [clone the repo](#clone) if you have not done so already -2. `cd my-project && cp .env.example .env` to copy the example environment variables. You'll need to add the `MONGODB_URI` from your Cloud project to your `.env` if you want to use S3 storage and the MongoDB database that was created for you. - -3. `pnpm install && pnpm dev` to install dependencies and start the dev server -4. open `http://localhost:3000` to open the app in your browser - -That's it! Changes made in `./src` will be reflected in your app. Follow the on-screen instructions to login and create your first admin user. Then check out [Production](#production) once you're ready to build and serve your app, and [Deployment](#deployment) when you're ready to go live. - -#### Docker (Optional) - -If you prefer to use Docker for local development instead of a local MongoDB instance, the provided docker-compose.yml file can be used. - -To do so, follow these steps: - -- Modify the `MONGODB_URI` in your `.env` file to `mongodb://127.0.0.1/` -- Modify the `docker-compose.yml` file's `MONGODB_URI` to match the above `` -- Run `docker-compose up` to start the database, optionally pass `-d` to run in the background. - -## How it works - -The Payload config is tailored specifically to the needs of most websites. It is pre-configured in the following ways: - -### Collections - -See the [Collections](https://payloadcms.com/docs/configuration/collections) docs for details on how to extend this functionality. - -- #### Users (Authentication) - - Users are auth-enabled collections that have access to the admin panel. - - For additional help, see the official [Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth) or the [Authentication](https://payloadcms.com/docs/authentication/overview#authentication-overview) docs. - -- #### Media - - This is the uploads enabled collection. It features pre-configured sizes, focal point and manual resizing to help you manage your pictures. - -### Docker - -Alternatively, you can use [Docker](https://www.docker.com) to spin up this template locally. To do so, follow these steps: - -1. Follow [steps 1 and 2 from above](#development), the docker-compose file will automatically use the `.env` file in your project root -1. Next run `docker-compose up` -1. Follow [steps 4 and 5 from above](#development) to login and create your first admin user - -That's it! The Docker instance will help you get up and running quickly while also standardizing the development environment across your teams. - -## Questions - -If you have any issues or questions, reach out to us on [Discord](https://discord.com/invite/payload) or start a [GitHub discussion](https://github.com/payloadcms/payload/discussions). diff --git a/apps/cms-payload/package.json b/apps/cms-payload/package.json deleted file mode 100644 index 8ab501b84b3..00000000000 --- a/apps/cms-payload/package.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "name": "cms-payload", - "version": "1.0.0", - "description": "A blank template to get started with Payload 3.0", - "license": "MIT", - "type": "module", - "scripts": { - "build": "cross-env NODE_OPTIONS=--no-deprecation next build --turbopack", - "dev": "cross-env NODE_OPTIONS=--no-deprecation next dev --turbopack --port 3030", - "devsafe": "rm -rf .next && cross-env NODE_OPTIONS=--no-deprecation next dev", - "generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap", - "generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types", - "lint": "cross-env NODE_OPTIONS=--no-deprecation next lint", - "payload": "cross-env NODE_OPTIONS=--no-deprecation payload", - "start": "cross-env NODE_OPTIONS=--no-deprecation next start", - "ci": "payload migrate && pnpm build" - }, - "dependencies": { - "@payloadcms/db-postgres": "3.33.0", - "@payloadcms/next": "3.33.0", - "@payloadcms/payload-cloud": "3.33.0", - "@payloadcms/richtext-lexical": "3.33.0", - "@payloadcms/storage-s3": "^3.33.0", - "cross-env": "^7.0.3", - "graphql": "^16.8.1", - "next": "15.3.0", - "payload": "3.33.0", - "react": "19.1.0", - "react-dom": "19.1.0", - "sharp": "0.32.6" - }, - "devDependencies": { - "@eslint/eslintrc": "^3.2.0", - "@types/node": "^22.5.4", - "@types/react": "19.1.0", - "@types/react-dom": "19.1.2", - "eslint": "^9.16.0", - "eslint-config-next": "15.3.0", - "prettier": "^3.4.2", - "typescript": "5.7.3" - }, - "engines": { - "node": "^18.20.2 || >=20.9.0", - "pnpm": "^9 || ^10" - }, - "pnpm": { - "onlyBuiltDependencies": [ - "sharp" - ] - } -} diff --git a/apps/cms-payload/src/collections/BlogPost.ts b/apps/cms-payload/src/collections/BlogPost.ts deleted file mode 100644 index c7afdc5125d..00000000000 --- a/apps/cms-payload/src/collections/BlogPost.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { CollectionConfig } from 'payload' - -const BlogPost: CollectionConfig = { - slug: 'blog-posts', - admin: { - useAsTitle: 'Title', - }, - access: { - read: () => true, - }, - fields: [ - { - name: 'Title', - type: 'text', - required: true, - }, - { - name: 'slug', - type: 'text', - required: true, - unique: true, - admin: { - position: 'sidebar', - }, - validate: (value: string | string[] | null | undefined) => { - const regex = /^[a-z][a-z0-9-]{2,49}$/ - if (!regex.test(value as string)) { - return 'Slug must start with a letter, contain only lowercase letters, numbers, and hyphens, and be between 3-50 characters' - } - return true - }, - }, - { - name: 'thumb', - type: 'upload', - relationTo: 'media', - required: false, - }, - { - name: 'date', - type: 'date', - admin: { - position: 'sidebar', - }, - }, - { - name: 'image', - type: 'upload', - relationTo: 'media', - required: false, - }, - { - name: 'toc_depth', - type: 'number', - defaultValue: 2, - admin: { - position: 'sidebar', - }, - }, - { - name: 'content', - type: 'richText', - required: true, - }, - { - name: 'authors', - type: 'relationship', - relationTo: 'authors', - hasMany: true, - admin: { - position: 'sidebar', - }, - }, - { - name: 'tags', - type: 'relationship', - relationTo: 'tags', - hasMany: true, - admin: { - position: 'sidebar', - }, - }, - { - name: 'categories', - type: 'relationship', - relationTo: 'categories', - hasMany: true, - admin: { - position: 'sidebar', - }, - }, - { - name: 'description', - type: 'textarea', - }, - { - name: 'launchweek', - type: 'text', - admin: { - position: 'sidebar', - }, - }, - { - name: 'readingTime', - type: 'number', - admin: { - hidden: true, - }, - }, - ], - timestamps: true, - // hooks: { - // beforeValidate: [generateReadingTime], - // }, -} - -export default BlogPost diff --git a/apps/cms-payload/src/collections/Media.ts b/apps/cms-payload/src/collections/Media.ts deleted file mode 100644 index 568cf42657f..00000000000 --- a/apps/cms-payload/src/collections/Media.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { CollectionConfig } from 'payload' - -export const Media: CollectionConfig = { - slug: 'media', - access: { - read: () => true, - }, - fields: [ - { - name: 'alt', - type: 'text', - required: true, - }, - ], - upload: true, -} diff --git a/apps/cms-payload/src/payload-types.ts b/apps/cms-payload/src/payload-types.ts deleted file mode 100644 index 5ff2d8642c9..00000000000 --- a/apps/cms-payload/src/payload-types.ts +++ /dev/null @@ -1,435 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * This file was automatically generated by Payload. - * DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config, - * and re-run `payload generate:types` to regenerate this file. - */ - -/** - * Supported timezones in IANA format. - * - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "supportedTimezones". - */ -export type SupportedTimezones = - | 'Pacific/Midway' - | 'Pacific/Niue' - | 'Pacific/Honolulu' - | 'Pacific/Rarotonga' - | 'America/Anchorage' - | 'Pacific/Gambier' - | 'America/Los_Angeles' - | 'America/Tijuana' - | 'America/Denver' - | 'America/Phoenix' - | 'America/Chicago' - | 'America/Guatemala' - | 'America/New_York' - | 'America/Bogota' - | 'America/Caracas' - | 'America/Santiago' - | 'America/Buenos_Aires' - | 'America/Sao_Paulo' - | 'Atlantic/South_Georgia' - | 'Atlantic/Azores' - | 'Atlantic/Cape_Verde' - | 'Europe/London' - | 'Europe/Berlin' - | 'Africa/Lagos' - | 'Europe/Athens' - | 'Africa/Cairo' - | 'Europe/Moscow' - | 'Asia/Riyadh' - | 'Asia/Dubai' - | 'Asia/Baku' - | 'Asia/Karachi' - | 'Asia/Tashkent' - | 'Asia/Calcutta' - | 'Asia/Dhaka' - | 'Asia/Almaty' - | 'Asia/Jakarta' - | 'Asia/Bangkok' - | 'Asia/Shanghai' - | 'Asia/Singapore' - | 'Asia/Tokyo' - | 'Asia/Seoul' - | 'Australia/Brisbane' - | 'Australia/Sydney' - | 'Pacific/Guam' - | 'Pacific/Noumea' - | 'Pacific/Auckland' - | 'Pacific/Fiji'; - -export interface Config { - auth: { - users: UserAuthOperations; - }; - blocks: {}; - collections: { - users: User; - media: Media; - 'blog-posts': BlogPost; - authors: Author; - categories: Category; - tags: Tag; - 'payload-locked-documents': PayloadLockedDocument; - 'payload-preferences': PayloadPreference; - 'payload-migrations': PayloadMigration; - }; - collectionsJoins: {}; - collectionsSelect: { - users: UsersSelect | UsersSelect; - media: MediaSelect | MediaSelect; - 'blog-posts': BlogPostsSelect | BlogPostsSelect; - authors: AuthorsSelect | AuthorsSelect; - categories: CategoriesSelect | CategoriesSelect; - tags: TagsSelect | TagsSelect; - 'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect; - 'payload-preferences': PayloadPreferencesSelect | PayloadPreferencesSelect; - 'payload-migrations': PayloadMigrationsSelect | PayloadMigrationsSelect; - }; - db: { - defaultIDType: number; - }; - globals: {}; - globalsSelect: {}; - locale: null; - user: User & { - collection: 'users'; - }; - jobs: { - tasks: unknown; - workflows: unknown; - }; -} -export interface UserAuthOperations { - forgotPassword: { - email: string; - password: string; - }; - login: { - email: string; - password: string; - }; - registerFirstUser: { - email: string; - password: string; - }; - unlock: { - email: string; - password: string; - }; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "users". - */ -export interface User { - id: number; - updatedAt: string; - createdAt: string; - email: string; - resetPasswordToken?: string | null; - resetPasswordExpiration?: string | null; - salt?: string | null; - hash?: string | null; - loginAttempts?: number | null; - lockUntil?: string | null; - password?: string | null; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "media". - */ -export interface Media { - id: number; - alt: string; - prefix?: string | null; - updatedAt: string; - createdAt: string; - url?: string | null; - thumbnailURL?: string | null; - filename?: string | null; - mimeType?: string | null; - filesize?: number | null; - width?: number | null; - height?: number | null; - focalX?: number | null; - focalY?: number | null; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "blog-posts". - */ -export interface BlogPost { - id: number; - Title: string; - slug: string; - thumb?: (number | null) | Media; - date?: string | null; - image?: (number | null) | Media; - toc_depth?: number | null; - content: { - root: { - type: string; - children: { - type: string; - version: number; - [k: string]: unknown; - }[]; - direction: ('ltr' | 'rtl') | null; - format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | ''; - indent: number; - version: number; - }; - [k: string]: unknown; - }; - authors?: (number | Author)[] | null; - tags?: (number | Tag)[] | null; - categories?: (number | Category)[] | null; - description?: string | null; - launchweek?: string | null; - readingTime?: number | null; - updatedAt: string; - createdAt: string; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "authors". - */ -export interface Author { - id: number; - author: string; - author_id?: string | null; - position?: string | null; - author_url?: string | null; - author_image_url?: (number | null) | Media; - username?: string | null; - updatedAt: string; - createdAt: string; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "tags". - */ -export interface Tag { - id: number; - name: string; - updatedAt: string; - createdAt: string; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "categories". - */ -export interface Category { - id: number; - name: string; - updatedAt: string; - createdAt: string; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "payload-locked-documents". - */ -export interface PayloadLockedDocument { - id: number; - document?: - | ({ - relationTo: 'users'; - value: number | User; - } | null) - | ({ - relationTo: 'media'; - value: number | Media; - } | null) - | ({ - relationTo: 'blog-posts'; - value: number | BlogPost; - } | null) - | ({ - relationTo: 'authors'; - value: number | Author; - } | null) - | ({ - relationTo: 'categories'; - value: number | Category; - } | null) - | ({ - relationTo: 'tags'; - value: number | Tag; - } | null); - globalSlug?: string | null; - user: { - relationTo: 'users'; - value: number | User; - }; - updatedAt: string; - createdAt: string; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "payload-preferences". - */ -export interface PayloadPreference { - id: number; - user: { - relationTo: 'users'; - value: number | User; - }; - key?: string | null; - value?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; - updatedAt: string; - createdAt: string; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "payload-migrations". - */ -export interface PayloadMigration { - id: number; - name?: string | null; - batch?: number | null; - updatedAt: string; - createdAt: string; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "users_select". - */ -export interface UsersSelect { - updatedAt?: T; - createdAt?: T; - email?: T; - resetPasswordToken?: T; - resetPasswordExpiration?: T; - salt?: T; - hash?: T; - loginAttempts?: T; - lockUntil?: T; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "media_select". - */ -export interface MediaSelect { - alt?: T; - prefix?: T; - updatedAt?: T; - createdAt?: T; - url?: T; - thumbnailURL?: T; - filename?: T; - mimeType?: T; - filesize?: T; - width?: T; - height?: T; - focalX?: T; - focalY?: T; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "blog-posts_select". - */ -export interface BlogPostsSelect { - Title?: T; - slug?: T; - thumb?: T; - date?: T; - image?: T; - toc_depth?: T; - content?: T; - authors?: T; - tags?: T; - categories?: T; - description?: T; - launchweek?: T; - readingTime?: T; - updatedAt?: T; - createdAt?: T; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "authors_select". - */ -export interface AuthorsSelect { - author?: T; - author_id?: T; - position?: T; - author_url?: T; - author_image_url?: T; - username?: T; - updatedAt?: T; - createdAt?: T; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "categories_select". - */ -export interface CategoriesSelect { - name?: T; - updatedAt?: T; - createdAt?: T; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "tags_select". - */ -export interface TagsSelect { - name?: T; - updatedAt?: T; - createdAt?: T; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "payload-locked-documents_select". - */ -export interface PayloadLockedDocumentsSelect { - document?: T; - globalSlug?: T; - user?: T; - updatedAt?: T; - createdAt?: T; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "payload-preferences_select". - */ -export interface PayloadPreferencesSelect { - user?: T; - key?: T; - value?: T; - updatedAt?: T; - createdAt?: T; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "payload-migrations_select". - */ -export interface PayloadMigrationsSelect { - name?: T; - batch?: T; - updatedAt?: T; - createdAt?: T; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "auth". - */ -export interface Auth { - [k: string]: unknown; -} - - -declare module 'payload' { - export interface GeneratedTypes extends Config {} -} \ No newline at end of file diff --git a/apps/cms-payload/src/payload.config.ts b/apps/cms-payload/src/payload.config.ts deleted file mode 100644 index 266605d8876..00000000000 --- a/apps/cms-payload/src/payload.config.ts +++ /dev/null @@ -1,61 +0,0 @@ -// storage-adapter-import-placeholder -import { postgresAdapter } from '@payloadcms/db-postgres' -import { payloadCloudPlugin } from '@payloadcms/payload-cloud' -import { lexicalEditor } from '@payloadcms/richtext-lexical' -import path from 'path' -import { buildConfig } from 'payload' -import { fileURLToPath } from 'url' -import { s3Storage } from '@payloadcms/storage-s3' -import sharp from 'sharp' - -import { Users } from './collections/Users' -import { Media } from './collections/Media' -import BlogPost from './collections/BlogPost' -import Author from './collections/Author' -import Category from './collections/Category' -import Tag from './collections/Tag' - -const filename = fileURLToPath(import.meta.url) -const dirname = path.dirname(filename) - -export default buildConfig({ - admin: { - user: Users.slug, - importMap: { - baseDir: path.resolve(dirname), - }, - }, - collections: [Users, Media, BlogPost, Author, Category, Tag], - editor: lexicalEditor(), - secret: process.env.PAYLOAD_SECRET || '', - typescript: { - outputFile: path.resolve(dirname, 'payload-types.ts'), - }, - db: postgresAdapter({ - pool: { - connectionString: process.env.DATABASE_URI || '', - }, - schemaName: 'cms-payload', - }), - sharp, - plugins: [ - payloadCloudPlugin(), - s3Storage({ - collections: { - media: { - prefix: 'media', - }, - }, - bucket: process.env.S3_BUCKET || '', - config: { - forcePathStyle: true, - credentials: { - accessKeyId: process.env.S3_ACCESS_KEY_ID || '', - secretAccessKey: process.env.S3_SECRET_ACCESS_KEY || '', - }, - region: process.env.S3_REGION, - endpoint: process.env.S3_ENDPOINT, - }, - }), - ], -}) diff --git a/apps/cms-payload/supabase/.gitignore b/apps/cms-payload/supabase/.gitignore deleted file mode 100644 index ad9264f0b14..00000000000 --- a/apps/cms-payload/supabase/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Supabase -.branches -.temp - -# dotenvx -.env.keys -.env.local -.env.*.local diff --git a/apps/cms-payload/supabase/config.toml b/apps/cms-payload/supabase/config.toml deleted file mode 100644 index 255373749f1..00000000000 --- a/apps/cms-payload/supabase/config.toml +++ /dev/null @@ -1,308 +0,0 @@ -# For detailed configuration reference documentation, visit: -# https://supabase.com/docs/guides/local-development/cli/config -# A string used to distinguish different Supabase projects on the same host. Defaults to the -# working directory name when running `supabase init`. -project_id = "cms-payload" - -[api] -enabled = true -# Port to use for the API URL. -port = 54321 -# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API -# endpoints. `public` and `graphql_public` schemas are included by default. -schemas = ["public", "graphql_public"] -# Extra schemas to add to the search_path of every request. -extra_search_path = ["public", "extensions"] -# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size -# for accidental or malicious requests. -max_rows = 1000 - -[api.tls] -# Enable HTTPS endpoints locally using a self-signed certificate. -enabled = false - -[db] -# Port to use for the local database URL. -port = 54322 -# Port used by db diff command to initialize the shadow database. -shadow_port = 54320 -# The database major version to use. This has to be the same as your remote database's. Run `SHOW -# server_version;` on the remote database to check. -major_version = 15 - -[db.pooler] -enabled = false -# Port to use for the local connection pooler. -port = 54329 -# Specifies when a server connection can be reused by other clients. -# Configure one of the supported pooler modes: `transaction`, `session`. -pool_mode = "transaction" -# How many server connections to allow per user/database pair. -default_pool_size = 20 -# Maximum number of client connections allowed. -max_client_conn = 100 - -# [db.vault] -# secret_key = "env(SECRET_VALUE)" - -[db.migrations] -# Specifies an ordered list of schema files that describe your database. -# Supports glob patterns relative to supabase directory: "./schemas/*.sql" -schema_paths = [] - -[db.seed] -# If enabled, seeds the database after migrations during a db reset. -enabled = true -# Specifies an ordered list of seed files to load during db reset. -# Supports glob patterns relative to supabase directory: "./seeds/*.sql" -sql_paths = ["./seed.sql"] - -[realtime] -enabled = true -# Bind realtime via either IPv4 or IPv6. (default: IPv4) -# ip_version = "IPv6" -# The maximum length in bytes of HTTP request headers. (default: 4096) -# max_header_length = 4096 - -[studio] -enabled = true -# Port to use for Supabase Studio. -port = 54323 -# External URL of the API server that frontend connects to. -api_url = "http://127.0.0.1" -# OpenAI API Key to use for Supabase AI in the Supabase Studio. -openai_api_key = "env(OPENAI_API_KEY)" - -# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they -# are monitored, and you can view the emails that would have been sent from the web interface. -[inbucket] -enabled = true -# Port to use for the email testing server web interface. -port = 54324 -# Uncomment to expose additional ports for testing user applications that send emails. -# smtp_port = 54325 -# pop3_port = 54326 -# admin_email = "admin@email.com" -# sender_name = "Admin" - -[storage] -enabled = true -# The maximum file size allowed (e.g. "5MB", "500KB"). -file_size_limit = "50MiB" - -# Image transformation API is available to Supabase Pro plan. -# [storage.image_transformation] -# enabled = true - -# Uncomment to configure local storage buckets -# [storage.buckets.images] -# public = false -# file_size_limit = "50MiB" -# allowed_mime_types = ["image/png", "image/jpeg"] -# objects_path = "./images" - -[auth] -enabled = true -# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used -# in emails. -site_url = "http://127.0.0.1:3000" -# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. -additional_redirect_urls = ["https://127.0.0.1:3000"] -# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). -jwt_expiry = 3600 -# If disabled, the refresh token will never expire. -enable_refresh_token_rotation = true -# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. -# Requires enable_refresh_token_rotation = true. -refresh_token_reuse_interval = 10 -# Allow/disallow new user signups to your project. -enable_signup = true -# Allow/disallow anonymous sign-ins to your project. -enable_anonymous_sign_ins = false -# Allow/disallow testing manual linking of accounts -enable_manual_linking = false -# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more. -minimum_password_length = 6 -# Passwords that do not meet the following requirements will be rejected as weak. Supported values -# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols` -password_requirements = "" - -[auth.rate_limit] -# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled. -email_sent = 2 -# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled. -sms_sent = 30 -# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true. -anonymous_users = 30 -# Number of sessions that can be refreshed in a 5 minute interval per IP address. -token_refresh = 150 -# Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users). -sign_in_sign_ups = 30 -# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address. -token_verifications = 30 - -# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`. -# [auth.captcha] -# enabled = true -# provider = "hcaptcha" -# secret = "" - -[auth.email] -# Allow/disallow new user signups via email to your project. -enable_signup = true -# If enabled, a user will be required to confirm any email change on both the old, and new email -# addresses. If disabled, only the new email is required to confirm. -double_confirm_changes = true -# If enabled, users need to confirm their email address before signing in. -enable_confirmations = false -# If enabled, users will need to reauthenticate or have logged in recently to change their password. -secure_password_change = false -# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. -max_frequency = "1s" -# Number of characters used in the email OTP. -otp_length = 6 -# Number of seconds before the email OTP expires (defaults to 1 hour). -otp_expiry = 3600 - -# Use a production-ready SMTP server -# [auth.email.smtp] -# enabled = true -# host = "smtp.sendgrid.net" -# port = 587 -# user = "apikey" -# pass = "env(SENDGRID_API_KEY)" -# admin_email = "admin@email.com" -# sender_name = "Admin" - -# Uncomment to customize email template -# [auth.email.template.invite] -# subject = "You have been invited" -# content_path = "./supabase/templates/invite.html" - -[auth.sms] -# Allow/disallow new user signups via SMS to your project. -enable_signup = false -# If enabled, users need to confirm their phone number before signing in. -enable_confirmations = false -# Template for sending OTP to users -template = "Your code is {{ .Code }}" -# Controls the minimum amount of time that must pass before sending another sms otp. -max_frequency = "5s" - -# Use pre-defined map of phone number to OTP for testing. -# [auth.sms.test_otp] -# 4152127777 = "123456" - -# Configure logged in session timeouts. -# [auth.sessions] -# Force log out after the specified duration. -# timebox = "24h" -# Force log out if the user has been inactive longer than the specified duration. -# inactivity_timeout = "8h" - -# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. -# [auth.hook.custom_access_token] -# enabled = true -# uri = "pg-functions:////" - -# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. -[auth.sms.twilio] -enabled = false -account_sid = "" -message_service_sid = "" -# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: -auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" - -# Multi-factor-authentication is available to Supabase Pro plan. -[auth.mfa] -# Control how many MFA factors can be enrolled at once per user. -max_enrolled_factors = 10 - -# Control MFA via App Authenticator (TOTP) -[auth.mfa.totp] -enroll_enabled = false -verify_enabled = false - -# Configure MFA via Phone Messaging -[auth.mfa.phone] -enroll_enabled = false -verify_enabled = false -otp_length = 6 -template = "Your code is {{ .Code }}" -max_frequency = "5s" - -# Configure MFA via WebAuthn -# [auth.mfa.web_authn] -# enroll_enabled = true -# verify_enabled = true - -# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, -# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, -# `twitter`, `slack`, `spotify`, `workos`, `zoom`. -[auth.external.apple] -enabled = false -client_id = "" -# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: -secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" -# Overrides the default auth redirectUrl. -redirect_uri = "" -# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, -# or any other third-party OIDC providers. -url = "" -# If enabled, the nonce check will be skipped. Required for local sign in with Google auth. -skip_nonce_check = false - -# Use Firebase Auth as a third-party provider alongside Supabase Auth. -[auth.third_party.firebase] -enabled = false -# project_id = "my-firebase-project" - -# Use Auth0 as a third-party provider alongside Supabase Auth. -[auth.third_party.auth0] -enabled = false -# tenant = "my-auth0-tenant" -# tenant_region = "us" - -# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth. -[auth.third_party.aws_cognito] -enabled = false -# user_pool_id = "my-user-pool-id" -# user_pool_region = "us-east-1" - -# Use Clerk as a third-party provider alongside Supabase Auth. -[auth.third_party.clerk] -enabled = false -# Obtain from https://clerk.com/setup/supabase -# domain = "example.clerk.accounts.dev" - -[edge_runtime] -enabled = true -# Configure one of the supported request policies: `oneshot`, `per_worker`. -# Use `oneshot` for hot reload, or `per_worker` for load testing. -policy = "oneshot" -# Port to attach the Chrome inspector for debugging edge functions. -inspector_port = 8083 -# The Deno major version to use. -deno_version = 1 - -# [edge_runtime.secrets] -# secret_key = "env(SECRET_VALUE)" - -[analytics] -enabled = true -port = 54327 -# Configure one of the supported backends: `postgres`, `bigquery`. -backend = "postgres" - -# Experimental features may be deprecated any time -[experimental] -# Configures Postgres storage engine to use OrioleDB (S3) -orioledb_version = "" -# Configures S3 bucket URL, eg. .s3-.amazonaws.com -s3_host = "env(S3_HOST)" -# Configures S3 bucket region, eg. us-east-1 -s3_region = "env(S3_REGION)" -# Configures AWS_ACCESS_KEY_ID for S3 bucket -s3_access_key = "env(S3_ACCESS_KEY)" -# Configures AWS_SECRET_ACCESS_KEY for S3 bucket -s3_secret_key = "env(S3_SECRET_KEY)" diff --git a/apps/cms-payload/tsconfig.json b/apps/cms-payload/tsconfig.json deleted file mode 100644 index 0409a883a97..00000000000 --- a/apps/cms-payload/tsconfig.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "compilerOptions": { - "baseUrl": ".", - "lib": [ - "DOM", - "DOM.Iterable", - "ES2022" - ], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": [ - "./src/*" - ], - "@payload-config": [ - "./src/payload.config.ts" - ] - }, - "target": "ES2022", - }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - ".next/types/**/*.ts" - ], - "exclude": [ - "node_modules" - ] -} diff --git a/apps/cms-payload/.editorconfig b/apps/cms/.editorconfig similarity index 100% rename from apps/cms-payload/.editorconfig rename to apps/cms/.editorconfig diff --git a/apps/cms/.env.example b/apps/cms/.env.example index d9f1b75b0bc..f92052c377e 100644 --- a/apps/cms/.env.example +++ b/apps/cms/.env.example @@ -1,29 +1,12 @@ -# Server -HOST=0.0.0.0 -PORT=1337 +DATABASE_URI=postgres://postgres:@127.0.0.1:5432/your-database-name +PAYLOAD_SECRET=YOUR_SECRET_HERE -# Secrets -APP_KEYS="toBeModified1,toBeModified2" -API_TOKEN_SALT=tobemodified -ADMIN_JWT_SECRET=tobemodified -TRANSFER_TOKEN_SALT=tobemodified +S3_BUCKET= +S3_ACCESS_KEY_ID= +S3_SECRET_ACCESS_KEY= +S3_REGION= +S3_ENDPOINT= -# Database -DATABASE_CLIENT=postgres -DATABASE_SSL=false -DATABASE_FILENAME=.tmp/data.db -DATABASE_URL=tobemodified -DATABASE_HOST=127.0.0.1 -DATABASE_PORT=3302 -DATABASE_NAME=postgres -DATABASE_USER=postgres -DATABASE_PASSWORD=postgres -DATABASE_SCHEMA=public -JWT_SECRET=tobemodified - -# Supabase Provider -SUPABASE_URL= -SUPABASE_API_URL= -SUPABASE_API_KEY= -SUPABASE_BUCKET=cms -SUPABASE_DIRECTORY= +NEXT_PUBLIC_SERVER_URL=http://localhost:3000 +CRON_SECRET=YOUR_CRON_SECRET_HERE +PREVIEW_SECRET=YOUR_SECRET_HERE diff --git a/apps/cms/.gitignore b/apps/cms/.gitignore index bc4262f4354..3d53a40bcb8 100644 --- a/apps/cms/.gitignore +++ b/apps/cms/.gitignore @@ -1,131 +1,43 @@ -############################ -# OS X -############################ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +/.idea/* +!/.idea/runConfigurations + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc .DS_Store -.AppleDouble -.LSOverride -Icon -.Spotlight-V100 -.Trashes -._* +*.pem +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* -############################ -# Linux -############################ +# local env files +.env*.local -*~ +# vercel +.vercel - -############################ -# Windows -############################ - -Thumbs.db -ehthumbs.db -Desktop.ini -$RECYCLE.BIN/ -*.cab -*.msi -*.msm -*.msp - - -############################ -# Packages -############################ - -*.7z -*.csv -*.dat -*.dmg -*.gz -*.iso -*.jar -*.rar -*.tar -*.zip -*.com -*.class -*.dll -*.exe -*.o -*.seed -*.so -*.swo -*.swp -*.swn -*.swm -*.out -*.pid - - -############################ -# Logs and databases -############################ - -.tmp -*.log -*.sql -*.sqlite -*.sqlite3 - - -############################ -# Misc. -############################ - -*# -ssl -.idea -nbproject -public/uploads/* -!public/uploads/.gitkeep -.tsbuildinfo -.eslintcache - -############################ -# Node.js -############################ - -lib-cov -lcov.info -pids -logs -results -node_modules -.node_history - -############################ -# Package managers -############################ - -.yarn/* -!.yarn/cache -!.yarn/unplugged -!.yarn/patches -!.yarn/releases -!.yarn/sdks -!.yarn/versions -.pnp.* -yarn-error.log - -############################ -# Tests -############################ - -coverage - -############################ -# Strapi -############################ +# typescript +*.tsbuildinfo +next-env.d.ts .env -license.txt -exports -.strapi -dist -build -.strapi-updater.json -.strapi-cloud.json \ No newline at end of file + +/media diff --git a/apps/cms-payload/.npmrc b/apps/cms/.npmrc similarity index 100% rename from apps/cms-payload/.npmrc rename to apps/cms/.npmrc diff --git a/apps/cms-payload/.prettierrc.json b/apps/cms/.prettierrc.json similarity index 100% rename from apps/cms-payload/.prettierrc.json rename to apps/cms/.prettierrc.json diff --git a/apps/cms-payload/.yarnrc b/apps/cms/.yarnrc similarity index 100% rename from apps/cms-payload/.yarnrc rename to apps/cms/.yarnrc diff --git a/apps/cms-payload/Dockerfile b/apps/cms/Dockerfile similarity index 100% rename from apps/cms-payload/Dockerfile rename to apps/cms/Dockerfile diff --git a/apps/cms/README.md b/apps/cms/README.md index f8386c3b6a9..4a2dde69c19 100644 --- a/apps/cms/README.md +++ b/apps/cms/README.md @@ -1,17 +1,67 @@ -### `develop` +# Payload Blank Template -Start the Strapi application with autoReload enabled. +This template comes configured with the bare minimum to get started on anything you need. -From the `apps/cms` directory: +## Quick start -``` -pnpm run develop -``` +This template can be deployed directly from our Cloud hosting and it will setup MongoDB and cloud S3 object storage for media. -From the monorepo root: +## Quick Start - local setup -``` -pnpm dev:cms -``` +To spin up this template locally, follow these steps: -You also need to add the .env vars either to point to the hosted supabase project or to the local supabase project inside the `apps/cms/supabase` directory. +### Clone + +After you click the `Deploy` button above, you'll want to have standalone copy of this repo on your machine. If you've already cloned this repo, skip to [Development](#development). + +### Development + +1. First [clone the repo](#clone) if you have not done so already +2. `cd my-project && cp .env.example .env` to copy the example environment variables. You'll need to add the `MONGODB_URI` from your Cloud project to your `.env` if you want to use S3 storage and the MongoDB database that was created for you. + +3. `pnpm install && pnpm dev` to install dependencies and start the dev server +4. open `http://localhost:3000` to open the app in your browser + +That's it! Changes made in `./src` will be reflected in your app. Follow the on-screen instructions to login and create your first admin user. Then check out [Production](#production) once you're ready to build and serve your app, and [Deployment](#deployment) when you're ready to go live. + +#### Docker (Optional) + +If you prefer to use Docker for local development instead of a local MongoDB instance, the provided docker-compose.yml file can be used. + +To do so, follow these steps: + +- Modify the `MONGODB_URI` in your `.env` file to `mongodb://127.0.0.1/` +- Modify the `docker-compose.yml` file's `MONGODB_URI` to match the above `` +- Run `docker-compose up` to start the database, optionally pass `-d` to run in the background. + +## How it works + +The Payload config is tailored specifically to the needs of most websites. It is pre-configured in the following ways: + +### Collections + +See the [Collections](https://payloadcms.com/docs/configuration/collections) docs for details on how to extend this functionality. + +- #### Users (Authentication) + + Users are auth-enabled collections that have access to the admin panel. + + For additional help, see the official [Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth) or the [Authentication](https://payloadcms.com/docs/authentication/overview#authentication-overview) docs. + +- #### Media + + This is the uploads enabled collection. It features pre-configured sizes, focal point and manual resizing to help you manage your pictures. + +### Docker + +Alternatively, you can use [Docker](https://www.docker.com) to spin up this template locally. To do so, follow these steps: + +1. Follow [steps 1 and 2 from above](#development), the docker-compose file will automatically use the `.env` file in your project root +1. Next run `docker-compose up` +1. Follow [steps 4 and 5 from above](#development) to login and create your first admin user + +That's it! The Docker instance will help you get up and running quickly while also standardizing the development environment across your teams. + +## Questions + +If you have any issues or questions, reach out to us on [Discord](https://discord.com/invite/payload) or start a [GitHub discussion](https://github.com/payloadcms/payload/discussions). diff --git a/apps/cms/config/admin.js b/apps/cms/config/admin.js deleted file mode 100644 index ec24ece814d..00000000000 --- a/apps/cms/config/admin.js +++ /dev/null @@ -1,54 +0,0 @@ -module.exports = ({ env }) => ({ - auth: { - secret: env('ADMIN_JWT_SECRET'), - }, - apiToken: { - salt: env('API_TOKEN_SALT'), - }, - transfer: { - token: { - salt: env('TRANSFER_TOKEN_SALT'), - }, - }, - url: env('PUBLIC_URL', 'http://localhost:1337'), - serveAdminPanel: true, - config: { - locales: ['en'], - host: env('HOST', '0.0.0.0'), - port: env.int('PORT', 1337), - admin: { - auth: { - secret: env('ADMIN_JWT_SECRET'), - }, - }, - }, - security: { - csp: { - enabled: false, - policy: { - 'default-src': ["'self'"], - 'img-src': [ - "'self'", - 'data:', - 'blob:', - 'https://market-assets.strapi.io', - 'https://koovxkbhviltdcwwaqvc.supabase.co', - 'https://*.supabase.co', - 'https://*.supabase.in', - ], - 'media-src': [ - "'self'", - 'data:', - 'blob:', - 'https://market-assets.strapi.io', - 'https://koovxkbhviltdcwwaqvc.supabase.co', - 'https://*.supabase.co', - 'https://*.supabase.in', - ], - 'script-src': ["'self'", "'unsafe-inline'", "'unsafe-eval'"], - 'style-src': ["'self'", "'unsafe-inline'"], - 'connect-src': ["'self'", 'https:', 'http:'], - }, - }, - }, -}) diff --git a/apps/cms/config/admin.ts b/apps/cms/config/admin.ts deleted file mode 100644 index a802a12cc01..00000000000 --- a/apps/cms/config/admin.ts +++ /dev/null @@ -1,37 +0,0 @@ -export default ({ env }) => ({ - auth: { - secret: env('ADMIN_JWT_SECRET'), - }, - apiToken: { - salt: env('API_TOKEN_SALT'), - }, - transfer: { - token: { - salt: env('TRANSFER_TOKEN_SALT'), - }, - }, - flags: { - nps: env.bool('FLAG_NPS', true), - promoteEE: env.bool('FLAG_PROMOTE_EE', true), - }, - url: env('PUBLIC_URL', 'http://localhost:1337'), - serveAdminPanel: true, - config: { - locales: ['en'], - host: env('HOST', '0.0.0.0'), - port: env.int('PORT', 1337), - admin: { - auth: { - secret: env('ADMIN_JWT_SECRET'), - }, - url: env('PUBLIC_URL', 'http://localhost:1337'), - serveAdminPanel: true, - watchIgnoreFiles: ['**/config/sync/**'], - }, - }, - security: { - csp: { - enabled: false, - }, - }, -}) diff --git a/apps/cms/config/api.ts b/apps/cms/config/api.ts deleted file mode 100644 index 37f7c14a422..00000000000 --- a/apps/cms/config/api.ts +++ /dev/null @@ -1,7 +0,0 @@ -export default { - rest: { - defaultLimit: 25, - maxLimit: 100, - withCount: true, - }, -}; diff --git a/apps/cms/config/database.ts b/apps/cms/config/database.ts deleted file mode 100644 index f90046008c1..00000000000 --- a/apps/cms/config/database.ts +++ /dev/null @@ -1,32 +0,0 @@ -export default ({ env }) => { - const client = env('DATABASE_CLIENT', 'postgres') - - return { - connection: { - client, - connection: { - connectionString: env('DATABASE_URL'), - host: env('DATABASE_HOST', 'localhost'), - port: env.int('DATABASE_PORT', 3302), - database: env('DATABASE_NAME', 'postgres'), - user: env('DATABASE_USER', 'postgres'), - password: env('DATABASE_PASSWORD', 'postgres'), - schema: env('DATABASE_SCHEMA', 'public'), - ssl: env.bool('DATABASE_SSL', false) && { - rejectUnauthorized: env.bool('DATABASE_SSL_REJECT_UNAUTHORIZED', true), - }, - }, - pool: { - min: 0, - max: 10, - acquireTimeoutMillis: 300000, - createTimeoutMillis: 300000, - idleTimeoutMillis: 30000, - reapIntervalMillis: 1000, - createRetryIntervalMillis: 100, - }, - debug: true, - acquireConnectionTimeout: 300000, - }, - } -} diff --git a/apps/cms/config/middlewares.js b/apps/cms/config/middlewares.js deleted file mode 100644 index 2babcfa6ebd..00000000000 --- a/apps/cms/config/middlewares.js +++ /dev/null @@ -1,48 +0,0 @@ -export default ({ env }) => [ - 'strapi::logger', - 'strapi::errors', - { - name: 'strapi::security', - config: { - contentSecurityPolicy: { - useDefaults: true, - directives: { - 'connect-src': ["'self'", 'https:', 'http:'], - 'default-src': ["'self'"], - 'img-src': [ - "'self'", - 'data:', - 'blob:', - 'market-assets.strapi.io', - 'avatars.githubusercontent.com', - env('SUPABASE_URL'), - ], - 'media-src': [ - "'self'", - 'data:', - 'blob:', - 'market-assets.strapi.io', - 'avatars.githubusercontent.com', - env('SUPABASE_URL'), - ], - }, - }, - }, - }, - { - name: 'strapi::cors', - config: { - enabled: true, - origin: ['http://localhost:3000', 'http://localhost:1337'], - methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'], - headers: ['Content-Type', 'Authorization', 'Origin', 'Accept'], - keepHeaderOnError: true, - }, - }, - 'strapi::poweredBy', - 'strapi::query', - 'strapi::body', - 'strapi::session', - 'strapi::favicon', - 'strapi::public', -] diff --git a/apps/cms/config/plugins.ts b/apps/cms/config/plugins.ts deleted file mode 100644 index f4215d3e091..00000000000 --- a/apps/cms/config/plugins.ts +++ /dev/null @@ -1,23 +0,0 @@ -export default ({ env }) => ({ - upload: { - config: { - provider: 'strapi-provider-upload-supabase', - providerOptions: { - apiUrl: env('SUPABASE_API_URL'), - apiKey: env('SUPABASE_API_KEY'), - bucket: env('SUPABASE_BUCKET'), - directory: env('SUPABASE_DIRECTORY'), - options: { - cacheControl: 'public, max-age=31536000', - }, - }, - }, - }, - 'strapi-provider-upload-supabase': { - // enabled: true, - config: { - baseUrl: `${env('SUPABASE_URL')}/storage/v1/object/public/${env('SUPABASE_BUCKET')}`, - prefix: '', - }, - }, -}) diff --git a/apps/cms/config/server.ts b/apps/cms/config/server.ts deleted file mode 100644 index 0e7801b8036..00000000000 --- a/apps/cms/config/server.ts +++ /dev/null @@ -1,11 +0,0 @@ -export default ({ env }) => ({ - host: env('HOST', '0.0.0.0'), - port: env.int('PORT', 1337), - app: { - keys: env.array('APP_KEYS'), - }, - webhooks: { - populateRelations: env.bool('WEBHOOKS_POPULATE_RELATIONS', false), - }, - url: env('PUBLIC_URL', 'http://localhost:1337'), -}) diff --git a/apps/cms/database/migrations/.gitkeep b/apps/cms/database/migrations/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/apps/cms-payload/docker-compose.yml b/apps/cms/docker-compose.yml similarity index 100% rename from apps/cms-payload/docker-compose.yml rename to apps/cms/docker-compose.yml diff --git a/apps/cms-payload/eslint.config.mjs b/apps/cms/eslint.config.mjs similarity index 100% rename from apps/cms-payload/eslint.config.mjs rename to apps/cms/eslint.config.mjs diff --git a/apps/cms/favicon.png b/apps/cms/favicon.png deleted file mode 100644 index df668a881b5..00000000000 Binary files a/apps/cms/favicon.png and /dev/null differ diff --git a/apps/cms-payload/next.config.mjs b/apps/cms/next.config.mjs similarity index 100% rename from apps/cms-payload/next.config.mjs rename to apps/cms/next.config.mjs diff --git a/apps/cms/package.json b/apps/cms/package.json index f97d49c2106..4421a468afd 100644 --- a/apps/cms/package.json +++ b/apps/cms/package.json @@ -1,38 +1,57 @@ { "name": "cms", - "version": "0.1.0", - "private": true, - "description": "A Strapi application", + "version": "1.0.0", + "description": "CMS based on Payload", + "license": "MIT", + "type": "module", "scripts": { - "build": "strapi build", - "console": "strapi console", - "deploy": "strapi deploy", - "dev": "strapi develop --debug", - "develop": "strapi develop", - "start": "strapi start", - "strapi": "strapi", - "upgrade": "npx @strapi/upgrade latest", - "upgrade:dry": "npx @strapi/upgrade latest --dry" + "build": "cross-env NODE_OPTIONS=--no-deprecation next build --turbopack", + "dev": "cross-env NODE_OPTIONS=--no-deprecation next dev --turbopack --port 3030", + "devsafe": "rm -rf .next && cross-env NODE_OPTIONS=--no-deprecation next dev", + "generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap", + "generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types", + "lint": "cross-env NODE_OPTIONS=--no-deprecation next lint", + "payload": "cross-env NODE_OPTIONS=--no-deprecation payload", + "start": "cross-env NODE_OPTIONS=--no-deprecation next start", + "ci": "cross-env NODE_OPTIONS=--no-deprecation payload migrate && pnpm build" }, "dependencies": { - "@strapi/plugin-cloud": "5.12.5", - "@strapi/plugin-users-permissions": "5.12.5", - "@strapi/strapi": "5.12.5", - "better-sqlite3": "11.3.0", - "pg": "^8.13.1", - "react": "^18.0.0", - "react-dom": "^18.0.0", - "react-router-dom": "^6.0.0", - "strapi-provider-upload-supabase": "^1.0.0", - "styled-components": "^6.0.0" + "@payloadcms/db-postgres": "3.33.0", + "@payloadcms/next": "3.33.0", + "@payloadcms/payload-cloud": "3.33.0", + "@payloadcms/plugin-form-builder": "3.33.0", + "@payloadcms/plugin-nested-docs": "3.33.0", + "@payloadcms/plugin-redirects": "3.33.0", + "@payloadcms/plugin-search": "3.33.0", + "@payloadcms/plugin-seo": "3.33.0", + "@payloadcms/richtext-lexical": "3.33.0", + "@payloadcms/storage-s3": "^3.33.0", + "@payloadcms/ui": "3.33.0", + "cross-env": "^7.0.3", + "graphql": "^16.8.1", + "next": "15.3.0", + "payload": "3.33.0", + "react": "19.1.0", + "react-dom": "19.1.0", + "sharp": "0.32.6" }, "devDependencies": { - "@types/node": "^20", - "@types/react": "^18", - "@types/react-dom": "^18", - "typescript": "^5" + "@eslint/eslintrc": "^3.2.0", + "@types/node": "^22.5.4", + "@types/react": "19.1.0", + "@types/react-dom": "19.1.2", + "eslint": "^9.16.0", + "eslint-config-next": "15.3.0", + "prettier": "^3.4.2", + "typescript": "5.7.3" }, - "strapi": { - "uuid": "b85c4d47-a812-4181-9373-f0524815c5f0" + "engines": { + "node": "^18.20.2 || >=20.9.0", + "pnpm": "^9 || ^10" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "sharp" + ] } } diff --git a/apps/cms/public/robots.txt b/apps/cms/public/robots.txt deleted file mode 100644 index ff5d3164e04..00000000000 --- a/apps/cms/public/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# To prevent search engines from seeing the site altogether, uncomment the next two lines: -# User-Agent: * -# Disallow: / diff --git a/apps/cms/public/uploads/.gitkeep b/apps/cms/public/uploads/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/apps/cms-payload/redirects.js b/apps/cms/redirects.js similarity index 100% rename from apps/cms-payload/redirects.js rename to apps/cms/redirects.js diff --git a/apps/cms/src/access/anyone.ts b/apps/cms/src/access/anyone.ts new file mode 100644 index 00000000000..bf37c3a1157 --- /dev/null +++ b/apps/cms/src/access/anyone.ts @@ -0,0 +1,3 @@ +import type { Access } from 'payload' + +export const anyone: Access = () => true diff --git a/apps/cms/src/access/authenticated.ts b/apps/cms/src/access/authenticated.ts new file mode 100644 index 00000000000..e2dc34d2a8c --- /dev/null +++ b/apps/cms/src/access/authenticated.ts @@ -0,0 +1,9 @@ +import type { AccessArgs } from 'payload' + +import type { User } from '@/payload-types' + +type isAuthenticated = (args: AccessArgs) => boolean + +export const authenticated: isAuthenticated = ({ req: { user } }) => { + return Boolean(user) +} diff --git a/apps/cms/src/access/authenticatedOrPublished.ts b/apps/cms/src/access/authenticatedOrPublished.ts new file mode 100644 index 00000000000..e49198fbaaa --- /dev/null +++ b/apps/cms/src/access/authenticatedOrPublished.ts @@ -0,0 +1,13 @@ +import type { Access } from 'payload' + +export const authenticatedOrPublished: Access = ({ req: { user } }) => { + if (user) { + return true + } + + return { + _status: { + equals: 'published', + }, + } +} diff --git a/apps/cms/src/admin/app.example.tsx b/apps/cms/src/admin/app.example.tsx deleted file mode 100644 index 3ea4d919fb5..00000000000 --- a/apps/cms/src/admin/app.example.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import type { StrapiApp } from '@strapi/strapi/admin'; - -export default { - config: { - locales: [ - // 'ar', - // 'fr', - // 'cs', - // 'de', - // 'dk', - // 'es', - // 'he', - // 'id', - // 'it', - // 'ja', - // 'ko', - // 'ms', - // 'nl', - // 'no', - // 'pl', - // 'pt-BR', - // 'pt', - // 'ru', - // 'sk', - // 'sv', - // 'th', - // 'tr', - // 'uk', - // 'vi', - // 'zh-Hans', - // 'zh', - ], - }, - bootstrap(app: StrapiApp) { - console.log(app); - }, -}; diff --git a/apps/cms/src/admin/tsconfig.json b/apps/cms/src/admin/tsconfig.json deleted file mode 100644 index 083046e7cd7..00000000000 --- a/apps/cms/src/admin/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "Bundler", - "useDefineForClassFields": true, - "lib": ["DOM", "DOM.Iterable", "ESNext"], - "allowJs": false, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "noEmit": true, - "jsx": "react-jsx" - }, - "include": ["../plugins/**/admin/src/**/*", "./"], - "exclude": ["node_modules/", "build/", "dist/", "**/*.test.ts"] -} diff --git a/apps/cms/src/admin/vite.config.example.ts b/apps/cms/src/admin/vite.config.example.ts deleted file mode 100644 index 85f6982bdda..00000000000 --- a/apps/cms/src/admin/vite.config.example.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { mergeConfig, type UserConfig } from 'vite'; - -export default (config: UserConfig) => { - // Important: always return the modified config - return mergeConfig(config, { - resolve: { - alias: { - '@': '/src', - }, - }, - }); -}; diff --git a/apps/cms/src/api/.gitkeep b/apps/cms/src/api/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/apps/cms/src/api/author/content-types/author/schema.json b/apps/cms/src/api/author/content-types/author/schema.json deleted file mode 100644 index 84bff0ac974..00000000000 --- a/apps/cms/src/api/author/content-types/author/schema.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "kind": "collectionType", - "collectionName": "authors", - "info": { - "singularName": "author", - "pluralName": "authors", - "displayName": "Author", - "description": "" - }, - "options": { - "draftAndPublish": true - }, - "attributes": { - "author": { - "type": "string" - }, - "author_id": { - "type": "string" - }, - "position": { - "type": "string" - }, - "author_url": { - "type": "string" - }, - "author_image_url": { - "type": "media", - "multiple": false, - "required": false, - "allowedTypes": [ - "images" - ] - }, - "username": { - "type": "string" - } - } -} diff --git a/apps/cms/src/api/author/controllers/author.ts b/apps/cms/src/api/author/controllers/author.ts deleted file mode 100644 index 091b2c3a68b..00000000000 --- a/apps/cms/src/api/author/controllers/author.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * author controller - */ - -import { factories } from '@strapi/strapi' - -export default factories.createCoreController('api::author.author'); diff --git a/apps/cms/src/api/author/routes/author.ts b/apps/cms/src/api/author/routes/author.ts deleted file mode 100644 index d85383ad665..00000000000 --- a/apps/cms/src/api/author/routes/author.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * author router - */ - -import { factories } from '@strapi/strapi'; - -export default factories.createCoreRouter('api::author.author'); diff --git a/apps/cms/src/api/author/services/author.ts b/apps/cms/src/api/author/services/author.ts deleted file mode 100644 index cf062dc28ff..00000000000 --- a/apps/cms/src/api/author/services/author.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * author service - */ - -import { factories } from '@strapi/strapi'; - -export default factories.createCoreService('api::author.author'); diff --git a/apps/cms/src/api/blog-post/content-types/blog-post/schema.json b/apps/cms/src/api/blog-post/content-types/blog-post/schema.json deleted file mode 100644 index 23feda6944e..00000000000 --- a/apps/cms/src/api/blog-post/content-types/blog-post/schema.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "kind": "collectionType", - "collectionName": "blog_posts", - "info": { - "singularName": "blog-post", - "pluralName": "blog-posts", - "displayName": "Blog Post", - "description": "" - }, - "options": { - "draftAndPublish": true - }, - "attributes": { - "Title": { - "type": "string", - "required": true - }, - "slug": { - "type": "string", - "unique": true, - "regex": "^[a-z][a-z0-9-]{2,49}$", - "required": true - }, - "thumb": { - "type": "media", - "multiple": false, - "required": false, - "allowedTypes": [ - "images", - "files", - "videos", - "audios" - ] - }, - "date": { - "type": "datetime" - }, - "image": { - "type": "media", - "multiple": false, - "required": false, - "allowedTypes": [ - "images", - "files", - "videos", - "audios" - ] - }, - "toc_depth": { - "type": "integer", - "default": 2 - }, - "content": { - "type": "richtext" - }, - "authors": { - "type": "relation", - "relation": "oneToMany", - "target": "api::author.author" - }, - "tags": { - "type": "relation", - "relation": "oneToMany", - "target": "api::tag.tag" - }, - "categories": { - "type": "relation", - "relation": "oneToMany", - "target": "api::category.category" - }, - "description": { - "type": "text" - }, - "launchweek": { - "type": "string" - } - } -} diff --git a/apps/cms/src/api/blog-post/controllers/blog-post.ts b/apps/cms/src/api/blog-post/controllers/blog-post.ts deleted file mode 100644 index 4e7c76b4ccb..00000000000 --- a/apps/cms/src/api/blog-post/controllers/blog-post.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * blog-post controller - */ - -import { factories } from '@strapi/strapi' - -export default factories.createCoreController('api::blog-post.blog-post'); diff --git a/apps/cms/src/api/blog-post/routes/blog-post.ts b/apps/cms/src/api/blog-post/routes/blog-post.ts deleted file mode 100644 index 965c3d720e8..00000000000 --- a/apps/cms/src/api/blog-post/routes/blog-post.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * blog-post router - */ - -import { factories } from '@strapi/strapi'; - -export default factories.createCoreRouter('api::blog-post.blog-post'); diff --git a/apps/cms/src/api/blog-post/services/blog-post.ts b/apps/cms/src/api/blog-post/services/blog-post.ts deleted file mode 100644 index b325fb357ba..00000000000 --- a/apps/cms/src/api/blog-post/services/blog-post.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * blog-post service - */ - -import { factories } from '@strapi/strapi'; - -export default factories.createCoreService('api::blog-post.blog-post'); diff --git a/apps/cms/src/api/category/content-types/category/schema.json b/apps/cms/src/api/category/content-types/category/schema.json deleted file mode 100644 index 8314b12e84b..00000000000 --- a/apps/cms/src/api/category/content-types/category/schema.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "kind": "collectionType", - "collectionName": "categories", - "info": { - "singularName": "category", - "pluralName": "categories", - "displayName": "Category" - }, - "options": { - "draftAndPublish": true - }, - "attributes": { - "name": { - "type": "string" - } - } -} diff --git a/apps/cms/src/api/category/controllers/category.ts b/apps/cms/src/api/category/controllers/category.ts deleted file mode 100644 index 4f9f5213ebe..00000000000 --- a/apps/cms/src/api/category/controllers/category.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * category controller - */ - -import { factories } from '@strapi/strapi' - -export default factories.createCoreController('api::category.category'); diff --git a/apps/cms/src/api/category/routes/category.ts b/apps/cms/src/api/category/routes/category.ts deleted file mode 100644 index 29daf72f7e2..00000000000 --- a/apps/cms/src/api/category/routes/category.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * category router - */ - -import { factories } from '@strapi/strapi'; - -export default factories.createCoreRouter('api::category.category'); diff --git a/apps/cms/src/api/category/services/category.ts b/apps/cms/src/api/category/services/category.ts deleted file mode 100644 index b4d79e27c6b..00000000000 --- a/apps/cms/src/api/category/services/category.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * category service - */ - -import { factories } from '@strapi/strapi'; - -export default factories.createCoreService('api::category.category'); diff --git a/apps/cms/src/api/tag/content-types/tag/schema.json b/apps/cms/src/api/tag/content-types/tag/schema.json deleted file mode 100644 index 531527e704b..00000000000 --- a/apps/cms/src/api/tag/content-types/tag/schema.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "kind": "collectionType", - "collectionName": "tags", - "info": { - "singularName": "tag", - "pluralName": "tags", - "displayName": "Tag", - "description": "" - }, - "options": { - "draftAndPublish": true - }, - "attributes": { - "name": { - "type": "string" - } - } -} diff --git a/apps/cms/src/api/tag/controllers/tag.ts b/apps/cms/src/api/tag/controllers/tag.ts deleted file mode 100644 index 0b3e99721d3..00000000000 --- a/apps/cms/src/api/tag/controllers/tag.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * tag controller - */ - -import { factories } from '@strapi/strapi' - -export default factories.createCoreController('api::tag.tag'); diff --git a/apps/cms/src/api/tag/routes/tag.ts b/apps/cms/src/api/tag/routes/tag.ts deleted file mode 100644 index d15b0cf382c..00000000000 --- a/apps/cms/src/api/tag/routes/tag.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * tag router - */ - -import { factories } from '@strapi/strapi'; - -export default factories.createCoreRouter('api::tag.tag'); diff --git a/apps/cms/src/api/tag/services/tag.ts b/apps/cms/src/api/tag/services/tag.ts deleted file mode 100644 index d45b6763d7c..00000000000 --- a/apps/cms/src/api/tag/services/tag.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * tag service - */ - -import { factories } from '@strapi/strapi'; - -export default factories.createCoreService('api::tag.tag'); diff --git a/apps/cms-payload/src/app/(frontend)/layout.tsx b/apps/cms/src/app/(frontend)/layout.tsx similarity index 100% rename from apps/cms-payload/src/app/(frontend)/layout.tsx rename to apps/cms/src/app/(frontend)/layout.tsx diff --git a/apps/cms-payload/src/app/(frontend)/page.tsx b/apps/cms/src/app/(frontend)/page.tsx similarity index 100% rename from apps/cms-payload/src/app/(frontend)/page.tsx rename to apps/cms/src/app/(frontend)/page.tsx diff --git a/apps/cms-payload/src/app/(frontend)/styles.css b/apps/cms/src/app/(frontend)/styles.css similarity index 100% rename from apps/cms-payload/src/app/(frontend)/styles.css rename to apps/cms/src/app/(frontend)/styles.css diff --git a/apps/cms-payload/src/app/(payload)/admin/[[...segments]]/not-found.tsx b/apps/cms/src/app/(payload)/admin/[[...segments]]/not-found.tsx similarity index 100% rename from apps/cms-payload/src/app/(payload)/admin/[[...segments]]/not-found.tsx rename to apps/cms/src/app/(payload)/admin/[[...segments]]/not-found.tsx diff --git a/apps/cms-payload/src/app/(payload)/admin/[[...segments]]/page.tsx b/apps/cms/src/app/(payload)/admin/[[...segments]]/page.tsx similarity index 100% rename from apps/cms-payload/src/app/(payload)/admin/[[...segments]]/page.tsx rename to apps/cms/src/app/(payload)/admin/[[...segments]]/page.tsx diff --git a/apps/cms-payload/src/app/(payload)/admin/importMap.js b/apps/cms/src/app/(payload)/admin/importMap.js similarity index 51% rename from apps/cms-payload/src/app/(payload)/admin/importMap.js rename to apps/cms/src/app/(payload)/admin/importMap.js index a68a9c3c1e7..327fe16985e 100644 --- a/apps/cms-payload/src/app/(payload)/admin/importMap.js +++ b/apps/cms/src/app/(payload)/admin/importMap.js @@ -1,51 +1,47 @@ +import { SlugComponent as SlugComponent_92cc057d0a2abb4f6cf0307edf59f986 } from '@/fields/slug/SlugComponent' import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc' import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc' import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc' -import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { FixedToolbarFeatureClient as FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { OverviewComponent as OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client' +import { MetaTitleComponent as MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client' +import { MetaImageComponent as MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client' +import { MetaDescriptionComponent as MetaDescriptionComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client' +import { PreviewComponent as PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client' +import { LinkToDoc as LinkToDoc_aead06e4cbf6b2620c5c51c9ab283634 } from '@payloadcms/plugin-search/client' +import { ReindexButton as ReindexButton_aead06e4cbf6b2620c5c51c9ab283634 } from '@payloadcms/plugin-search/client' import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client' export const importMap = { + "@/fields/slug/SlugComponent#SlugComponent": SlugComponent_92cc057d0a2abb4f6cf0307edf59f986, "@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e, "@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e, "@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e, - "@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#UploadFeatureClient": UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#BlockquoteFeatureClient": BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#RelationshipFeatureClient": RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#ChecklistFeatureClient": ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#OrderedListFeatureClient": OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#UnorderedListFeatureClient": UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#IndentFeatureClient": IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#AlignFeatureClient": AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#FixedToolbarFeatureClient": FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#InlineCodeFeatureClient": InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#SuperscriptFeatureClient": SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#SubscriptFeatureClient": SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/plugin-seo/client#OverviewComponent": OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860, + "@payloadcms/plugin-seo/client#MetaTitleComponent": MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c0860, + "@payloadcms/plugin-seo/client#MetaImageComponent": MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860, + "@payloadcms/plugin-seo/client#MetaDescriptionComponent": MetaDescriptionComponent_a8a977ebc872c5d5ea7ee689724c0860, + "@payloadcms/plugin-seo/client#PreviewComponent": PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860, + "@payloadcms/plugin-search/client#LinkToDoc": LinkToDoc_aead06e4cbf6b2620c5c51c9ab283634, + "@payloadcms/plugin-search/client#ReindexButton": ReindexButton_aead06e4cbf6b2620c5c51c9ab283634, "@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } diff --git a/apps/cms-payload/src/app/(payload)/api/[...slug]/route.ts b/apps/cms/src/app/(payload)/api/[...slug]/route.ts similarity index 100% rename from apps/cms-payload/src/app/(payload)/api/[...slug]/route.ts rename to apps/cms/src/app/(payload)/api/[...slug]/route.ts diff --git a/apps/cms-payload/src/app/(payload)/api/graphql-playground/route.ts b/apps/cms/src/app/(payload)/api/graphql-playground/route.ts similarity index 100% rename from apps/cms-payload/src/app/(payload)/api/graphql-playground/route.ts rename to apps/cms/src/app/(payload)/api/graphql-playground/route.ts diff --git a/apps/cms-payload/src/app/(payload)/api/graphql/route.ts b/apps/cms/src/app/(payload)/api/graphql/route.ts similarity index 100% rename from apps/cms-payload/src/app/(payload)/api/graphql/route.ts rename to apps/cms/src/app/(payload)/api/graphql/route.ts diff --git a/apps/cms-payload/src/app/(payload)/custom.scss b/apps/cms/src/app/(payload)/custom.scss similarity index 100% rename from apps/cms-payload/src/app/(payload)/custom.scss rename to apps/cms/src/app/(payload)/custom.scss diff --git a/apps/cms-payload/src/app/(payload)/layout.tsx b/apps/cms/src/app/(payload)/layout.tsx similarity index 100% rename from apps/cms-payload/src/app/(payload)/layout.tsx rename to apps/cms/src/app/(payload)/layout.tsx diff --git a/apps/cms-payload/src/app/my-route/route.ts b/apps/cms/src/app/my-route/route.ts similarity index 100% rename from apps/cms-payload/src/app/my-route/route.ts rename to apps/cms/src/app/my-route/route.ts diff --git a/apps/cms/src/blocks/ArchiveBlock/Component.tsx b/apps/cms/src/blocks/ArchiveBlock/Component.tsx new file mode 100644 index 00000000000..48a57ec5d69 --- /dev/null +++ b/apps/cms/src/blocks/ArchiveBlock/Component.tsx @@ -0,0 +1,65 @@ +import type { Post, ArchiveBlock as ArchiveBlockProps } from '@/payload-types' + +import configPromise from '@payload-config' +import { getPayload } from 'payload' +import React from 'react' +import RichText from '@/components/RichText' + +import { CollectionArchive } from '@/components/CollectionArchive' + +export const ArchiveBlock: React.FC< + ArchiveBlockProps & { + id?: string + } +> = async (props) => { + const { id, categories, introContent, limit: limitFromProps, populateBy, selectedDocs } = props + + const limit = limitFromProps || 3 + + let posts: Post[] = [] + + if (populateBy === 'collection') { + const payload = await getPayload({ config: configPromise }) + + const flattenedCategories = categories?.map((category) => { + if (typeof category === 'object') return category.id + else return category + }) + + const fetchedPosts = await payload.find({ + collection: 'posts', + depth: 1, + limit, + ...(flattenedCategories && flattenedCategories.length > 0 + ? { + where: { + categories: { + in: flattenedCategories, + }, + }, + } + : {}), + }) + + posts = fetchedPosts.docs + } else { + if (selectedDocs?.length) { + const filteredSelectedPosts = selectedDocs.map((post) => { + if (typeof post.value === 'object') return post.value + }) as Post[] + + posts = filteredSelectedPosts + } + } + + return ( +
+ {introContent && ( +
+ +
+ )} + +
+ ) +} diff --git a/apps/cms/src/blocks/ArchiveBlock/config.ts b/apps/cms/src/blocks/ArchiveBlock/config.ts new file mode 100644 index 00000000000..f87a376bd51 --- /dev/null +++ b/apps/cms/src/blocks/ArchiveBlock/config.ts @@ -0,0 +1,94 @@ +import type { Block } from 'payload' + +import { + FixedToolbarFeature, + HeadingFeature, + InlineToolbarFeature, + lexicalEditor, +} from '@payloadcms/richtext-lexical' + +export const Archive: Block = { + slug: 'archive', + interfaceName: 'ArchiveBlock', + fields: [ + { + name: 'introContent', + type: 'richText', + editor: lexicalEditor({ + features: ({ rootFeatures }) => { + return [ + ...rootFeatures, + HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }), + FixedToolbarFeature(), + InlineToolbarFeature(), + ] + }, + }), + label: 'Intro Content', + }, + { + name: 'populateBy', + type: 'select', + defaultValue: 'collection', + options: [ + { + label: 'Collection', + value: 'collection', + }, + { + label: 'Individual Selection', + value: 'selection', + }, + ], + }, + { + name: 'relationTo', + type: 'select', + admin: { + condition: (_, siblingData) => siblingData.populateBy === 'collection', + }, + defaultValue: 'posts', + label: 'Collections To Show', + options: [ + { + label: 'Posts', + value: 'posts', + }, + ], + }, + { + name: 'categories', + type: 'relationship', + admin: { + condition: (_, siblingData) => siblingData.populateBy === 'collection', + }, + hasMany: true, + label: 'Categories To Show', + relationTo: 'categories', + }, + { + name: 'limit', + type: 'number', + admin: { + condition: (_, siblingData) => siblingData.populateBy === 'collection', + step: 1, + }, + defaultValue: 10, + label: 'Limit', + }, + { + name: 'selectedDocs', + type: 'relationship', + admin: { + condition: (_, siblingData) => siblingData.populateBy === 'selection', + }, + hasMany: true, + label: 'Selection', + relationTo: ['posts'], + }, + ], + labels: { + plural: 'Archives', + singular: 'Archive', + }, +} diff --git a/apps/cms/src/blocks/Banner/Component.tsx b/apps/cms/src/blocks/Banner/Component.tsx new file mode 100644 index 00000000000..6abb8ad09a6 --- /dev/null +++ b/apps/cms/src/blocks/Banner/Component.tsx @@ -0,0 +1,26 @@ +import type { BannerBlock as BannerBlockProps } from 'src/payload-types' + +import { cn } from '@/utilities/ui' +import React from 'react' +import RichText from '@/components/RichText' + +type Props = { + className?: string +} & BannerBlockProps + +export const BannerBlock: React.FC = ({ className, content, style }) => { + return ( +
+
+ +
+
+ ) +} diff --git a/apps/cms/src/blocks/Banner/config.ts b/apps/cms/src/blocks/Banner/config.ts new file mode 100644 index 00000000000..53e46b5cbc4 --- /dev/null +++ b/apps/cms/src/blocks/Banner/config.ts @@ -0,0 +1,37 @@ +import type { Block } from 'payload' + +import { + FixedToolbarFeature, + InlineToolbarFeature, + lexicalEditor, +} from '@payloadcms/richtext-lexical' + +export const Banner: Block = { + slug: 'banner', + fields: [ + { + name: 'style', + type: 'select', + defaultValue: 'info', + options: [ + { label: 'Info', value: 'info' }, + { label: 'Warning', value: 'warning' }, + { label: 'Error', value: 'error' }, + { label: 'Success', value: 'success' }, + ], + required: true, + }, + { + name: 'content', + type: 'richText', + editor: lexicalEditor({ + features: ({ rootFeatures }) => { + return [...rootFeatures, FixedToolbarFeature(), InlineToolbarFeature()] + }, + }), + label: false, + required: true, + }, + ], + interfaceName: 'BannerBlock', +} diff --git a/apps/cms/src/blocks/CallToAction/Component.tsx b/apps/cms/src/blocks/CallToAction/Component.tsx new file mode 100644 index 00000000000..6b3771cf232 --- /dev/null +++ b/apps/cms/src/blocks/CallToAction/Component.tsx @@ -0,0 +1,23 @@ +import React from 'react' + +import type { CallToActionBlock as CTABlockProps } from '@/payload-types' + +import RichText from '@/components/RichText' +import { CMSLink } from '@/components/Link' + +export const CallToActionBlock: React.FC = ({ links, richText }) => { + return ( +
+
+
+ {richText && } +
+
+ {(links || []).map(({ link }, i) => { + return + })} +
+
+
+ ) +} diff --git a/apps/cms/src/blocks/CallToAction/config.ts b/apps/cms/src/blocks/CallToAction/config.ts new file mode 100644 index 00000000000..f4ffa77c6a1 --- /dev/null +++ b/apps/cms/src/blocks/CallToAction/config.ts @@ -0,0 +1,42 @@ +import type { Block } from 'payload' + +import { + FixedToolbarFeature, + HeadingFeature, + InlineToolbarFeature, + lexicalEditor, +} from '@payloadcms/richtext-lexical' + +import { linkGroup } from '../../fields/linkGroup' + +export const CallToAction: Block = { + slug: 'cta', + interfaceName: 'CallToActionBlock', + fields: [ + { + name: 'richText', + type: 'richText', + editor: lexicalEditor({ + features: ({ rootFeatures }) => { + return [ + ...rootFeatures, + HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }), + FixedToolbarFeature(), + InlineToolbarFeature(), + ] + }, + }), + label: false, + }, + linkGroup({ + appearances: ['default', 'outline'], + overrides: { + maxRows: 2, + }, + }), + ], + labels: { + plural: 'Calls to Action', + singular: 'Call to Action', + }, +} diff --git a/apps/cms/src/blocks/Code/Component.client.tsx b/apps/cms/src/blocks/Code/Component.client.tsx new file mode 100644 index 00000000000..fc8fb62545a --- /dev/null +++ b/apps/cms/src/blocks/Code/Component.client.tsx @@ -0,0 +1,33 @@ +'use client' +import { Highlight, themes } from 'prism-react-renderer' +import React from 'react' +import { CopyButton } from './CopyButton' + +type Props = { + code: string + language?: string +} + +export const Code: React.FC = ({ code, language = '' }) => { + if (!code) return null + + return ( + + {({ getLineProps, getTokenProps, tokens }) => ( +
+          {tokens.map((line, i) => (
+            
+ {i + 1} + + {line.map((token, key) => ( + + ))} + +
+ ))} + +
+ )} +
+ ) +} diff --git a/apps/cms/src/blocks/Code/Component.tsx b/apps/cms/src/blocks/Code/Component.tsx new file mode 100644 index 00000000000..7f776d74de3 --- /dev/null +++ b/apps/cms/src/blocks/Code/Component.tsx @@ -0,0 +1,21 @@ +import React from 'react' + +import { Code } from './Component.client' + +export type CodeBlockProps = { + code: string + language?: string + blockType: 'code' +} + +type Props = CodeBlockProps & { + className?: string +} + +export const CodeBlock: React.FC = ({ className, code, language }) => { + return ( +
+ +
+ ) +} diff --git a/apps/cms/src/blocks/Code/CopyButton.tsx b/apps/cms/src/blocks/Code/CopyButton.tsx new file mode 100644 index 00000000000..8d204bfffd6 --- /dev/null +++ b/apps/cms/src/blocks/Code/CopyButton.tsx @@ -0,0 +1,33 @@ +'use client' +import { Button } from '@/components/ui/button' +import { CopyIcon } from '@payloadcms/ui/icons/Copy' +import { useState } from 'react' + +export function CopyButton({ code }: { code: string }) { + const [text, setText] = useState('Copy') + + function updateCopyStatus() { + if (text === 'Copy') { + setText(() => 'Copied!') + setTimeout(() => { + setText(() => 'Copy') + }, 1000) + } + } + + return ( +
+ +
+ ) +} diff --git a/apps/cms/src/blocks/Code/config.ts b/apps/cms/src/blocks/Code/config.ts new file mode 100644 index 00000000000..7b26f805db2 --- /dev/null +++ b/apps/cms/src/blocks/Code/config.ts @@ -0,0 +1,33 @@ +import type { Block } from 'payload' + +export const Code: Block = { + slug: 'code', + interfaceName: 'CodeBlock', + fields: [ + { + name: 'language', + type: 'select', + defaultValue: 'typescript', + options: [ + { + label: 'Typescript', + value: 'typescript', + }, + { + label: 'Javascript', + value: 'javascript', + }, + { + label: 'CSS', + value: 'css', + }, + ], + }, + { + name: 'code', + type: 'code', + label: false, + required: true, + }, + ], +} diff --git a/apps/cms/src/blocks/Content/Component.tsx b/apps/cms/src/blocks/Content/Component.tsx new file mode 100644 index 00000000000..2c2550b7598 --- /dev/null +++ b/apps/cms/src/blocks/Content/Component.tsx @@ -0,0 +1,43 @@ +import { cn } from '@/utilities/ui' +import React from 'react' +import RichText from '@/components/RichText' + +import type { ContentBlock as ContentBlockProps } from '@/payload-types' + +import { CMSLink } from '../../components/Link' + +export const ContentBlock: React.FC = (props) => { + const { columns } = props + + const colsSpanClasses = { + full: '12', + half: '6', + oneThird: '4', + twoThirds: '8', + } + + return ( +
+
+ {columns && + columns.length > 0 && + columns.map((col, index) => { + const { enableLink, link, richText, size } = col + + return ( +
+ {richText && } + + {enableLink && } +
+ ) + })} +
+
+ ) +} diff --git a/apps/cms/src/blocks/Content/config.ts b/apps/cms/src/blocks/Content/config.ts new file mode 100644 index 00000000000..5c2fb0771d3 --- /dev/null +++ b/apps/cms/src/blocks/Content/config.ts @@ -0,0 +1,79 @@ +import type { Block, Field } from 'payload' + +import { + FixedToolbarFeature, + HeadingFeature, + InlineToolbarFeature, + lexicalEditor, +} from '@payloadcms/richtext-lexical' + +import { link } from '@/fields/link' + +const columnFields: Field[] = [ + { + name: 'size', + type: 'select', + defaultValue: 'oneThird', + options: [ + { + label: 'One Third', + value: 'oneThird', + }, + { + label: 'Half', + value: 'half', + }, + { + label: 'Two Thirds', + value: 'twoThirds', + }, + { + label: 'Full', + value: 'full', + }, + ], + }, + { + name: 'richText', + type: 'richText', + editor: lexicalEditor({ + features: ({ rootFeatures }) => { + return [ + ...rootFeatures, + HeadingFeature({ enabledHeadingSizes: ['h2', 'h3', 'h4'] }), + FixedToolbarFeature(), + InlineToolbarFeature(), + ] + }, + }), + label: false, + }, + { + name: 'enableLink', + type: 'checkbox', + }, + link({ + overrides: { + admin: { + condition: (_data, siblingData) => { + return Boolean(siblingData?.enableLink) + }, + }, + }, + }), +] + +export const Content: Block = { + slug: 'content', + interfaceName: 'ContentBlock', + fields: [ + { + name: 'columns', + type: 'array', + admin: { + initCollapsed: true, + }, + fields: columnFields, + }, + ], +} diff --git a/apps/cms/src/blocks/Form/Checkbox/index.tsx b/apps/cms/src/blocks/Form/Checkbox/index.tsx new file mode 100644 index 00000000000..633d5db0d84 --- /dev/null +++ b/apps/cms/src/blocks/Form/Checkbox/index.tsx @@ -0,0 +1,45 @@ +import type { CheckboxField } from '@payloadcms/plugin-form-builder/types' +import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form' + +import { useFormContext } from 'react-hook-form' + +import { Checkbox as CheckboxUi } from '@/components/ui/checkbox' +import { Label } from '@/components/ui/label' +import React from 'react' + +import { Error } from '../Error' +import { Width } from '../Width' + +export const Checkbox: React.FC< + CheckboxField & { + errors: Partial + register: UseFormRegister + } +> = ({ name, defaultValue, errors, label, register, required, width }) => { + const props = register(name, { required: required }) + const { setValue } = useFormContext() + + return ( + +
+ { + setValue(props.name, checked) + }} + /> + +
+ {errors[name] && } +
+ ) +} diff --git a/apps/cms/src/blocks/Form/Component.tsx b/apps/cms/src/blocks/Form/Component.tsx new file mode 100644 index 00000000000..b0b5d38cbc7 --- /dev/null +++ b/apps/cms/src/blocks/Form/Component.tsx @@ -0,0 +1,163 @@ +'use client' +import type { FormFieldBlock, Form as FormType } from '@payloadcms/plugin-form-builder/types' + +import { useRouter } from 'next/navigation' +import React, { useCallback, useState } from 'react' +import { useForm, FormProvider } from 'react-hook-form' +import RichText from '@/components/RichText' +import { Button } from '@/components/ui/button' +import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' + +import { fields } from './fields' +import { getClientSideURL } from '@/utilities/getURL' + +export type FormBlockType = { + blockName?: string + blockType?: 'formBlock' + enableIntro: boolean + form: FormType + introContent?: SerializedEditorState +} + +export const FormBlock: React.FC< + { + id?: string + } & FormBlockType +> = (props) => { + const { + enableIntro, + form: formFromProps, + form: { id: formID, confirmationMessage, confirmationType, redirect, submitButtonLabel } = {}, + introContent, + } = props + + const formMethods = useForm({ + defaultValues: formFromProps.fields, + }) + const { + control, + formState: { errors }, + handleSubmit, + register, + } = formMethods + + const [isLoading, setIsLoading] = useState(false) + const [hasSubmitted, setHasSubmitted] = useState() + const [error, setError] = useState<{ message: string; status?: string } | undefined>() + const router = useRouter() + + const onSubmit = useCallback( + (data: FormFieldBlock[]) => { + let loadingTimerID: ReturnType + const submitForm = async () => { + setError(undefined) + + const dataToSend = Object.entries(data).map(([name, value]) => ({ + field: name, + value, + })) + + // delay loading indicator by 1s + loadingTimerID = setTimeout(() => { + setIsLoading(true) + }, 1000) + + try { + const req = await fetch(`${getClientSideURL()}/api/form-submissions`, { + body: JSON.stringify({ + form: formID, + submissionData: dataToSend, + }), + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + }) + + const res = await req.json() + + clearTimeout(loadingTimerID) + + if (req.status >= 400) { + setIsLoading(false) + + setError({ + message: res.errors?.[0]?.message || 'Internal Server Error', + status: res.status, + }) + + return + } + + setIsLoading(false) + setHasSubmitted(true) + + if (confirmationType === 'redirect' && redirect) { + const { url } = redirect + + const redirectUrl = url + + if (redirectUrl) router.push(redirectUrl) + } + } catch (err) { + console.warn(err) + setIsLoading(false) + setError({ + message: 'Something went wrong.', + }) + } + } + + void submitForm() + }, + [router, formID, redirect, confirmationType], + ) + + return ( +
+ {enableIntro && introContent && !hasSubmitted && ( + + )} +
+ + {!isLoading && hasSubmitted && confirmationType === 'message' && ( + + )} + {isLoading && !hasSubmitted &&

Loading, please wait...

} + {error &&
{`${error.status || '500'}: ${error.message || ''}`}
} + {!hasSubmitted && ( +
+
+ {formFromProps && + formFromProps.fields && + formFromProps.fields?.map((field, index) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const Field: React.FC = fields?.[field.blockType as keyof typeof fields] + if (Field) { + return ( +
+ +
+ ) + } + return null + })} +
+ + +
+ )} +
+
+
+ ) +} diff --git a/apps/cms/src/blocks/Form/Country/index.tsx b/apps/cms/src/blocks/Form/Country/index.tsx new file mode 100644 index 00000000000..9c85b753972 --- /dev/null +++ b/apps/cms/src/blocks/Form/Country/index.tsx @@ -0,0 +1,65 @@ +import type { CountryField } from '@payloadcms/plugin-form-builder/types' +import type { Control, FieldErrorsImpl } from 'react-hook-form' + +import { Label } from '@/components/ui/label' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import React from 'react' +import { Controller } from 'react-hook-form' + +import { Error } from '../Error' +import { Width } from '../Width' +import { countryOptions } from './options' + +export const Country: React.FC< + CountryField & { + control: Control + errors: Partial + } +> = ({ name, control, errors, label, required, width }) => { + return ( + + + { + const controlledValue = countryOptions.find((t) => t.value === value) + + return ( + + ) + }} + rules={{ required }} + /> + {errors[name] && } + + ) +} diff --git a/apps/cms/src/blocks/Form/Country/options.ts b/apps/cms/src/blocks/Form/Country/options.ts new file mode 100644 index 00000000000..f952c1df890 --- /dev/null +++ b/apps/cms/src/blocks/Form/Country/options.ts @@ -0,0 +1,982 @@ +export const countryOptions = [ + { + label: 'Afghanistan', + value: 'AF', + }, + { + label: 'Ã…land Islands', + value: 'AX', + }, + { + label: 'Albania', + value: 'AL', + }, + { + label: 'Algeria', + value: 'DZ', + }, + { + label: 'American Samoa', + value: 'AS', + }, + { + label: 'Andorra', + value: 'AD', + }, + { + label: 'Angola', + value: 'AO', + }, + { + label: 'Anguilla', + value: 'AI', + }, + { + label: 'Antarctica', + value: 'AQ', + }, + { + label: 'Antigua and Barbuda', + value: 'AG', + }, + { + label: 'Argentina', + value: 'AR', + }, + { + label: 'Armenia', + value: 'AM', + }, + { + label: 'Aruba', + value: 'AW', + }, + { + label: 'Australia', + value: 'AU', + }, + { + label: 'Austria', + value: 'AT', + }, + { + label: 'Azerbaijan', + value: 'AZ', + }, + { + label: 'Bahamas', + value: 'BS', + }, + { + label: 'Bahrain', + value: 'BH', + }, + { + label: 'Bangladesh', + value: 'BD', + }, + { + label: 'Barbados', + value: 'BB', + }, + { + label: 'Belarus', + value: 'BY', + }, + { + label: 'Belgium', + value: 'BE', + }, + { + label: 'Belize', + value: 'BZ', + }, + { + label: 'Benin', + value: 'BJ', + }, + { + label: 'Bermuda', + value: 'BM', + }, + { + label: 'Bhutan', + value: 'BT', + }, + { + label: 'Bolivia', + value: 'BO', + }, + { + label: 'Bosnia and Herzegovina', + value: 'BA', + }, + { + label: 'Botswana', + value: 'BW', + }, + { + label: 'Bouvet Island', + value: 'BV', + }, + { + label: 'Brazil', + value: 'BR', + }, + { + label: 'British Indian Ocean Territory', + value: 'IO', + }, + { + label: 'Brunei Darussalam', + value: 'BN', + }, + { + label: 'Bulgaria', + value: 'BG', + }, + { + label: 'Burkina Faso', + value: 'BF', + }, + { + label: 'Burundi', + value: 'BI', + }, + { + label: 'Cambodia', + value: 'KH', + }, + { + label: 'Cameroon', + value: 'CM', + }, + { + label: 'Canada', + value: 'CA', + }, + { + label: 'Cape Verde', + value: 'CV', + }, + { + label: 'Cayman Islands', + value: 'KY', + }, + { + label: 'Central African Republic', + value: 'CF', + }, + { + label: 'Chad', + value: 'TD', + }, + { + label: 'Chile', + value: 'CL', + }, + { + label: 'China', + value: 'CN', + }, + { + label: 'Christmas Island', + value: 'CX', + }, + { + label: 'Cocos (Keeling) Islands', + value: 'CC', + }, + { + label: 'Colombia', + value: 'CO', + }, + { + label: 'Comoros', + value: 'KM', + }, + { + label: 'Congo', + value: 'CG', + }, + { + label: 'Congo, The Democratic Republic of the', + value: 'CD', + }, + { + label: 'Cook Islands', + value: 'CK', + }, + { + label: 'Costa Rica', + value: 'CR', + }, + { + label: "Cote D'Ivoire", + value: 'CI', + }, + { + label: 'Croatia', + value: 'HR', + }, + { + label: 'Cuba', + value: 'CU', + }, + { + label: 'Cyprus', + value: 'CY', + }, + { + label: 'Czech Republic', + value: 'CZ', + }, + { + label: 'Denmark', + value: 'DK', + }, + { + label: 'Djibouti', + value: 'DJ', + }, + { + label: 'Dominica', + value: 'DM', + }, + { + label: 'Dominican Republic', + value: 'DO', + }, + { + label: 'Ecuador', + value: 'EC', + }, + { + label: 'Egypt', + value: 'EG', + }, + { + label: 'El Salvador', + value: 'SV', + }, + { + label: 'Equatorial Guinea', + value: 'GQ', + }, + { + label: 'Eritrea', + value: 'ER', + }, + { + label: 'Estonia', + value: 'EE', + }, + { + label: 'Ethiopia', + value: 'ET', + }, + { + label: 'Falkland Islands (Malvinas)', + value: 'FK', + }, + { + label: 'Faroe Islands', + value: 'FO', + }, + { + label: 'Fiji', + value: 'FJ', + }, + { + label: 'Finland', + value: 'FI', + }, + { + label: 'France', + value: 'FR', + }, + { + label: 'French Guiana', + value: 'GF', + }, + { + label: 'French Polynesia', + value: 'PF', + }, + { + label: 'French Southern Territories', + value: 'TF', + }, + { + label: 'Gabon', + value: 'GA', + }, + { + label: 'Gambia', + value: 'GM', + }, + { + label: 'Georgia', + value: 'GE', + }, + { + label: 'Germany', + value: 'DE', + }, + { + label: 'Ghana', + value: 'GH', + }, + { + label: 'Gibraltar', + value: 'GI', + }, + { + label: 'Greece', + value: 'GR', + }, + { + label: 'Greenland', + value: 'GL', + }, + { + label: 'Grenada', + value: 'GD', + }, + { + label: 'Guadeloupe', + value: 'GP', + }, + { + label: 'Guam', + value: 'GU', + }, + { + label: 'Guatemala', + value: 'GT', + }, + { + label: 'Guernsey', + value: 'GG', + }, + { + label: 'Guinea', + value: 'GN', + }, + { + label: 'Guinea-Bissau', + value: 'GW', + }, + { + label: 'Guyana', + value: 'GY', + }, + { + label: 'Haiti', + value: 'HT', + }, + { + label: 'Heard Island and Mcdonald Islands', + value: 'HM', + }, + { + label: 'Holy See (Vatican City State)', + value: 'VA', + }, + { + label: 'Honduras', + value: 'HN', + }, + { + label: 'Hong Kong', + value: 'HK', + }, + { + label: 'Hungary', + value: 'HU', + }, + { + label: 'Iceland', + value: 'IS', + }, + { + label: 'India', + value: 'IN', + }, + { + label: 'Indonesia', + value: 'ID', + }, + { + label: 'Iran, Islamic Republic Of', + value: 'IR', + }, + { + label: 'Iraq', + value: 'IQ', + }, + { + label: 'Ireland', + value: 'IE', + }, + { + label: 'Isle of Man', + value: 'IM', + }, + { + label: 'Israel', + value: 'IL', + }, + { + label: 'Italy', + value: 'IT', + }, + { + label: 'Jamaica', + value: 'JM', + }, + { + label: 'Japan', + value: 'JP', + }, + { + label: 'Jersey', + value: 'JE', + }, + { + label: 'Jordan', + value: 'JO', + }, + { + label: 'Kazakhstan', + value: 'KZ', + }, + { + label: 'Kenya', + value: 'KE', + }, + { + label: 'Kiribati', + value: 'KI', + }, + { + label: "Democratic People's Republic of Korea", + value: 'KP', + }, + { + label: 'Korea, Republic of', + value: 'KR', + }, + { + label: 'Kosovo', + value: 'XK', + }, + { + label: 'Kuwait', + value: 'KW', + }, + { + label: 'Kyrgyzstan', + value: 'KG', + }, + { + label: "Lao People's Democratic Republic", + value: 'LA', + }, + { + label: 'Latvia', + value: 'LV', + }, + { + label: 'Lebanon', + value: 'LB', + }, + { + label: 'Lesotho', + value: 'LS', + }, + { + label: 'Liberia', + value: 'LR', + }, + { + label: 'Libyan Arab Jamahiriya', + value: 'LY', + }, + { + label: 'Liechtenstein', + value: 'LI', + }, + { + label: 'Lithuania', + value: 'LT', + }, + { + label: 'Luxembourg', + value: 'LU', + }, + { + label: 'Macao', + value: 'MO', + }, + { + label: 'Macedonia, The Former Yugoslav Republic of', + value: 'MK', + }, + { + label: 'Madagascar', + value: 'MG', + }, + { + label: 'Malawi', + value: 'MW', + }, + { + label: 'Malaysia', + value: 'MY', + }, + { + label: 'Maldives', + value: 'MV', + }, + { + label: 'Mali', + value: 'ML', + }, + { + label: 'Malta', + value: 'MT', + }, + { + label: 'Marshall Islands', + value: 'MH', + }, + { + label: 'Martinique', + value: 'MQ', + }, + { + label: 'Mauritania', + value: 'MR', + }, + { + label: 'Mauritius', + value: 'MU', + }, + { + label: 'Mayotte', + value: 'YT', + }, + { + label: 'Mexico', + value: 'MX', + }, + { + label: 'Micronesia, Federated States of', + value: 'FM', + }, + { + label: 'Moldova, Republic of', + value: 'MD', + }, + { + label: 'Monaco', + value: 'MC', + }, + { + label: 'Mongolia', + value: 'MN', + }, + { + label: 'Montenegro', + value: 'ME', + }, + { + label: 'Montserrat', + value: 'MS', + }, + { + label: 'Morocco', + value: 'MA', + }, + { + label: 'Mozambique', + value: 'MZ', + }, + { + label: 'Myanmar', + value: 'MM', + }, + { + label: 'Namibia', + value: 'NA', + }, + { + label: 'Nauru', + value: 'NR', + }, + { + label: 'Nepal', + value: 'NP', + }, + { + label: 'Netherlands', + value: 'NL', + }, + { + label: 'Netherlands Antilles', + value: 'AN', + }, + { + label: 'New Caledonia', + value: 'NC', + }, + { + label: 'New Zealand', + value: 'NZ', + }, + { + label: 'Nicaragua', + value: 'NI', + }, + { + label: 'Niger', + value: 'NE', + }, + { + label: 'Nigeria', + value: 'NG', + }, + { + label: 'Niue', + value: 'NU', + }, + { + label: 'Norfolk Island', + value: 'NF', + }, + { + label: 'Northern Mariana Islands', + value: 'MP', + }, + { + label: 'Norway', + value: 'NO', + }, + { + label: 'Oman', + value: 'OM', + }, + { + label: 'Pakistan', + value: 'PK', + }, + { + label: 'Palau', + value: 'PW', + }, + { + label: 'Palestinian Territory, Occupied', + value: 'PS', + }, + { + label: 'Panama', + value: 'PA', + }, + { + label: 'Papua New Guinea', + value: 'PG', + }, + { + label: 'Paraguay', + value: 'PY', + }, + { + label: 'Peru', + value: 'PE', + }, + { + label: 'Philippines', + value: 'PH', + }, + { + label: 'Pitcairn', + value: 'PN', + }, + { + label: 'Poland', + value: 'PL', + }, + { + label: 'Portugal', + value: 'PT', + }, + { + label: 'Puerto Rico', + value: 'PR', + }, + { + label: 'Qatar', + value: 'QA', + }, + { + label: 'Reunion', + value: 'RE', + }, + { + label: 'Romania', + value: 'RO', + }, + { + label: 'Russian Federation', + value: 'RU', + }, + { + label: 'Rwanda', + value: 'RW', + }, + { + label: 'Saint Helena', + value: 'SH', + }, + { + label: 'Saint Kitts and Nevis', + value: 'KN', + }, + { + label: 'Saint Lucia', + value: 'LC', + }, + { + label: 'Saint Pierre and Miquelon', + value: 'PM', + }, + { + label: 'Saint Vincent and the Grenadines', + value: 'VC', + }, + { + label: 'Samoa', + value: 'WS', + }, + { + label: 'San Marino', + value: 'SM', + }, + { + label: 'Sao Tome and Principe', + value: 'ST', + }, + { + label: 'Saudi Arabia', + value: 'SA', + }, + { + label: 'Senegal', + value: 'SN', + }, + { + label: 'Serbia', + value: 'RS', + }, + { + label: 'Seychelles', + value: 'SC', + }, + { + label: 'Sierra Leone', + value: 'SL', + }, + { + label: 'Singapore', + value: 'SG', + }, + { + label: 'Slovakia', + value: 'SK', + }, + { + label: 'Slovenia', + value: 'SI', + }, + { + label: 'Solomon Islands', + value: 'SB', + }, + { + label: 'Somalia', + value: 'SO', + }, + { + label: 'South Africa', + value: 'ZA', + }, + { + label: 'South Georgia and the South Sandwich Islands', + value: 'GS', + }, + { + label: 'Spain', + value: 'ES', + }, + { + label: 'Sri Lanka', + value: 'LK', + }, + { + label: 'Sudan', + value: 'SD', + }, + { + label: 'Suriname', + value: 'SR', + }, + { + label: 'Svalbard and Jan Mayen', + value: 'SJ', + }, + { + label: 'Swaziland', + value: 'SZ', + }, + { + label: 'Sweden', + value: 'SE', + }, + { + label: 'Switzerland', + value: 'CH', + }, + { + label: 'Syrian Arab Republic', + value: 'SY', + }, + { + label: 'Taiwan', + value: 'TW', + }, + { + label: 'Tajikistan', + value: 'TJ', + }, + { + label: 'Tanzania, United Republic of', + value: 'TZ', + }, + { + label: 'Thailand', + value: 'TH', + }, + { + label: 'Timor-Leste', + value: 'TL', + }, + { + label: 'Togo', + value: 'TG', + }, + { + label: 'Tokelau', + value: 'TK', + }, + { + label: 'Tonga', + value: 'TO', + }, + { + label: 'Trinidad and Tobago', + value: 'TT', + }, + { + label: 'Tunisia', + value: 'TN', + }, + { + label: 'Turkey', + value: 'TR', + }, + { + label: 'Turkmenistan', + value: 'TM', + }, + { + label: 'Turks and Caicos Islands', + value: 'TC', + }, + { + label: 'Tuvalu', + value: 'TV', + }, + { + label: 'Uganda', + value: 'UG', + }, + { + label: 'Ukraine', + value: 'UA', + }, + { + label: 'United Arab Emirates', + value: 'AE', + }, + { + label: 'United Kingdom', + value: 'GB', + }, + { + label: 'United States', + value: 'US', + }, + { + label: 'United States Minor Outlying Islands', + value: 'UM', + }, + { + label: 'Uruguay', + value: 'UY', + }, + { + label: 'Uzbekistan', + value: 'UZ', + }, + { + label: 'Vanuatu', + value: 'VU', + }, + { + label: 'Venezuela', + value: 'VE', + }, + { + label: 'Viet Nam', + value: 'VN', + }, + { + label: 'Virgin Islands, British', + value: 'VG', + }, + { + label: 'Virgin Islands, U.S.', + value: 'VI', + }, + { + label: 'Wallis and Futuna', + value: 'WF', + }, + { + label: 'Western Sahara', + value: 'EH', + }, + { + label: 'Yemen', + value: 'YE', + }, + { + label: 'Zambia', + value: 'ZM', + }, + { + label: 'Zimbabwe', + value: 'ZW', + }, +] diff --git a/apps/cms/src/blocks/Form/Email/index.tsx b/apps/cms/src/blocks/Form/Email/index.tsx new file mode 100644 index 00000000000..fc9fd2804cd --- /dev/null +++ b/apps/cms/src/blocks/Form/Email/index.tsx @@ -0,0 +1,38 @@ +import type { EmailField } from '@payloadcms/plugin-form-builder/types' +import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form' + +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import React from 'react' + +import { Error } from '../Error' +import { Width } from '../Width' + +export const Email: React.FC< + EmailField & { + errors: Partial + register: UseFormRegister + } +> = ({ name, defaultValue, errors, label, register, required, width }) => { + return ( + + + + + {errors[name] && } + + ) +} diff --git a/apps/cms/src/blocks/Form/Error/index.tsx b/apps/cms/src/blocks/Form/Error/index.tsx new file mode 100644 index 00000000000..a7b9e47e805 --- /dev/null +++ b/apps/cms/src/blocks/Form/Error/index.tsx @@ -0,0 +1,15 @@ +'use client' + +import * as React from 'react' +import { useFormContext } from 'react-hook-form' + +export const Error = ({ name }: { name: string }) => { + const { + formState: { errors }, + } = useFormContext() + return ( +
+ {(errors[name]?.message as string) || 'This field is required'} +
+ ) +} diff --git a/apps/cms/src/blocks/Form/Message/index.tsx b/apps/cms/src/blocks/Form/Message/index.tsx new file mode 100644 index 00000000000..5924cf95f40 --- /dev/null +++ b/apps/cms/src/blocks/Form/Message/index.tsx @@ -0,0 +1,13 @@ +import RichText from '@/components/RichText' +import React from 'react' + +import { Width } from '../Width' +import { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' + +export const Message: React.FC<{ message: SerializedEditorState }> = ({ message }) => { + return ( + + {message && } + + ) +} diff --git a/apps/cms/src/blocks/Form/Number/index.tsx b/apps/cms/src/blocks/Form/Number/index.tsx new file mode 100644 index 00000000000..f26e54a4486 --- /dev/null +++ b/apps/cms/src/blocks/Form/Number/index.tsx @@ -0,0 +1,36 @@ +import type { TextField } from '@payloadcms/plugin-form-builder/types' +import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form' + +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import React from 'react' + +import { Error } from '../Error' +import { Width } from '../Width' +export const Number: React.FC< + TextField & { + errors: Partial + register: UseFormRegister + } +> = ({ name, defaultValue, errors, label, register, required, width }) => { + return ( + + + + {errors[name] && } + + ) +} diff --git a/apps/cms/src/blocks/Form/Select/index.tsx b/apps/cms/src/blocks/Form/Select/index.tsx new file mode 100644 index 00000000000..30c0e8312ac --- /dev/null +++ b/apps/cms/src/blocks/Form/Select/index.tsx @@ -0,0 +1,63 @@ +import type { SelectField } from '@payloadcms/plugin-form-builder/types' +import type { Control, FieldErrorsImpl } from 'react-hook-form' + +import { Label } from '@/components/ui/label' +import { + Select as SelectComponent, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import React from 'react' +import { Controller } from 'react-hook-form' + +import { Error } from '../Error' +import { Width } from '../Width' + +export const Select: React.FC< + SelectField & { + control: Control + errors: Partial + } +> = ({ name, control, errors, label, options, required, width, defaultValue }) => { + return ( + + + { + const controlledValue = options.find((t) => t.value === value) + + return ( + onChange(val)} value={controlledValue?.value}> + + + + + {options.map(({ label, value }) => { + return ( + + {label} + + ) + })} + + + ) + }} + rules={{ required }} + /> + {errors[name] && } + + ) +} diff --git a/apps/cms/src/blocks/Form/State/index.tsx b/apps/cms/src/blocks/Form/State/index.tsx new file mode 100644 index 00000000000..29e49cae0c3 --- /dev/null +++ b/apps/cms/src/blocks/Form/State/index.tsx @@ -0,0 +1,64 @@ +import type { StateField } from '@payloadcms/plugin-form-builder/types' +import type { Control, FieldErrorsImpl } from 'react-hook-form' + +import { Label } from '@/components/ui/label' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import React from 'react' +import { Controller } from 'react-hook-form' + +import { Error } from '../Error' +import { Width } from '../Width' +import { stateOptions } from './options' + +export const State: React.FC< + StateField & { + control: Control + errors: Partial + } +> = ({ name, control, errors, label, required, width }) => { + return ( + + + { + const controlledValue = stateOptions.find((t) => t.value === value) + + return ( + + ) + }} + rules={{ required }} + /> + {errors[name] && } + + ) +} diff --git a/apps/cms/src/blocks/Form/State/options.ts b/apps/cms/src/blocks/Form/State/options.ts new file mode 100644 index 00000000000..8dff991e7ab --- /dev/null +++ b/apps/cms/src/blocks/Form/State/options.ts @@ -0,0 +1,52 @@ +export const stateOptions = [ + { label: 'Alabama', value: 'AL' }, + { label: 'Alaska', value: 'AK' }, + { label: 'Arizona', value: 'AZ' }, + { label: 'Arkansas', value: 'AR' }, + { label: 'California', value: 'CA' }, + { label: 'Colorado', value: 'CO' }, + { label: 'Connecticut', value: 'CT' }, + { label: 'Delaware', value: 'DE' }, + { label: 'Florida', value: 'FL' }, + { label: 'Georgia', value: 'GA' }, + { label: 'Hawaii', value: 'HI' }, + { label: 'Idaho', value: 'ID' }, + { label: 'Illinois', value: 'IL' }, + { label: 'Indiana', value: 'IN' }, + { label: 'Iowa', value: 'IA' }, + { label: 'Kansas', value: 'KS' }, + { label: 'Kentucky', value: 'KY' }, + { label: 'Louisiana', value: 'LA' }, + { label: 'Maine', value: 'ME' }, + { label: 'Maryland', value: 'MD' }, + { label: 'Massachusetts', value: 'MA' }, + { label: 'Michigan', value: 'MI' }, + { label: 'Minnesota', value: 'MN' }, + { label: 'Mississippi', value: 'MS' }, + { label: 'Missouri', value: 'MO' }, + { label: 'Montana', value: 'MT' }, + { label: 'Nebraska', value: 'NE' }, + { label: 'Nevada', value: 'NV' }, + { label: 'New Hampshire', value: 'NH' }, + { label: 'New Jersey', value: 'NJ' }, + { label: 'New Mexico', value: 'NM' }, + { label: 'New York', value: 'NY' }, + { label: 'North Carolina', value: 'NC' }, + { label: 'North Dakota', value: 'ND' }, + { label: 'Ohio', value: 'OH' }, + { label: 'Oklahoma', value: 'OK' }, + { label: 'Oregon', value: 'OR' }, + { label: 'Pennsylvania', value: 'PA' }, + { label: 'Rhode Island', value: 'RI' }, + { label: 'South Carolina', value: 'SC' }, + { label: 'South Dakota', value: 'SD' }, + { label: 'Tennessee', value: 'TN' }, + { label: 'Texas', value: 'TX' }, + { label: 'Utah', value: 'UT' }, + { label: 'Vermont', value: 'VT' }, + { label: 'Virginia', value: 'VA' }, + { label: 'Washington', value: 'WA' }, + { label: 'West Virginia', value: 'WV' }, + { label: 'Wisconsin', value: 'WI' }, + { label: 'Wyoming', value: 'WY' }, +] diff --git a/apps/cms/src/blocks/Form/Text/index.tsx b/apps/cms/src/blocks/Form/Text/index.tsx new file mode 100644 index 00000000000..be1e0ff12bd --- /dev/null +++ b/apps/cms/src/blocks/Form/Text/index.tsx @@ -0,0 +1,32 @@ +import type { TextField } from '@payloadcms/plugin-form-builder/types' +import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form' + +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import React from 'react' + +import { Error } from '../Error' +import { Width } from '../Width' + +export const Text: React.FC< + TextField & { + errors: Partial + register: UseFormRegister + } +> = ({ name, defaultValue, errors, label, register, required, width }) => { + return ( + + + + {errors[name] && } + + ) +} diff --git a/apps/cms/src/blocks/Form/Textarea/index.tsx b/apps/cms/src/blocks/Form/Textarea/index.tsx new file mode 100644 index 00000000000..ecb6e21afdb --- /dev/null +++ b/apps/cms/src/blocks/Form/Textarea/index.tsx @@ -0,0 +1,40 @@ +import type { TextField } from '@payloadcms/plugin-form-builder/types' +import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form' + +import { Label } from '@/components/ui/label' +import { Textarea as TextAreaComponent } from '@/components/ui/textarea' +import React from 'react' + +import { Error } from '../Error' +import { Width } from '../Width' + +export const Textarea: React.FC< + TextField & { + errors: Partial + register: UseFormRegister + rows?: number + } +> = ({ name, defaultValue, errors, label, register, required, rows = 3, width }) => { + return ( + + + + + + {errors[name] && } + + ) +} diff --git a/apps/cms/src/blocks/Form/Width/index.tsx b/apps/cms/src/blocks/Form/Width/index.tsx new file mode 100644 index 00000000000..bcc51a33333 --- /dev/null +++ b/apps/cms/src/blocks/Form/Width/index.tsx @@ -0,0 +1,13 @@ +import * as React from 'react' + +export const Width: React.FC<{ + children: React.ReactNode + className?: string + width?: number | string +}> = ({ children, className, width }) => { + return ( +
+ {children} +
+ ) +} diff --git a/apps/cms/src/blocks/Form/config.ts b/apps/cms/src/blocks/Form/config.ts new file mode 100644 index 00000000000..5334289f3a0 --- /dev/null +++ b/apps/cms/src/blocks/Form/config.ts @@ -0,0 +1,51 @@ +import type { Block } from 'payload' + +import { + FixedToolbarFeature, + HeadingFeature, + InlineToolbarFeature, + lexicalEditor, +} from '@payloadcms/richtext-lexical' + +export const FormBlock: Block = { + slug: 'formBlock', + interfaceName: 'FormBlock', + fields: [ + { + name: 'form', + type: 'relationship', + relationTo: 'forms', + required: true, + }, + { + name: 'enableIntro', + type: 'checkbox', + label: 'Enable Intro Content', + }, + { + name: 'introContent', + type: 'richText', + admin: { + condition: (_, { enableIntro }) => Boolean(enableIntro), + }, + editor: lexicalEditor({ + features: ({ rootFeatures }) => { + return [ + ...rootFeatures, + HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }), + FixedToolbarFeature(), + InlineToolbarFeature(), + ] + }, + }), + label: 'Intro Content', + }, + ], + graphQL: { + singularName: 'FormBlock', + }, + labels: { + plural: 'Form Blocks', + singular: 'Form Block', + }, +} diff --git a/apps/cms/src/blocks/Form/fields.tsx b/apps/cms/src/blocks/Form/fields.tsx new file mode 100644 index 00000000000..fa660f7e398 --- /dev/null +++ b/apps/cms/src/blocks/Form/fields.tsx @@ -0,0 +1,21 @@ +import { Checkbox } from './Checkbox' +import { Country } from './Country' +import { Email } from './Email' +import { Message } from './Message' +import { Number } from './Number' +import { Select } from './Select' +import { State } from './State' +import { Text } from './Text' +import { Textarea } from './Textarea' + +export const fields = { + checkbox: Checkbox, + country: Country, + email: Email, + message: Message, + number: Number, + select: Select, + state: State, + text: Text, + textarea: Textarea, +} diff --git a/apps/cms/src/blocks/MediaBlock/Component.tsx b/apps/cms/src/blocks/MediaBlock/Component.tsx new file mode 100644 index 00000000000..a88896aa9d6 --- /dev/null +++ b/apps/cms/src/blocks/MediaBlock/Component.tsx @@ -0,0 +1,67 @@ +import type { StaticImageData } from 'next/image' + +import { cn } from '@/utilities/ui' +import React from 'react' +import RichText from '@/components/RichText' + +import type { MediaBlock as MediaBlockProps } from '@/payload-types' + +import { Media } from '../../components/Media' + +type Props = MediaBlockProps & { + breakout?: boolean + captionClassName?: string + className?: string + enableGutter?: boolean + imgClassName?: string + staticImage?: StaticImageData + disableInnerContainer?: boolean +} + +export const MediaBlock: React.FC = (props) => { + const { + captionClassName, + className, + enableGutter = true, + imgClassName, + media, + staticImage, + disableInnerContainer, + } = props + + let caption + if (media && typeof media === 'object') caption = media.caption + + return ( +
+ {(media || staticImage) && ( + + )} + {caption && ( +
+ +
+ )} +
+ ) +} diff --git a/apps/cms/src/blocks/MediaBlock/config.ts b/apps/cms/src/blocks/MediaBlock/config.ts new file mode 100644 index 00000000000..7beb79b7eeb --- /dev/null +++ b/apps/cms/src/blocks/MediaBlock/config.ts @@ -0,0 +1,14 @@ +import type { Block } from 'payload' + +export const MediaBlock: Block = { + slug: 'mediaBlock', + interfaceName: 'MediaBlock', + fields: [ + { + name: 'media', + type: 'upload', + relationTo: 'media', + required: true, + }, + ], +} diff --git a/apps/cms/src/blocks/RelatedPosts/Component.tsx b/apps/cms/src/blocks/RelatedPosts/Component.tsx new file mode 100644 index 00000000000..8d8c9798b20 --- /dev/null +++ b/apps/cms/src/blocks/RelatedPosts/Component.tsx @@ -0,0 +1,32 @@ +import clsx from 'clsx' +import React from 'react' +import RichText from '@/components/RichText' + +import type { Post } from '@/payload-types' + +import { Card } from '../../components/Card' +import { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' + +export type RelatedPostsProps = { + className?: string + docs?: Post[] + introContent?: SerializedEditorState +} + +export const RelatedPosts: React.FC = (props) => { + const { className, docs, introContent } = props + + return ( +
+ {introContent && } + +
+ {docs?.map((doc, index) => { + if (typeof doc === 'string') return null + + return + })} +
+
+ ) +} diff --git a/apps/cms/src/blocks/RenderBlocks.tsx b/apps/cms/src/blocks/RenderBlocks.tsx new file mode 100644 index 00000000000..c84634a64ad --- /dev/null +++ b/apps/cms/src/blocks/RenderBlocks.tsx @@ -0,0 +1,51 @@ +import React, { Fragment } from 'react' + +import type { Page } from '@/payload-types' + +import { ArchiveBlock } from '@/blocks/ArchiveBlock/Component' +import { CallToActionBlock } from '@/blocks/CallToAction/Component' +import { ContentBlock } from '@/blocks/Content/Component' +import { FormBlock } from '@/blocks/Form/Component' +import { MediaBlock } from '@/blocks/MediaBlock/Component' + +const blockComponents = { + archive: ArchiveBlock, + content: ContentBlock, + cta: CallToActionBlock, + formBlock: FormBlock, + mediaBlock: MediaBlock, +} + +export const RenderBlocks: React.FC<{ + blocks: Page['layout'][0][] +}> = (props) => { + const { blocks } = props + + const hasBlocks = blocks && Array.isArray(blocks) && blocks.length > 0 + + if (hasBlocks) { + return ( + + {blocks.map((block, index) => { + const { blockType } = block + + if (blockType && blockType in blockComponents) { + const Block = blockComponents[blockType] + + if (Block) { + return ( +
+ {/* @ts-expect-error there may be some mismatch between the expected types here */} + +
+ ) + } + } + return null + })} +
+ ) + } + + return null +} diff --git a/apps/cms-payload/src/collections/Author.ts b/apps/cms/src/collections/Authors.ts similarity index 90% rename from apps/cms-payload/src/collections/Author.ts rename to apps/cms/src/collections/Authors.ts index 52708e4ed49..f9ba90abc79 100644 --- a/apps/cms-payload/src/collections/Author.ts +++ b/apps/cms/src/collections/Authors.ts @@ -1,6 +1,6 @@ import { CollectionConfig } from 'payload' -const Author: CollectionConfig = { +export const Authors: CollectionConfig = { slug: 'authors', admin: { useAsTitle: 'author', @@ -40,4 +40,4 @@ const Author: CollectionConfig = { timestamps: true, } -export default Author +export default Authors diff --git a/apps/cms-payload/src/collections/Category.ts b/apps/cms/src/collections/Categories.ts similarity index 78% rename from apps/cms-payload/src/collections/Category.ts rename to apps/cms/src/collections/Categories.ts index 5d49f5ca230..6203fb9bed4 100644 --- a/apps/cms-payload/src/collections/Category.ts +++ b/apps/cms/src/collections/Categories.ts @@ -1,6 +1,6 @@ import { CollectionConfig } from 'payload' -const Category: CollectionConfig = { +export const Categories: CollectionConfig = { slug: 'categories', admin: { useAsTitle: 'name', @@ -18,4 +18,4 @@ const Category: CollectionConfig = { timestamps: true, } -export default Category +export default Categories diff --git a/apps/cms/src/collections/Media.ts b/apps/cms/src/collections/Media.ts new file mode 100644 index 00000000000..a9e152d38d4 --- /dev/null +++ b/apps/cms/src/collections/Media.ts @@ -0,0 +1,74 @@ +import type { CollectionConfig } from 'payload' +import { + FixedToolbarFeature, + InlineToolbarFeature, + lexicalEditor, +} from '@payloadcms/richtext-lexical' +import path from 'path' +import { fileURLToPath } from 'url' + +import { anyone } from '../access/anyone' +import { authenticated } from '../access/authenticated' + +export const Media: CollectionConfig = { + slug: 'media', + access: { + create: authenticated, + delete: authenticated, + read: anyone, + update: authenticated, + }, + fields: [ + { + name: 'alt', + type: 'text', + // required: true, + }, + { + name: 'caption', + type: 'richText', + editor: lexicalEditor({ + features: ({ rootFeatures }) => { + return [...rootFeatures, FixedToolbarFeature(), InlineToolbarFeature()] + }, + }), + }, + ], + upload: { + adminThumbnail: 'thumbnail', + focalPoint: true, + imageSizes: [ + { + name: 'thumbnail', + width: 300, + }, + { + name: 'square', + width: 500, + height: 500, + }, + { + name: 'small', + width: 600, + }, + { + name: 'medium', + width: 900, + }, + { + name: 'large', + width: 1400, + }, + { + name: 'xlarge', + width: 1920, + }, + { + name: 'og', + width: 1200, + height: 630, + crop: 'center', + }, + ], + }, +} diff --git a/apps/cms/src/collections/Pages/hooks/revalidatePage.ts b/apps/cms/src/collections/Pages/hooks/revalidatePage.ts new file mode 100644 index 00000000000..d21cec38a92 --- /dev/null +++ b/apps/cms/src/collections/Pages/hooks/revalidatePage.ts @@ -0,0 +1,43 @@ +import type { CollectionAfterChangeHook, CollectionAfterDeleteHook } from 'payload' + +import { revalidatePath, revalidateTag } from 'next/cache' + +import type { Page } from '@/payload-types' + +export const revalidatePage: CollectionAfterChangeHook = ({ + doc, + previousDoc, + req: { payload, context }, +}) => { + if (!context.disableRevalidate) { + if (doc._status === 'published') { + const path = doc.slug === 'home' ? '/' : `/${doc.slug}` + + payload.logger.info(`Revalidating page at path: ${path}`) + + revalidatePath(path) + revalidateTag('pages-sitemap') + } + + // If the page was previously published, we need to revalidate the old path + if (previousDoc?._status === 'published' && doc._status !== 'published') { + const oldPath = previousDoc.slug === 'home' ? '/' : `/${previousDoc.slug}` + + payload.logger.info(`Revalidating old page at path: ${oldPath}`) + + revalidatePath(oldPath) + revalidateTag('pages-sitemap') + } + } + return doc +} + +export const revalidateDelete: CollectionAfterDeleteHook = ({ doc, req: { context } }) => { + if (!context.disableRevalidate) { + const path = doc?.slug === 'home' ? '/' : `/${doc?.slug}` + revalidatePath(path) + revalidateTag('pages-sitemap') + } + + return doc +} diff --git a/apps/cms/src/collections/Pages/index.ts b/apps/cms/src/collections/Pages/index.ts new file mode 100644 index 00000000000..c81ffa8815e --- /dev/null +++ b/apps/cms/src/collections/Pages/index.ts @@ -0,0 +1,139 @@ +import type { CollectionConfig } from 'payload' + +import { authenticated } from '../../access/authenticated' +import { authenticatedOrPublished } from '../../access/authenticatedOrPublished' +import { Archive } from '../../blocks/ArchiveBlock/config' +import { CallToAction } from '../../blocks/CallToAction/config' +import { Content } from '../../blocks/Content/config' +import { FormBlock } from '../../blocks/Form/config' +import { MediaBlock } from '../../blocks/MediaBlock/config' +import { hero } from '@/heros/config' +import { slugField } from '@/fields/slug' +import { populatePublishedAt } from '../../hooks/populatePublishedAt' +import { generatePreviewPath } from '../../utilities/generatePreviewPath' +import { revalidateDelete, revalidatePage } from './hooks/revalidatePage' + +import { + MetaDescriptionField, + MetaImageField, + MetaTitleField, + OverviewField, + PreviewField, +} from '@payloadcms/plugin-seo/fields' + +export const Pages: CollectionConfig<'pages'> = { + slug: 'pages', + access: { + create: authenticated, + delete: authenticated, + read: authenticatedOrPublished, + update: authenticated, + }, + // This config controls what's populated by default when a page is referenced + // https://payloadcms.com/docs/queries/select#defaultpopulate-collection-config-property + // Type safe if the collection slug generic is passed to `CollectionConfig` - `CollectionConfig<'pages'> + defaultPopulate: { + title: true, + slug: true, + }, + admin: { + defaultColumns: ['title', 'slug', 'updatedAt'], + livePreview: { + url: ({ data, req }) => { + const path = generatePreviewPath({ + slug: typeof data?.slug === 'string' ? data.slug : '', + collection: 'pages', + req, + }) + + return path + }, + }, + preview: (data, { req }) => + generatePreviewPath({ + slug: typeof data?.slug === 'string' ? data.slug : '', + collection: 'pages', + req, + }), + useAsTitle: 'title', + }, + fields: [ + { + name: 'title', + type: 'text', + required: true, + }, + { + type: 'tabs', + tabs: [ + { + fields: [hero], + label: 'Hero', + }, + { + fields: [ + { + name: 'layout', + type: 'blocks', + blocks: [CallToAction, Content, MediaBlock, Archive, FormBlock], + required: true, + admin: { + initCollapsed: true, + }, + }, + ], + label: 'Content', + }, + { + name: 'meta', + label: 'SEO', + fields: [ + OverviewField({ + titlePath: 'meta.title', + descriptionPath: 'meta.description', + imagePath: 'meta.image', + }), + MetaTitleField({ + hasGenerateFn: true, + }), + MetaImageField({ + relationTo: 'media', + }), + + MetaDescriptionField({}), + PreviewField({ + // if the `generateUrl` function is configured + hasGenerateFn: true, + + // field paths to match the target field for data + titlePath: 'meta.title', + descriptionPath: 'meta.description', + }), + ], + }, + ], + }, + { + name: 'publishedAt', + type: 'date', + admin: { + position: 'sidebar', + }, + }, + ...slugField(), + ], + hooks: { + afterChange: [revalidatePage], + beforeChange: [populatePublishedAt], + afterDelete: [revalidateDelete], + }, + versions: { + drafts: { + autosave: { + interval: 100, // We set this interval for optimal live preview + }, + schedulePublish: true, + }, + maxPerDoc: 50, + }, +} diff --git a/apps/cms/src/collections/Posts/hooks/populateAuthors.ts b/apps/cms/src/collections/Posts/hooks/populateAuthors.ts new file mode 100644 index 00000000000..40b4fe5b4fa --- /dev/null +++ b/apps/cms/src/collections/Posts/hooks/populateAuthors.ts @@ -0,0 +1,37 @@ +import type { CollectionAfterReadHook } from 'payload' +import { User } from 'src/payload-types' + +// The `user` collection has access control locked so that users are not publicly accessible +// This means that we need to populate the authors manually here to protect user privacy +// GraphQL will not return mutated user data that differs from the underlying schema +// So we use an alternative `populatedAuthors` field to populate the user data, hidden from the admin UI +export const populateAuthors: CollectionAfterReadHook = async ({ doc, req, req: { payload } }) => { + if (doc?.authors && doc?.authors?.length > 0) { + const authorDocs: User[] = [] + + for (const author of doc.authors) { + try { + const authorDoc = await payload.findByID({ + id: typeof author === 'object' ? author?.id : author, + collection: 'users', + depth: 0, + }) + + if (authorDoc) { + authorDocs.push(authorDoc) + } + + if (authorDocs.length > 0) { + doc.populatedAuthors = authorDocs.map((authorDoc) => ({ + id: authorDoc.id, + name: authorDoc.name, + })) + } + } catch { + // swallow error + } + } + } + + return doc +} diff --git a/apps/cms/src/collections/Posts/hooks/revalidatePost.ts b/apps/cms/src/collections/Posts/hooks/revalidatePost.ts new file mode 100644 index 00000000000..ff43752852e --- /dev/null +++ b/apps/cms/src/collections/Posts/hooks/revalidatePost.ts @@ -0,0 +1,44 @@ +import type { CollectionAfterChangeHook, CollectionAfterDeleteHook } from 'payload' + +import { revalidatePath, revalidateTag } from 'next/cache' + +import type { Post } from '../../../payload-types' + +export const revalidatePost: CollectionAfterChangeHook = ({ + doc, + previousDoc, + req: { payload, context }, +}) => { + if (!context.disableRevalidate) { + if (doc._status === 'published') { + const path = `/posts/${doc.slug}` + + payload.logger.info(`Revalidating post at path: ${path}`) + + revalidatePath(path) + revalidateTag('posts-sitemap') + } + + // If the post was previously published, we need to revalidate the old path + if (previousDoc._status === 'published' && doc._status !== 'published') { + const oldPath = `/posts/${previousDoc.slug}` + + payload.logger.info(`Revalidating old post at path: ${oldPath}`) + + revalidatePath(oldPath) + revalidateTag('posts-sitemap') + } + } + return doc +} + +export const revalidateDelete: CollectionAfterDeleteHook = ({ doc, req: { context } }) => { + if (!context.disableRevalidate) { + const path = `/posts/${doc?.slug}` + + revalidatePath(path) + revalidateTag('posts-sitemap') + } + + return doc +} diff --git a/apps/cms/src/collections/Posts/index.ts b/apps/cms/src/collections/Posts/index.ts new file mode 100644 index 00000000000..39f68813644 --- /dev/null +++ b/apps/cms/src/collections/Posts/index.ts @@ -0,0 +1,277 @@ +import type { CollectionConfig } from 'payload' + +import { + BlocksFeature, + FixedToolbarFeature, + HeadingFeature, + HorizontalRuleFeature, + InlineToolbarFeature, + lexicalEditor, +} from '@payloadcms/richtext-lexical' + +import { authenticated } from '../../access/authenticated' +import { authenticatedOrPublished } from '../../access/authenticatedOrPublished' +import { Banner } from '../../blocks/Banner/config' +import { Code } from '../../blocks/Code/config' +import { MediaBlock } from '../../blocks/MediaBlock/config' +import { generatePreviewPath } from '../../utilities/generatePreviewPath' +import { populateAuthors } from './hooks/populateAuthors' +import { revalidateDelete, revalidatePost } from './hooks/revalidatePost' + +import { + MetaDescriptionField, + MetaImageField, + MetaTitleField, + OverviewField, + PreviewField, +} from '@payloadcms/plugin-seo/fields' +import { slugField } from '@/fields/slug' + +export const Posts: CollectionConfig = { + slug: 'posts', + admin: { + useAsTitle: 'title', + defaultColumns: ['title', 'slug', 'updatedAt'], + livePreview: { + url: ({ data, req }) => { + const path = generatePreviewPath({ + slug: typeof data?.slug === 'string' ? data.slug : '', + collection: 'posts', + req, + }) + + return path + }, + }, + preview: (data, { req }) => + generatePreviewPath({ + slug: typeof data?.slug === 'string' ? data.slug : '', + collection: 'posts', + req, + }), + }, + access: { + create: authenticated, + delete: authenticated, + read: authenticatedOrPublished, + update: authenticated, + }, + defaultPopulate: { + title: true, + slug: true, + categories: true, + meta: { + image: true, + description: true, + }, + }, + fields: [ + { + name: 'title', + type: 'text', + required: true, + }, + // { + // name: 'slug', + // type: 'text', + // required: true, + // unique: true, + // admin: { + // position: 'sidebar', + // }, + // validate: (value: string | string[] | null | undefined) => { + // const regex = /^[a-z][a-z0-9-]{2,49}$/ + // if (!regex.test(value as string)) { + // return 'Slug must start with a letter, contain only lowercase letters, numbers, and hyphens, and be between 3-50 characters' + // } + // return true + // }, + // }, + ...slugField(), + { + type: 'tabs', + tabs: [ + { + fields: [ + { + name: 'content', + type: 'richText', + editor: lexicalEditor({ + features: ({ rootFeatures }) => { + return [ + ...rootFeatures, + HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }), + BlocksFeature({ blocks: [Banner, Code, MediaBlock] }), + FixedToolbarFeature(), + InlineToolbarFeature(), + HorizontalRuleFeature(), + ] + }, + }), + label: false, + required: true, + }, + ], + label: 'Content', + }, + { + fields: [ + { + name: 'thumb', + type: 'upload', + relationTo: 'media', + required: false, + }, + { + name: 'image', + type: 'upload', + relationTo: 'media', + required: false, + }, + { + name: 'categories', + type: 'relationship', + admin: { + position: 'sidebar', + }, + hasMany: true, + relationTo: 'categories', + }, + { + name: 'launchweek', + type: 'text', + admin: { + position: 'sidebar', + }, + }, + { + name: 'readingTime', + type: 'number', + admin: { + hidden: true, + }, + }, + { + name: 'date', + type: 'date', + admin: { + position: 'sidebar', + }, + }, + { + name: 'toc_depth', + type: 'number', + defaultValue: 2, + admin: { + position: 'sidebar', + }, + }, + { + name: 'description', + type: 'textarea', + }, + { + name: 'authors', + type: 'relationship', + relationTo: 'authors', + hasMany: true, + admin: { + position: 'sidebar', + }, + }, + { + name: 'tags', + type: 'relationship', + relationTo: 'tags', + hasMany: true, + admin: { + position: 'sidebar', + }, + }, + // { + // name: 'relatedPosts', + // type: 'relationship', + // admin: { + // position: 'sidebar', + // }, + // filterOptions: ({ id }) => { + // return { + // id: { + // not_in: [id], + // }, + // } + // }, + // hasMany: true, + // relationTo: 'posts', + // }, + ], + label: 'Meta', + }, + { + name: 'meta', + label: 'SEO', + fields: [ + OverviewField({ + titlePath: 'meta.title', + descriptionPath: 'meta.description', + imagePath: 'meta.image', + }), + MetaTitleField({ + hasGenerateFn: true, + }), + MetaImageField({ + relationTo: 'media', + }), + + MetaDescriptionField({}), + PreviewField({ + // if the `generateUrl` function is configured + hasGenerateFn: true, + + // field paths to match the target field for data + titlePath: 'meta.title', + descriptionPath: 'meta.description', + }), + ], + }, + ], + }, + { + name: 'publishedAt', + type: 'date', + admin: { + date: { + pickerAppearance: 'dayAndTime', + }, + position: 'sidebar', + }, + hooks: { + beforeChange: [ + ({ siblingData, value }) => { + if (siblingData._status === 'published' && !value) { + return new Date() + } + return value + }, + ], + }, + }, + ], + timestamps: true, + hooks: { + afterChange: [revalidatePost], + afterRead: [populateAuthors], + afterDelete: [revalidateDelete], + }, + versions: { + drafts: { + autosave: { + interval: 100, // We set this interval for optimal live preview + }, + schedulePublish: true, + }, + maxPerDoc: 50, + }, +} + +export default Posts diff --git a/apps/cms-payload/src/collections/Tag.ts b/apps/cms/src/collections/Tags.ts similarity index 80% rename from apps/cms-payload/src/collections/Tag.ts rename to apps/cms/src/collections/Tags.ts index 252ff5aa188..07bad6cdffc 100644 --- a/apps/cms-payload/src/collections/Tag.ts +++ b/apps/cms/src/collections/Tags.ts @@ -1,6 +1,6 @@ import { CollectionConfig } from 'payload' -const Tag: CollectionConfig = { +export const Tags: CollectionConfig = { slug: 'tags', admin: { useAsTitle: 'name', @@ -18,4 +18,4 @@ const Tag: CollectionConfig = { timestamps: true, } -export default Tag +export default Tags diff --git a/apps/cms-payload/src/collections/Users.ts b/apps/cms/src/collections/Users.ts similarity index 100% rename from apps/cms-payload/src/collections/Users.ts rename to apps/cms/src/collections/Users.ts diff --git a/apps/cms/src/components/AdminBar/index.scss b/apps/cms/src/components/AdminBar/index.scss new file mode 100644 index 00000000000..54060c5ba48 --- /dev/null +++ b/apps/cms/src/components/AdminBar/index.scss @@ -0,0 +1,7 @@ +@import '~@payloadcms/ui/scss'; + +.admin-bar { + @include small-break { + display: none; + } +} diff --git a/apps/cms/src/components/AdminBar/index.tsx b/apps/cms/src/components/AdminBar/index.tsx new file mode 100644 index 00000000000..08c5c6c2e9d --- /dev/null +++ b/apps/cms/src/components/AdminBar/index.tsx @@ -0,0 +1,89 @@ +'use client' + +import type { PayloadAdminBarProps, PayloadMeUser } from '@payloadcms/admin-bar' + +import { cn } from '@/utilities/ui' +import { useSelectedLayoutSegments } from 'next/navigation' +import { PayloadAdminBar } from '@payloadcms/admin-bar' +import React, { useState } from 'react' +import { useRouter } from 'next/navigation' + +import './index.scss' + +import { getClientSideURL } from '@/utilities/getURL' + +const baseClass = 'admin-bar' + +const collectionLabels = { + pages: { + plural: 'Pages', + singular: 'Page', + }, + posts: { + plural: 'Posts', + singular: 'Post', + }, + projects: { + plural: 'Projects', + singular: 'Project', + }, +} + +const Title: React.FC = () => Dashboard + +export const AdminBar: React.FC<{ + adminBarProps?: PayloadAdminBarProps +}> = (props) => { + const { adminBarProps } = props || {} + const segments = useSelectedLayoutSegments() + const [show, setShow] = useState(false) + const collection = ( + collectionLabels[segments?.[1] as keyof typeof collectionLabels] ? segments[1] : 'pages' + ) as keyof typeof collectionLabels + const router = useRouter() + + const onAuthChange = React.useCallback((user: PayloadMeUser) => { + setShow(Boolean(user?.id)) + }, []) + + return ( +
+
+ } + onAuthChange={onAuthChange} + onPreviewExit={() => { + fetch('/next/exit-preview').then(() => { + router.push('/') + router.refresh() + }) + }} + style={{ + backgroundColor: 'transparent', + padding: 0, + position: 'relative', + zIndex: 'unset', + }} + /> +
+
+ ) +} diff --git a/apps/cms/src/components/BeforeDashboard/SeedButton/index.scss b/apps/cms/src/components/BeforeDashboard/SeedButton/index.scss new file mode 100644 index 00000000000..3ed9bcb1091 --- /dev/null +++ b/apps/cms/src/components/BeforeDashboard/SeedButton/index.scss @@ -0,0 +1,12 @@ +.seedButton { + appearance: none; + background: none; + border: none; + padding: 0; + text-decoration: underline; + + &:hover { + cursor: pointer; + opacity: 0.85; + } +} diff --git a/apps/cms/src/components/BeforeDashboard/SeedButton/index.tsx b/apps/cms/src/components/BeforeDashboard/SeedButton/index.tsx new file mode 100644 index 00000000000..87abdf8296f --- /dev/null +++ b/apps/cms/src/components/BeforeDashboard/SeedButton/index.tsx @@ -0,0 +1,88 @@ +'use client' + +import React, { Fragment, useCallback, useState } from 'react' +import { toast } from '@payloadcms/ui' + +import './index.scss' + +const SuccessMessage: React.FC = () => ( +
+ Database seeded! You can now{' '} + + visit your website + +
+) + +export const SeedButton: React.FC = () => { + const [loading, setLoading] = useState(false) + const [seeded, setSeeded] = useState(false) + const [error, setError] = useState(null) + + const handleClick = useCallback( + async (e: React.MouseEvent) => { + e.preventDefault() + + if (seeded) { + toast.info('Database already seeded.') + return + } + if (loading) { + toast.info('Seeding already in progress.') + return + } + if (error) { + toast.error(`An error occurred, please refresh and try again.`) + return + } + + setLoading(true) + + try { + toast.promise( + new Promise((resolve, reject) => { + try { + fetch('/next/seed', { method: 'POST', credentials: 'include' }) + .then((res) => { + if (res.ok) { + resolve(true) + setSeeded(true) + } else { + reject('An error occurred while seeding.') + } + }) + .catch((error) => { + reject(error) + }) + } catch (error) { + reject(error) + } + }), + { + loading: 'Seeding with data....', + success: , + error: 'An error occurred while seeding.', + }, + ) + } catch (err) { + const error = err instanceof Error ? err.message : String(err) + setError(error) + } + }, + [loading, seeded, error], + ) + + let message = '' + if (loading) message = ' (seeding...)' + if (seeded) message = ' (done!)' + if (error) message = ` (error: ${error})` + + return ( + + + {message} + + ) +} diff --git a/apps/cms/src/components/BeforeDashboard/index.scss b/apps/cms/src/components/BeforeDashboard/index.scss new file mode 100644 index 00000000000..7169b4f620e --- /dev/null +++ b/apps/cms/src/components/BeforeDashboard/index.scss @@ -0,0 +1,24 @@ +@import '~@payloadcms/ui/scss'; + +.dashboard .before-dashboard { + margin-bottom: base(1.5); + + &__banner { + & h4 { + margin: 0; + } + } + + &__instructions { + list-style: decimal; + margin-bottom: base(0.5); + + & li { + width: 100%; + } + } + + & a:hover { + opacity: 0.85; + } +} diff --git a/apps/cms/src/components/BeforeDashboard/index.tsx b/apps/cms/src/components/BeforeDashboard/index.tsx new file mode 100644 index 00000000000..907eae47fd6 --- /dev/null +++ b/apps/cms/src/components/BeforeDashboard/index.tsx @@ -0,0 +1,74 @@ +import { Banner } from '@payloadcms/ui/elements/Banner' +import React from 'react' + +import { SeedButton } from './SeedButton' +import './index.scss' + +const baseClass = 'before-dashboard' + +const BeforeDashboard: React.FC = () => { + return ( +
+ +

Welcome to your dashboard!

+
+ Here's what to do next: +
    +
  • + + {' with a few pages, posts, and projects to jump-start your new site, then '} + + visit your website + + {' to see the results.'} +
  • +
  • + If you created this repo using Payload Cloud, head over to GitHub and clone it to your + local machine. It will be under the GitHub Scope that you selected when creating + this project. +
  • +
  • + {'Modify your '} + + collections + + {' and add more '} + + fields + + {' as needed. If you are new to Payload, we also recommend you check out the '} + + Getting Started + + {' docs.'} +
  • +
  • + Commit and push your changes to the repository to trigger a redeployment of your project. +
  • +
+ {'Pro Tip: This block is a '} + + custom component + + , you can remove it at any time by updating your payload.config. +
+ ) +} + +export default BeforeDashboard diff --git a/apps/cms/src/components/BeforeLogin/index.tsx b/apps/cms/src/components/BeforeLogin/index.tsx new file mode 100644 index 00000000000..8768831c820 --- /dev/null +++ b/apps/cms/src/components/BeforeLogin/index.tsx @@ -0,0 +1,14 @@ +import React from 'react' + +const BeforeLogin: React.FC = () => { + return ( +
+

+ Welcome to your dashboard! + {' This is where site admins will log in to manage your website.'} +

+
+ ) +} + +export default BeforeLogin diff --git a/apps/cms/src/components/Card/index.tsx b/apps/cms/src/components/Card/index.tsx new file mode 100644 index 00000000000..cd64df2c956 --- /dev/null +++ b/apps/cms/src/components/Card/index.tsx @@ -0,0 +1,84 @@ +'use client' +import { cn } from '@/utilities/ui' +import useClickableCard from '@/utilities/useClickableCard' +import Link from 'next/link' +import React, { Fragment } from 'react' + +import type { Post } from '@/payload-types' + +import { Media } from '@/components/Media' + +export type CardPostData = Pick + +export const Card: React.FC<{ + alignItems?: 'center' + className?: string + doc?: CardPostData + relationTo?: 'posts' + showCategories?: boolean + title?: string +}> = (props) => { + const { card, link } = useClickableCard({}) + const { className, doc, relationTo, showCategories, title: titleFromProps } = props + + const { slug, categories, meta, title } = doc || {} + const { description, image: metaImage } = meta || {} + + const hasCategories = categories && Array.isArray(categories) && categories.length > 0 + const titleToUse = titleFromProps || title + const sanitizedDescription = description?.replace(/\s/g, ' ') // replace non-breaking space with white space + const href = `/${relationTo}/${slug}` + + return ( +
+
+ {!metaImage &&
No image
} + {metaImage && typeof metaImage !== 'string' && } +
+
+ {showCategories && hasCategories && ( +
+ {showCategories && hasCategories && ( +
+ {categories?.map((category, index) => { + if (typeof category === 'object') { + const { title: titleFromCategory } = category + + const categoryTitle = titleFromCategory || 'Untitled category' + + const isLast = index === categories.length - 1 + + return ( + + {categoryTitle} + {!isLast && ,  } + + ) + } + + return null + })} +
+ )} +
+ )} + {titleToUse && ( +
+

+ + {titleToUse} + +

+
+ )} + {description &&
{description &&

{sanitizedDescription}

}
} +
+
+ ) +} diff --git a/apps/cms/src/components/CollectionArchive/index.tsx b/apps/cms/src/components/CollectionArchive/index.tsx new file mode 100644 index 00000000000..b0a2a1c4935 --- /dev/null +++ b/apps/cms/src/components/CollectionArchive/index.tsx @@ -0,0 +1,32 @@ +import { cn } from '@/utilities/ui' +import React from 'react' + +import { Card, CardPostData } from '@/components/Card' + +export type Props = { + posts: CardPostData[] +} + +export const CollectionArchive: React.FC = (props) => { + const { posts } = props + + return ( +
+
+
+ {posts?.map((result, index) => { + if (typeof result === 'object' && result !== null) { + return ( +
+ +
+ ) + } + + return null + })} +
+
+
+ ) +} diff --git a/apps/cms/src/components/Link/index.tsx b/apps/cms/src/components/Link/index.tsx new file mode 100644 index 00000000000..0cf220eec59 --- /dev/null +++ b/apps/cms/src/components/Link/index.tsx @@ -0,0 +1,66 @@ +import { Button, type ButtonProps } from '@/components/ui/button' +import { cn } from '@/utilities/ui' +import Link from 'next/link' +import React from 'react' + +import type { Page, Post } from '@/payload-types' + +type CMSLinkType = { + appearance?: 'inline' | ButtonProps['variant'] + children?: React.ReactNode + className?: string + label?: string | null + newTab?: boolean | null + reference?: { + relationTo: 'pages' | 'posts' + value: Page | Post | string | number + } | null + size?: ButtonProps['size'] | null + type?: 'custom' | 'reference' | null + url?: string | null +} + +export const CMSLink: React.FC = (props) => { + const { + type, + appearance = 'inline', + children, + className, + label, + newTab, + reference, + size: sizeFromProps, + url, + } = props + + const href = + type === 'reference' && typeof reference?.value === 'object' && reference.value.slug + ? `${reference?.relationTo !== 'pages' ? `/${reference?.relationTo}` : ''}/${ + reference.value.slug + }` + : url + + if (!href) return null + + const size = appearance === 'link' ? 'clear' : sizeFromProps + const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {} + + /* Ensure we don't break any styles set by richText */ + if (appearance === 'inline') { + return ( + + {label && label} + {children && children} + + ) + } + + return ( + + ) +} diff --git a/apps/cms/src/components/LivePreviewListener/index.tsx b/apps/cms/src/components/LivePreviewListener/index.tsx new file mode 100644 index 00000000000..8b40675e7fa --- /dev/null +++ b/apps/cms/src/components/LivePreviewListener/index.tsx @@ -0,0 +1,10 @@ +'use client' +import { getClientSideURL } from '@/utilities/getURL' +import { RefreshRouteOnSave as PayloadLivePreview } from '@payloadcms/live-preview-react' +import { useRouter } from 'next/navigation' +import React from 'react' + +export const LivePreviewListener: React.FC = () => { + const router = useRouter() + return +} diff --git a/apps/cms/src/components/Logo/Logo.tsx b/apps/cms/src/components/Logo/Logo.tsx new file mode 100644 index 00000000000..3efa904516d --- /dev/null +++ b/apps/cms/src/components/Logo/Logo.tsx @@ -0,0 +1,29 @@ +import clsx from 'clsx' +import React from 'react' + +interface Props { + className?: string + loading?: 'lazy' | 'eager' + priority?: 'auto' | 'high' | 'low' +} + +export const Logo = (props: Props) => { + const { loading: loadingFromProps, priority: priorityFromProps, className } = props + + const loading = loadingFromProps || 'lazy' + const priority = priorityFromProps || 'low' + + return ( + /* eslint-disable @next/next/no-img-element */ + Payload Logo + ) +} diff --git a/apps/cms/src/components/Media/ImageMedia/index.tsx b/apps/cms/src/components/Media/ImageMedia/index.tsx new file mode 100644 index 00000000000..c472254da6c --- /dev/null +++ b/apps/cms/src/components/Media/ImageMedia/index.tsx @@ -0,0 +1,77 @@ +'use client' + +import type { StaticImageData } from 'next/image' + +import { cn } from '@/utilities/ui' +import NextImage from 'next/image' +import React from 'react' + +import type { Props as MediaProps } from '../types' + +import { cssVariables } from '@/cssVariables' +import { getClientSideURL } from '@/utilities/getURL' + +const { breakpoints } = cssVariables + +// A base64 encoded image to use as a placeholder while the image is loading +const placeholderBlur = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAABchJREFUWEdtlwtTG0kMhHtGM7N+AAdcDsjj///EBLzenbtuadbLJaZUTlHB+tRqSesETB3IABqQG1KbUFqDlQorBSmboqeEBcC1d8zrCixXYGZcgMsFmH8B+AngHdurAmXKOE8nHOoBrU6opcGswPi5KSP9CcBaQ9kACJH/ALAA1xm4zMD8AczvQCcAQeJVAZsy7nYApTSUzwCHUKACeUJi9TsFci7AHmDtuHYqQIC9AgQYKnSwNAig4NyOOwXq/xU47gDYggarjIpsRSEA3Fqw7AGkwgW4fgALAdiC2btKgNZwbgdMbEFpqFR2UyCR8xwAhf8bUHIGk1ckMyB5C1YkeWAdAPQBAeiD6wVYPoD1HUgXwFagZAGc6oSpTmilopoD5GzISQD3odcNIFca0BUQQM5YA2DpHV0AYURBDIAL0C+ugC0C4GedSsVUmwC8/4w8TPiwU6AClJ5RWL1PgQNkrABWdKB3YF3cBwRY5lsI4ApkKpCQi+FIgFJU/TDgDuAxAAwonJuKpGD1rkCXCR1ALyrAUSSEQAhwBdYZ6DPAgSUA2c1wKIZmRcHxMzMYR9DH8NlbkAwwApSAcABwBwTAbb6owAr0AFiZPILVEyCtMmK2jCkTwFDNUNj7nJETQx744gCUmgkZVGJUHyakEZE4W91jtGFA9KsD8Z3JFYDlhGYZLWcllwJMnplcPy+csFAgAAaIDOgeuAGoB96GLZg4kmtfMjnr6ig5oSoySsoy3ya/FMivXZWxwr0KIf9nACbfqcBEgmBSAtAlIT83R+70IWpyACamIjf5E1Iqb9ECVmnoI/FvAIRk8s2J0Y5IquQDgB+5wpScw5AUTC75VTmTs+72NUzoCvQIaAXv5Q8PDAZKLD+MxLv3RFE7KlsQChgBIlKiCv5ByaZv3gJZNm8AnVMhAN+EjrtTYQMICJpu6/0aiQnhClANlz+Bw0cIWa8ev0sBrtrhAyaXEnrfGfATQJiRKih5vKeOHNXXPFrgyamAADh0Q4F2/sESojomDS9o9k0b0H83xjB8qL+JNoTjN+enjpaBpingRh4e8MSugudM030A8FeqMI6PFIgNyPehkpZWGFEAARIQdH5LcAAqIACHkAJqg4OoBccHAuz76wr4BbzFOEa8iBuAZB8AtJHLP2VgMgJw/EIBowo7HxCAH3V6dAXEE/vZ5aZIA8BP8RKhm7Cp8BnAMnAQADdgQDA520AVIpScP+enHz0Gwp25h4i2dPg5FkDXrbsdJikQwXuWgaM5gEMk1AgH4DKKFjDf3bMD+FjEeIxLlRKYnBk2BbquvSDCAQ4gwZiMAAmH4gBTyRtEsYxi7gP6QSrc//39BrDNqG8rtYTmC4BV1SfMhOhaumFCT87zy4pPhQBZEK1kQVRjJBBi7AOlePgyAPYjwlvtagx9e/dnQraAyS894TIkkAIEYMKEc8k4EqJ68lZ5jjNqcQC2QteQOf7659umwBgPybNtK4dg9WvnMyFwXYGP7uEO1lwJgAnPNeMYMVXbIIYKFioI4PGFt+BWPVfmWJdjW2lTUnLGCswECAgaUy86iwA1464ajo0QhgMBFGyBoZahANsMpMfXr1JA1SN29m5lqgXj+UPV85uRA7yv/KYUO4Tk7Hc1AZwbIRzg0AyNj2UlAMwfSLSMnl7fdAbcxHuA27YaAMvaQ4GOjwX4RTUGAG8Ge14N963g1AynqUiFqRX9noasxT4b8entNRQYyamk/3tYcHsO7R3XJRRYOn4tw4iUnwBM5gDnySGOreAwAGo8F9IDHEcq8Pz2Kg/oXCpuIL6tOPD8LsDn0ABYQoGFRowlsAEUPPDrGAGowAbgKsgDMmE8mDy/vXQ9IAwI7u4wta+gAdAdgB64Ah9SgD4IgGKhwACoAjgNgFDhtxY8f33ZTMjqdTAiHMBPrn8ZWkEfzFdX4Oc1AHg3+ADbvN8PU8WdFKg4Tt6CQy2+D4YHaMT/JP4XzbAq98cPDIUAAAAASUVORK5CYII=' + +export const ImageMedia: React.FC = (props) => { + const { + alt: altFromProps, + fill, + pictureClassName, + imgClassName, + priority, + resource, + size: sizeFromProps, + src: srcFromProps, + loading: loadingFromProps, + } = props + + let width: number | undefined + let height: number | undefined + let alt = altFromProps + let src: StaticImageData | string = srcFromProps || '' + + if (!src && resource && typeof resource === 'object') { + const { alt: altFromResource, height: fullHeight, url, width: fullWidth } = resource + + width = fullWidth! + height = fullHeight! + alt = altFromResource || '' + + const cacheTag = resource.updatedAt + + src = `${getClientSideURL()}${url}?${cacheTag}` + } + + const loading = loadingFromProps || (!priority ? 'lazy' : undefined) + + // NOTE: this is used by the browser to determine which image to download at different screen sizes + const sizes = sizeFromProps + ? sizeFromProps + : Object.entries(breakpoints) + .map(([, value]) => `(max-width: ${value}px) ${value * 2}w`) + .join(', ') + + return ( + + + + ) +} diff --git a/apps/cms/src/components/Media/VideoMedia/index.tsx b/apps/cms/src/components/Media/VideoMedia/index.tsx new file mode 100644 index 00000000000..873eaacda51 --- /dev/null +++ b/apps/cms/src/components/Media/VideoMedia/index.tsx @@ -0,0 +1,46 @@ +'use client' + +import { cn } from '@/utilities/ui' +import React, { useEffect, useRef } from 'react' + +import type { Props as MediaProps } from '../types' + +import { getClientSideURL } from '@/utilities/getURL' + +export const VideoMedia: React.FC = (props) => { + const { onClick, resource, videoClassName } = props + + const videoRef = useRef(null) + // const [showFallback] = useState() + + useEffect(() => { + const { current: video } = videoRef + if (video) { + video.addEventListener('suspend', () => { + // setShowFallback(true); + // console.warn('Video was suspended, rendering fallback image.') + }) + } + }, []) + + if (resource && typeof resource === 'object') { + const { filename } = resource + + return ( + + ) + } + + return null +} diff --git a/apps/cms/src/components/Media/index.tsx b/apps/cms/src/components/Media/index.tsx new file mode 100644 index 00000000000..a4e2b9d0ef3 --- /dev/null +++ b/apps/cms/src/components/Media/index.tsx @@ -0,0 +1,25 @@ +import React, { Fragment } from 'react' + +import type { Props } from './types' + +import { ImageMedia } from './ImageMedia' +import { VideoMedia } from './VideoMedia' + +export const Media: React.FC = (props) => { + const { className, htmlElement = 'div', resource } = props + + const isVideo = typeof resource === 'object' && resource?.mimeType?.includes('video') + const Tag = htmlElement || Fragment + + return ( + + {isVideo ? : } + + ) +} diff --git a/apps/cms/src/components/Media/types.ts b/apps/cms/src/components/Media/types.ts new file mode 100644 index 00000000000..3bbbf225878 --- /dev/null +++ b/apps/cms/src/components/Media/types.ts @@ -0,0 +1,22 @@ +import type { StaticImageData } from 'next/image' +import type { ElementType, Ref } from 'react' + +import type { Media as MediaType } from '@/payload-types' + +export interface Props { + alt?: string + className?: string + fill?: boolean // for NextImage only + htmlElement?: ElementType | null + pictureClassName?: string + imgClassName?: string + onClick?: () => void + onLoad?: () => void + loading?: 'lazy' | 'eager' // for NextImage only + priority?: boolean // for NextImage only + ref?: Ref + resource?: MediaType | string | number | null // for Payload media + size?: string // for NextImage only + src?: StaticImageData // for static media + videoClassName?: string +} diff --git a/apps/cms/src/components/PageRange/index.tsx b/apps/cms/src/components/PageRange/index.tsx new file mode 100644 index 00000000000..2a3a39ddabe --- /dev/null +++ b/apps/cms/src/components/PageRange/index.tsx @@ -0,0 +1,57 @@ +import React from 'react' + +const defaultLabels = { + plural: 'Docs', + singular: 'Doc', +} + +const defaultCollectionLabels = { + posts: { + plural: 'Posts', + singular: 'Post', + }, +} + +export const PageRange: React.FC<{ + className?: string + collection?: keyof typeof defaultCollectionLabels + collectionLabels?: { + plural?: string + singular?: string + } + currentPage?: number + limit?: number + totalDocs?: number +}> = (props) => { + const { + className, + collection, + collectionLabels: collectionLabelsFromProps, + currentPage, + limit, + totalDocs, + } = props + + let indexStart = (currentPage ? currentPage - 1 : 1) * (limit || 1) + 1 + if (totalDocs && indexStart > totalDocs) indexStart = 0 + + let indexEnd = (currentPage || 1) * (limit || 1) + if (totalDocs && indexEnd > totalDocs) indexEnd = totalDocs + + const { plural, singular } = + collectionLabelsFromProps || + (collection ? defaultCollectionLabels[collection] : undefined) || + defaultLabels || + {} + + return ( +
+ {(typeof totalDocs === 'undefined' || totalDocs === 0) && 'Search produced no results.'} + {typeof totalDocs !== 'undefined' && + totalDocs > 0 && + `Showing ${indexStart}${indexStart > 0 ? ` - ${indexEnd}` : ''} of ${totalDocs} ${ + totalDocs > 1 ? plural : singular + }`} +
+ ) +} diff --git a/apps/cms/src/components/Pagination/index.tsx b/apps/cms/src/components/Pagination/index.tsx new file mode 100644 index 00000000000..448b301df2d --- /dev/null +++ b/apps/cms/src/components/Pagination/index.tsx @@ -0,0 +1,101 @@ +'use client' +import { + Pagination as PaginationComponent, + PaginationContent, + PaginationEllipsis, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from '@/components/ui/pagination' +import { cn } from '@/utilities/ui' +import { useRouter } from 'next/navigation' +import React from 'react' + +export const Pagination: React.FC<{ + className?: string + page: number + totalPages: number +}> = (props) => { + const router = useRouter() + + const { className, page, totalPages } = props + const hasNextPage = page < totalPages + const hasPrevPage = page > 1 + + const hasExtraPrevPages = page - 1 > 1 + const hasExtraNextPages = page + 1 < totalPages + + return ( +
+ + + + { + router.push(`/posts/page/${page - 1}`) + }} + /> + + + {hasExtraPrevPages && ( + + + + )} + + {hasPrevPage && ( + + { + router.push(`/posts/page/${page - 1}`) + }} + > + {page - 1} + + + )} + + + { + router.push(`/posts/page/${page}`) + }} + > + {page} + + + + {hasNextPage && ( + + { + router.push(`/posts/page/${page + 1}`) + }} + > + {page + 1} + + + )} + + {hasExtraNextPages && ( + + + + )} + + + { + router.push(`/posts/page/${page + 1}`) + }} + /> + + + +
+ ) +} diff --git a/apps/cms/src/components/PayloadRedirects/index.tsx b/apps/cms/src/components/PayloadRedirects/index.tsx new file mode 100644 index 00000000000..d2397716e35 --- /dev/null +++ b/apps/cms/src/components/PayloadRedirects/index.tsx @@ -0,0 +1,48 @@ +import type React from 'react' +import type { Page, Post } from '@/payload-types' + +import { getCachedDocument } from '@/utilities/getDocument' +import { getCachedRedirects } from '@/utilities/getRedirects' +import { notFound, redirect } from 'next/navigation' + +interface Props { + disableNotFound?: boolean + url: string +} + +/* This component helps us with SSR based dynamic redirects */ +export const PayloadRedirects: React.FC = async ({ disableNotFound, url }) => { + const redirects = await getCachedRedirects()() + + const redirectItem = redirects.find((redirect) => redirect.from === url) + + if (redirectItem) { + if (redirectItem.to?.url) { + redirect(redirectItem.to.url) + } + + let redirectUrl: string + + if (typeof redirectItem.to?.reference?.value === 'string') { + const collection = redirectItem.to?.reference?.relationTo + const id = redirectItem.to?.reference?.value + + const document = (await getCachedDocument(collection, id)()) as Page | Post + redirectUrl = `${redirectItem.to?.reference?.relationTo !== 'pages' ? `/${redirectItem.to?.reference?.relationTo}` : ''}/${ + document?.slug + }` + } else { + redirectUrl = `${redirectItem.to?.reference?.relationTo !== 'pages' ? `/${redirectItem.to?.reference?.relationTo}` : ''}/${ + typeof redirectItem.to?.reference?.value === 'object' + ? redirectItem.to?.reference?.value?.slug + : '' + }` + } + + if (redirectUrl) redirect(redirectUrl) + } + + if (disableNotFound) return null + + notFound() +} diff --git a/apps/cms/src/components/RichText/index.tsx b/apps/cms/src/components/RichText/index.tsx new file mode 100644 index 00000000000..1994159be28 --- /dev/null +++ b/apps/cms/src/components/RichText/index.tsx @@ -0,0 +1,81 @@ +import { MediaBlock } from '@/blocks/MediaBlock/Component' +import { + DefaultNodeTypes, + SerializedBlockNode, + SerializedLinkNode, + type DefaultTypedEditorState, +} from '@payloadcms/richtext-lexical' +import { + JSXConvertersFunction, + LinkJSXConverter, + RichText as ConvertRichText, +} from '@payloadcms/richtext-lexical/react' + +import { CodeBlock, CodeBlockProps } from '@/blocks/Code/Component' + +import type { + BannerBlock as BannerBlockProps, + CallToActionBlock as CTABlockProps, + MediaBlock as MediaBlockProps, +} from '@/payload-types' +import { BannerBlock } from '@/blocks/Banner/Component' +import { CallToActionBlock } from '@/blocks/CallToAction/Component' +import { cn } from '@/utilities/ui' + +type NodeTypes = + | DefaultNodeTypes + | SerializedBlockNode + +const internalDocToHref = ({ linkNode }: { linkNode: SerializedLinkNode }) => { + const { value, relationTo } = linkNode.fields.doc! + if (typeof value !== 'object') { + throw new Error('Expected value to be an object') + } + const slug = value.slug + return relationTo === 'posts' ? `/posts/${slug}` : `/${slug}` +} + +const jsxConverters: JSXConvertersFunction = ({ defaultConverters }) => ({ + ...defaultConverters, + ...LinkJSXConverter({ internalDocToHref }), + blocks: { + banner: ({ node }) => , + mediaBlock: ({ node }) => ( + + ), + code: ({ node }) => , + cta: ({ node }) => , + }, +}) + +type Props = { + data: DefaultTypedEditorState + enableGutter?: boolean + enableProse?: boolean +} & React.HTMLAttributes + +export default function RichText(props: Props) { + const { className, enableProse = true, enableGutter = true, ...rest } = props + return ( + + ) +} diff --git a/apps/cms/src/components/ui/button.tsx b/apps/cms/src/components/ui/button.tsx new file mode 100644 index 00000000000..391a566e389 --- /dev/null +++ b/apps/cms/src/components/ui/button.tsx @@ -0,0 +1,52 @@ +import { cn } from '@/utilities/ui' +import { Slot } from '@radix-ui/react-slot' +import { type VariantProps, cva } from 'class-variance-authority' +import * as React from 'react' + +const buttonVariants = cva( + 'inline-flex items-center justify-center whitespace-nowrap rounded text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', + { + defaultVariants: { + size: 'default', + variant: 'default', + }, + variants: { + size: { + clear: '', + default: 'h-10 px-4 py-2', + icon: 'h-10 w-10', + lg: 'h-11 rounded px-8', + sm: 'h-9 rounded px-3', + }, + variant: { + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', + ghost: 'hover:bg-card hover:text-accent-foreground', + link: 'text-primary items-start justify-start underline-offset-4 hover:underline', + outline: 'border border-border bg-background hover:bg-card hover:text-accent-foreground', + secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + }, + }, + }, +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean + ref?: React.Ref +} + +const Button: React.FC = ({ + asChild = false, + className, + size, + variant, + ref, + ...props +}) => { + const Comp = asChild ? Slot : 'button' + return +} + +export { Button, buttonVariants } diff --git a/apps/cms/src/components/ui/card.tsx b/apps/cms/src/components/ui/card.tsx new file mode 100644 index 00000000000..54b8b442d00 --- /dev/null +++ b/apps/cms/src/components/ui/card.tsx @@ -0,0 +1,48 @@ +import { cn } from '@/utilities/ui' +import * as React from 'react' + +const Card: React.FC< + { ref?: React.Ref } & React.HTMLAttributes +> = ({ className, ref, ...props }) => ( +
+) + +const CardHeader: React.FC< + { ref?: React.Ref } & React.HTMLAttributes +> = ({ className, ref, ...props }) => ( +
+) + +const CardTitle: React.FC< + { ref?: React.Ref } & React.HTMLAttributes +> = ({ className, ref, ...props }) => ( +

+) + +const CardDescription: React.FC< + { ref?: React.Ref } & React.HTMLAttributes +> = ({ className, ref, ...props }) => ( +

+) + +const CardContent: React.FC< + { ref?: React.Ref } & React.HTMLAttributes +> = ({ className, ref, ...props }) => ( +

+) + +const CardFooter: React.FC< + { ref?: React.Ref } & React.HTMLAttributes +> = ({ className, ref, ...props }) => ( +
+) + +export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } diff --git a/apps/cms/src/components/ui/checkbox.tsx b/apps/cms/src/components/ui/checkbox.tsx new file mode 100644 index 00000000000..e9d9cdd2993 --- /dev/null +++ b/apps/cms/src/components/ui/checkbox.tsx @@ -0,0 +1,27 @@ +'use client' + +import { cn } from '@/utilities/ui' +import * as CheckboxPrimitive from '@radix-ui/react-checkbox' +import { Check } from 'lucide-react' +import * as React from 'react' + +const Checkbox: React.FC< + { + ref?: React.Ref + } & React.ComponentProps +> = ({ className, ref, ...props }) => ( + + + + + +) + +export { Checkbox } diff --git a/apps/cms/src/components/ui/input.tsx b/apps/cms/src/components/ui/input.tsx new file mode 100644 index 00000000000..10df0d48a46 --- /dev/null +++ b/apps/cms/src/components/ui/input.tsx @@ -0,0 +1,22 @@ +import { cn } from '@/utilities/ui' +import * as React from 'react' + +const Input: React.FC< + { + ref?: React.Ref + } & React.InputHTMLAttributes +> = ({ type, className, ref, ...props }) => { + return ( + + ) +} + +export { Input } diff --git a/apps/cms/src/components/ui/label.tsx b/apps/cms/src/components/ui/label.tsx new file mode 100644 index 00000000000..d9c07ef18a9 --- /dev/null +++ b/apps/cms/src/components/ui/label.tsx @@ -0,0 +1,19 @@ +'use client' + +import { cn } from '@/utilities/ui' +import * as LabelPrimitive from '@radix-ui/react-label' +import { type VariantProps, cva } from 'class-variance-authority' +import * as React from 'react' + +const labelVariants = cva( + 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70', +) + +const Label: React.FC< + { ref?: React.Ref } & React.ComponentProps & + VariantProps +> = ({ className, ref, ...props }) => ( + +) + +export { Label } diff --git a/apps/cms/src/components/ui/pagination.tsx b/apps/cms/src/components/ui/pagination.tsx new file mode 100644 index 00000000000..b09be5b57ae --- /dev/null +++ b/apps/cms/src/components/ui/pagination.tsx @@ -0,0 +1,92 @@ +import type { ButtonProps } from '@/components/ui/button' + +import { buttonVariants } from '@/components/ui/button' +import { cn } from '@/utilities/ui' +import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react' +import * as React from 'react' + +const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => ( +