├── .DS_Store ├── .gitattributes ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── README.md ├── admin-search ├── .gitignore ├── .prettierrc.json ├── .swcrc ├── LICENSE ├── README.md ├── dev │ ├── .env.example │ ├── next-env.d.ts │ ├── next.config.mjs │ ├── package.json │ ├── payload-types.ts │ ├── pnpm-lock.yaml │ ├── seed-db.ts │ ├── src │ │ ├── app │ │ │ ├── (payload) │ │ │ │ ├── admin │ │ │ │ │ ├── [[...segments]] │ │ │ │ │ │ ├── not-found.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── importMap.js │ │ │ │ ├── api │ │ │ │ │ ├── [...slug] │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── graphql-playground │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── graphql │ │ │ │ │ │ └── route.ts │ │ │ │ ├── custom.scss │ │ │ │ └── layout.tsx │ │ │ └── my-route │ │ │ │ └── route.ts │ │ ├── collections │ │ │ ├── authors.ts │ │ │ ├── media.ts │ │ │ ├── pages.ts │ │ │ └── posts.ts │ │ ├── helpers │ │ │ ├── credentials.ts │ │ │ └── testEmailAdapter.ts │ │ ├── payload.config.ts │ │ └── seed.ts │ └── tsconfig.json ├── eslint.config.js ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── src │ ├── components │ │ ├── SearchBar │ │ │ ├── SearchBar.css │ │ │ └── SearchBar.tsx │ │ ├── SearchButton │ │ │ └── SearchButton.tsx │ │ ├── SearchModal │ │ │ ├── SearchModal.css │ │ │ ├── SearchModal.tsx │ │ │ └── SearchModalSkeleton.tsx │ │ └── SearchWrapper │ │ │ └── SearchWrapper.tsx │ ├── exports │ │ └── client.ts │ ├── index.ts │ ├── plugin.ts │ └── types │ │ ├── AdminSearchPluginConfig.ts │ │ └── SearchResult.ts └── tsconfig.json ├── cloudinary ├── .gitignore ├── .npmignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── dev │ ├── .gitignore │ ├── next-env.d.ts │ ├── next.config.mjs │ ├── nodemon.json │ ├── package.json │ ├── plugin.spec.ts │ ├── src │ │ ├── app │ │ │ └── (payload) │ │ │ │ ├── admin │ │ │ │ ├── [[...segments]] │ │ │ │ │ ├── not-found.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── importMap.js │ │ │ │ ├── api │ │ │ │ ├── [...slug] │ │ │ │ │ └── route.ts │ │ │ │ ├── graphql-playground │ │ │ │ │ └── route.ts │ │ │ │ └── graphql │ │ │ │ │ └── route.ts │ │ │ │ ├── custom.scss │ │ │ │ └── layout.tsx │ │ ├── collections │ │ │ ├── images.ts │ │ │ └── videos.ts │ │ ├── payload-types.ts │ │ └── payload.config.ts │ └── tsconfig.json ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── src │ ├── hooks │ │ ├── afterDelete.ts │ │ └── beforeChange.ts │ ├── index.ts │ ├── plugin.ts │ ├── types │ │ └── CloudinaryPluginConfig.ts │ └── utils │ │ ├── extendUploadCollectionConfig.ts │ │ └── streamUpload.ts └── tsconfig.json ├── geocoding ├── .editorconfig ├── .gitignore ├── .prettierrc.json ├── .vscode │ └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dev │ ├── .gitignore │ ├── next-env.d.ts │ ├── next.config.mjs │ ├── nodemon.json │ ├── package.json │ ├── plugin.spec.ts │ ├── src │ │ ├── app │ │ │ └── (payload) │ │ │ │ ├── admin │ │ │ │ ├── [[...segments]] │ │ │ │ │ ├── not-found.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── importMap.js │ │ │ │ ├── api │ │ │ │ ├── [...slug] │ │ │ │ │ └── route.ts │ │ │ │ ├── graphql-playground │ │ │ │ │ └── route.ts │ │ │ │ └── graphql │ │ │ │ │ └── route.ts │ │ │ │ ├── custom.scss │ │ │ │ └── layout.tsx │ │ ├── collection │ │ │ └── pages.ts │ │ ├── emailAdapter.ts │ │ ├── payload-types.ts │ │ └── payload.config.ts │ └── tsconfig.json ├── eslint.config.js ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── src │ ├── components │ │ └── GeocodingFieldComponent.tsx │ ├── exports │ │ └── client.ts │ ├── fields │ │ └── geocodingField.ts │ ├── index.ts │ ├── plugin.ts │ └── types │ │ ├── GeoCodingFieldConfig.ts │ │ └── GeoCodingPluginConfig.ts └── tsconfig.json ├── package.json ├── pages ├── .gitignore ├── .npmignore ├── .prettierrc.json ├── .swcrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dev │ ├── .gitignore │ ├── next-env.d.ts │ ├── next.config.mjs │ ├── nodemon.json │ ├── package.json │ ├── plugin.test.ts │ ├── src │ │ ├── app │ │ │ └── (payload) │ │ │ │ ├── admin │ │ │ │ ├── [[...segments]] │ │ │ │ │ ├── not-found.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── importMap.js │ │ │ │ ├── api │ │ │ │ ├── [...slug] │ │ │ │ │ └── route.ts │ │ │ │ ├── graphql-playground │ │ │ │ │ └── route.ts │ │ │ │ └── graphql │ │ │ │ │ └── route.ts │ │ │ │ ├── custom.scss │ │ │ │ └── layout.tsx │ │ ├── collections │ │ │ ├── authors.ts │ │ │ ├── blogpost-categories.ts │ │ │ ├── blogposts.ts │ │ │ ├── countries.ts │ │ │ ├── country-travel-tips.ts │ │ │ ├── pages.ts │ │ │ └── redirects.ts │ │ ├── payload-types.ts │ │ ├── payload.config.ts │ │ └── utils │ │ │ └── generatePageURL.ts │ ├── tsconfig.json │ └── vite.config.ts ├── dev_multi_tenant │ ├── .gitignore │ ├── next-env.d.ts │ ├── next.config.mjs │ ├── nodemon.json │ ├── package.json │ ├── plugin.test.ts │ ├── src │ │ ├── app │ │ │ └── (payload) │ │ │ │ ├── admin │ │ │ │ ├── [[...segments]] │ │ │ │ │ ├── not-found.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── importMap.js │ │ │ │ ├── api │ │ │ │ ├── [...slug] │ │ │ │ │ └── route.ts │ │ │ │ ├── graphql-playground │ │ │ │ │ └── route.ts │ │ │ │ └── graphql │ │ │ │ │ └── route.ts │ │ │ │ ├── custom.scss │ │ │ │ └── layout.tsx │ │ ├── collections │ │ │ ├── authors.ts │ │ │ ├── blogpost-categories.ts │ │ │ ├── blogposts.ts │ │ │ ├── countries.ts │ │ │ ├── country-travel-tips.ts │ │ │ ├── pages.ts │ │ │ ├── redirects.ts │ │ │ └── tenants.ts │ │ ├── payload-types.ts │ │ ├── payload.config.ts │ │ └── utils │ │ │ └── generatePageURL.ts │ ├── tsconfig.json │ └── vite.config.ts ├── dev_unlocalized │ ├── .gitignore │ ├── next-env.d.ts │ ├── next.config.mjs │ ├── nodemon.json │ ├── package.json │ ├── plugin.test.ts │ ├── src │ │ ├── app │ │ │ └── (payload) │ │ │ │ ├── admin │ │ │ │ ├── [[...segments]] │ │ │ │ │ ├── not-found.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── importMap.js │ │ │ │ ├── api │ │ │ │ ├── [...slug] │ │ │ │ │ └── route.ts │ │ │ │ ├── graphql-playground │ │ │ │ │ └── route.ts │ │ │ │ └── graphql │ │ │ │ │ └── route.ts │ │ │ │ ├── custom.scss │ │ │ │ └── layout.tsx │ │ ├── collections │ │ │ ├── authors.ts │ │ │ ├── blogpost-categories.ts │ │ │ ├── blogposts.ts │ │ │ ├── countries.ts │ │ │ ├── country-travel-tips.ts │ │ │ ├── pages.ts │ │ │ └── redirects.ts │ │ ├── payload-types.ts │ │ ├── payload.config.ts │ │ └── utils │ │ │ └── generatePageURL.ts │ ├── tsconfig.json │ └── vite.config.ts ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── src │ ├── collections │ │ ├── PageCollectionConfig.ts │ │ └── RedirectsCollectionConfig.ts │ ├── components │ │ ├── client │ │ │ ├── BreadcrumbsField.tsx │ │ │ ├── IsRootPageStatus.tsx │ │ │ ├── PathField.tsx │ │ │ ├── PreviewButtonField.tsx │ │ │ ├── SlugField.tsx │ │ │ └── hooks │ │ │ │ ├── useBreadcrumbs.ts │ │ │ │ ├── useCollectionConfig.ts │ │ │ │ └── usePageCollectionConfigAtrributes.ts │ │ └── server │ │ │ ├── IsRootPageField.tsx │ │ │ ├── ParentField.tsx │ │ │ └── SlugFieldWrapper.tsx │ ├── exports │ │ ├── client.ts │ │ └── server.ts │ ├── fields │ │ ├── alternatePathsField.ts │ │ ├── breadcrumbsField.ts │ │ ├── isRootPageField.ts │ │ ├── parentField.ts │ │ ├── pathField.ts │ │ └── slugField.ts │ ├── hooks │ │ ├── beforeDuplicate.ts │ │ ├── deleteUnselectedFieldsAfterRead.ts │ │ ├── preventParentDeletion.ts │ │ ├── selectDependentFieldsBeforeOperation.ts │ │ ├── setVirtualFields.ts │ │ ├── validateRedirect.ts │ │ └── validateSlug.ts │ ├── index.ts │ ├── plugin.ts │ ├── translations │ │ ├── de.ts │ │ ├── en.ts │ │ ├── index.ts │ │ └── translation-schema.json │ ├── types │ │ ├── Breadcrumb.ts │ │ ├── Locale.ts │ │ ├── PageCollectionConfig.ts │ │ ├── PageCollectionConfigAttributes.ts │ │ ├── PagesPluginConfig.ts │ │ ├── RedirectsCollectionConfig.ts │ │ ├── RedirectsCollectionConfigAttributes.ts │ │ └── SeoMetadata.ts │ └── utils │ │ ├── AdminPanelError.ts │ │ ├── childDocumentsOf.ts │ │ ├── deepMergeSimple.ts │ │ ├── fetchRestApi.ts │ │ ├── getBreadcrumbs.ts │ │ ├── getPageCollectionConfigAttributes.ts │ │ ├── localeFromRequest.ts │ │ ├── pageCollectionConfigHelpers.ts │ │ ├── pathFromBreadcrumbs.ts │ │ ├── setPageVirtualFields.ts │ │ ├── setRootPageVirtualFields.ts │ │ ├── translatedLabel.ts │ │ ├── useDidUpdateEffect.ts │ │ ├── usePluginTranslations.ts │ │ ├── validateBreadcrumbs.ts │ │ └── validatePath.ts └── tsconfig.json ├── pnpm-lock.yaml └── seo ├── .gitignore ├── .npmignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── dev ├── .gitignore ├── next-env.d.ts ├── next.config.mjs ├── nodemon.json ├── package.json ├── plugin.spec.ts ├── src │ ├── app │ │ └── (payload) │ │ │ ├── admin │ │ │ ├── [[...segments]] │ │ │ │ ├── not-found.tsx │ │ │ │ └── page.tsx │ │ │ └── importMap.js │ │ │ ├── api │ │ │ ├── [...slug] │ │ │ │ └── route.ts │ │ │ ├── graphql-playground │ │ │ │ └── route.ts │ │ │ └── graphql │ │ │ │ └── route.ts │ │ │ ├── custom.scss │ │ │ └── layout.tsx │ ├── collections │ │ └── pages.ts │ ├── payload-types.ts │ └── payload.config.ts └── tsconfig.json ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── src ├── components │ ├── KeywordsFieldLabel.tsx │ └── KeywordsFieldRowLabel.tsx ├── exports │ └── client.ts ├── fields │ └── keywordsField.ts ├── index.ts ├── plugin.ts ├── types │ ├── DocumentContentTransformer.ts │ ├── PageContext.ts │ ├── SeoPluginConfig.ts │ └── WebsiteContext.ts └── utils │ ├── generateMetaDescription.ts │ ├── generateOpenAIChatCompletion.ts │ └── lexicalToPlainText.ts └── tsconfig.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhb-software/payload-plugins/efb8fdb99155a824327e56c7f42a7712c2796054/.DS_Store -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help improve the packages 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Package(s) version:** 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | .next -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | importMap.js 2 | payload-types.ts 3 | .vscode 4 | .d.ts 5 | .spec.ts -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "parser": "typescript", 4 | "semi": false, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "overrides": [ 8 | { 9 | "files": ["**/importMap.js", "**/payload-types.ts", "**/**.yaml"], 10 | "options": { 11 | "requirePragma": true 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /admin-search/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | /.idea/* 10 | !/.idea/runConfigurations 11 | 12 | # testing 13 | /coverage 14 | 15 | # next.js 16 | .next/ 17 | /out/ 18 | 19 | # production 20 | /build 21 | /dist 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # local env files 33 | .env*.local 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | 41 | .env 42 | 43 | /dev/media 44 | 45 | # Playwright 46 | /test-results/ 47 | /playwright-report/ 48 | /blob-report/ 49 | /playwright/.cache/ 50 | -------------------------------------------------------------------------------- /admin-search/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "parser": "typescript", 4 | "semi": false, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "overrides": [ 8 | { 9 | "files": ["**/importMap.js", "**/payload-types.ts", "**/**.yaml"], 10 | "options": { 11 | "requirePragma": true 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /admin-search/.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/swcrc", 3 | "sourceMaps": true, 4 | "jsc": { 5 | "target": "esnext", 6 | "parser": { 7 | "syntax": "typescript", 8 | "tsx": true, 9 | "dts": true 10 | }, 11 | "transform": { 12 | "react": { 13 | "runtime": "automatic", 14 | "pragmaFrag": "React.Fragment", 15 | "throwIfNamespace": true, 16 | "development": false, 17 | "useBuiltins": true 18 | } 19 | } 20 | }, 21 | "module": { 22 | "type": "es6" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /admin-search/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 JHB Software 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /admin-search/dev/.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URI=mongodb://127.0.0.1/payload-plugin-template 2 | PAYLOAD_SECRET=YOUR_SECRET_HERE 3 | -------------------------------------------------------------------------------- /admin-search/dev/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /admin-search/dev/next.config.mjs: -------------------------------------------------------------------------------- 1 | import { withPayload } from '@payloadcms/next/withPayload' 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = { 5 | eslint: { 6 | ignoreDuringBuilds: true, 7 | }, 8 | 9 | // Allow for ESM .js import statements 10 | webpack: (webpackConfig) => { 11 | webpackConfig.resolve.extensionAlias = { 12 | '.cjs': ['.cts', '.cjs'], 13 | '.js': ['.ts', '.tsx', '.js', '.jsx'], 14 | '.mjs': ['.mts', '.mjs'], 15 | } 16 | 17 | return webpackConfig 18 | }, 19 | 20 | redirects: () => [ 21 | { 22 | source: '/', 23 | destination: '/admin', 24 | permanent: true, 25 | }, 26 | ], 27 | } 28 | 29 | export default withPayload(nextConfig) 30 | -------------------------------------------------------------------------------- /admin-search/dev/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "admin-search-dev", 3 | "description": "Dev app for the admin-search plugin.", 4 | "version": "0.0.1", 5 | "license": "MIT", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "cross-env NODE_OPTIONS=\"${NODE_OPTIONS} --no-deprecation\" next dev", 9 | "dev:turbo": "pnpm dev --turbo", 10 | "dev:safe": "rm -rf .next && pnpm dev", 11 | "build": "cross-env NODE_OPTIONS=--no-deprecation next build", 12 | "start": "cross-env NODE_OPTIONS=--no-deprecation next start", 13 | "format": "prettier --write src", 14 | "payload": "cross-env PAYLOAD_CONFIG_PATH=./src/payload.config.ts payload", 15 | "generate:types": "cross-env PAYLOAD_CONFIG_PATH=./src/payload.config.ts payload generate:types", 16 | "generate:schema": "payload-graphql generate:schema", 17 | "generate:importmap": "cross-env PAYLOAD_CONFIG_PATH=./src/payload.config.ts payload generate:importmap", 18 | "seed": "cross-env PAYLOAD_CONFIG_PATH=./src/payload.config.ts tsx seed-db.ts" 19 | }, 20 | "dependencies": { 21 | "@jhb.software/payload-admin-search": "workspace:*", 22 | "@payloadcms/db-mongodb": "3.x", 23 | "@payloadcms/next": "3.x", 24 | "@payloadcms/plugin-search": "3.x", 25 | "@payloadcms/richtext-lexical": "3.x", 26 | "@payloadcms/translations": "3.x", 27 | "@payloadcms/ui": "3.x", 28 | "next": "15.x", 29 | "payload": "3.x", 30 | "react": "19.x", 31 | "react-dom": "19.x" 32 | }, 33 | "devDependencies": { 34 | "cross-env": "^7.0.3", 35 | "dotenv": "^16.4.5", 36 | "tsx": "^4.7.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /admin-search/dev/seed-db.ts: -------------------------------------------------------------------------------- 1 | import { getPayload } from 'payload' 2 | 3 | import config from './src/payload.config.js' 4 | 5 | async function seedDatabase() { 6 | try { 7 | console.log('🌱 Starting database seeding...') 8 | 9 | const payload = await getPayload({ config }) 10 | 11 | // Import and run the seed function 12 | const { seed } = await import('./src/seed.js') 13 | await seed(payload) 14 | 15 | console.log('✅ Database seeding completed successfully!') 16 | process.exit(0) 17 | } catch (error) { 18 | console.error('❌ Error seeding database:', error) 19 | process.exit(1) 20 | } 21 | } 22 | 23 | // Handle the unhandled promise 24 | seedDatabase().catch((error) => { 25 | console.error('❌ Unhandled error in seedDatabase:', error) 26 | process.exit(1) 27 | }) 28 | -------------------------------------------------------------------------------- /admin-search/dev/src/app/(payload)/admin/[[...segments]]/not-found.tsx: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import type { Metadata } from 'next' 4 | 5 | import config from '@payload-config' 6 | import { generatePageMetadata, NotFoundPage } from '@payloadcms/next/views' 7 | 8 | import { importMap } from '../importMap.js' 9 | 10 | type Args = { 11 | params: Promise<{ 12 | segments: string[] 13 | }> 14 | searchParams: Promise<{ 15 | [key: string]: string | string[] 16 | }> 17 | } 18 | 19 | export const generateMetadata = ({ params, searchParams }: Args): Promise => 20 | generatePageMetadata({ config, params, searchParams }) 21 | 22 | const NotFound = ({ params, searchParams }: Args) => 23 | NotFoundPage({ config, importMap, params, searchParams }) 24 | 25 | export default NotFound 26 | -------------------------------------------------------------------------------- /admin-search/dev/src/app/(payload)/admin/[[...segments]]/page.tsx: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import type { Metadata } from 'next' 4 | 5 | import config from '@payload-config' 6 | import { generatePageMetadata, RootPage } from '@payloadcms/next/views' 7 | 8 | import { importMap } from '../importMap.js' 9 | 10 | type Args = { 11 | params: Promise<{ 12 | segments: string[] 13 | }> 14 | searchParams: Promise<{ 15 | [key: string]: string | string[] 16 | }> 17 | } 18 | 19 | export const generateMetadata = ({ params, searchParams }: Args): Promise => 20 | generatePageMetadata({ config, params, searchParams }) 21 | 22 | const Page = ({ params, searchParams }: Args) => 23 | RootPage({ config, importMap, params, searchParams }) 24 | 25 | export default Page 26 | -------------------------------------------------------------------------------- /admin-search/dev/src/app/(payload)/api/[...slug]/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import '@payloadcms/next/css' 5 | import { 6 | REST_DELETE, 7 | REST_GET, 8 | REST_OPTIONS, 9 | REST_PATCH, 10 | REST_POST, 11 | REST_PUT, 12 | } from '@payloadcms/next/routes' 13 | 14 | export const GET = REST_GET(config) 15 | export const POST = REST_POST(config) 16 | export const DELETE = REST_DELETE(config) 17 | export const PATCH = REST_PATCH(config) 18 | export const PUT = REST_PUT(config) 19 | export const OPTIONS = REST_OPTIONS(config) 20 | -------------------------------------------------------------------------------- /admin-search/dev/src/app/(payload)/api/graphql-playground/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import '@payloadcms/next/css' 5 | import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes' 6 | 7 | export const GET = GRAPHQL_PLAYGROUND_GET(config) 8 | -------------------------------------------------------------------------------- /admin-search/dev/src/app/(payload)/api/graphql/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes' 5 | 6 | export const POST = GRAPHQL_POST(config) 7 | 8 | export const OPTIONS = REST_OPTIONS(config) 9 | -------------------------------------------------------------------------------- /admin-search/dev/src/app/(payload)/custom.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhb-software/payload-plugins/efb8fdb99155a824327e56c7f42a7712c2796054/admin-search/dev/src/app/(payload)/custom.scss -------------------------------------------------------------------------------- /admin-search/dev/src/app/(payload)/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { ServerFunctionClient } from 'payload' 2 | 3 | import '@payloadcms/next/css' 4 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 5 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 6 | import config from '@payload-config' 7 | import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts' 8 | import React from 'react' 9 | 10 | import { importMap } from './admin/importMap.js' 11 | import './custom.scss' 12 | 13 | type Args = { 14 | children: React.ReactNode 15 | } 16 | 17 | const serverFunction: ServerFunctionClient = async function (args) { 18 | 'use server' 19 | return handleServerFunctions({ 20 | ...args, 21 | config, 22 | importMap, 23 | }) 24 | } 25 | 26 | const Layout = ({ children }: Args) => ( 27 | 28 | {children} 29 | 30 | ) 31 | 32 | export default Layout 33 | -------------------------------------------------------------------------------- /admin-search/dev/src/app/my-route/route.ts: -------------------------------------------------------------------------------- 1 | import configPromise from '@payload-config' 2 | import { getPayload } from 'payload' 3 | 4 | export const GET = async (request: Request) => { 5 | const payload = await getPayload({ 6 | config: configPromise, 7 | }) 8 | 9 | return Response.json({ 10 | message: 'This is an example of a custom route.', 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /admin-search/dev/src/collections/authors.ts: -------------------------------------------------------------------------------- 1 | import type { CollectionConfig } from 'payload' 2 | 3 | export const authorsSchema: CollectionConfig = { 4 | slug: 'authors', 5 | admin: { 6 | useAsTitle: 'name', 7 | }, 8 | fields: [ 9 | { 10 | name: 'name', 11 | type: 'text', 12 | required: true, 13 | }, 14 | { 15 | name: 'bio', 16 | type: 'textarea', 17 | }, 18 | ], 19 | } 20 | -------------------------------------------------------------------------------- /admin-search/dev/src/collections/media.ts: -------------------------------------------------------------------------------- 1 | import type { CollectionConfig } from 'payload' 2 | import path from 'path' 3 | import { fileURLToPath } from 'url' 4 | 5 | const filename = fileURLToPath(import.meta.url) 6 | const dirname = path.dirname(filename) 7 | 8 | export const mediaSchema: CollectionConfig = { 9 | slug: 'media', 10 | fields: [], 11 | upload: { 12 | staticDir: path.resolve(dirname, '../media'), 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /admin-search/dev/src/collections/pages.ts: -------------------------------------------------------------------------------- 1 | import type { CollectionConfig } from 'payload' 2 | 3 | export const pagesSchema: CollectionConfig = { 4 | slug: 'pages', 5 | fields: [ 6 | { 7 | name: 'title', 8 | type: 'text', 9 | required: true, 10 | }, 11 | { 12 | name: 'slug', 13 | type: 'text', 14 | required: true, 15 | unique: true, 16 | index: true, 17 | }, 18 | { 19 | name: 'content', 20 | type: 'richText', 21 | required: false, 22 | }, 23 | ], 24 | } 25 | -------------------------------------------------------------------------------- /admin-search/dev/src/collections/posts.ts: -------------------------------------------------------------------------------- 1 | import type { CollectionConfig, CollectionSlug } from 'payload' 2 | 3 | export const postsSchema: CollectionConfig = { 4 | slug: 'posts', 5 | fields: [ 6 | { 7 | name: 'title', 8 | type: 'text', 9 | required: true, 10 | }, 11 | { 12 | name: 'slug', 13 | type: 'text', 14 | required: true, 15 | unique: true, 16 | index: true, 17 | }, 18 | { 19 | name: 'author', 20 | type: 'relationship', 21 | relationTo: 'authors' as CollectionSlug, 22 | required: true, 23 | }, 24 | { 25 | name: 'content', 26 | type: 'richText', 27 | required: false, 28 | }, 29 | ], 30 | } 31 | -------------------------------------------------------------------------------- /admin-search/dev/src/helpers/credentials.ts: -------------------------------------------------------------------------------- 1 | export const devUser = { 2 | email: 'dev@payloadcms.com', 3 | password: 'test', 4 | } 5 | -------------------------------------------------------------------------------- /admin-search/dev/src/helpers/testEmailAdapter.ts: -------------------------------------------------------------------------------- 1 | import type { EmailAdapter, SendEmailOptions } from 'payload' 2 | 3 | /** 4 | * Logs all emails to stdout 5 | */ 6 | export const testEmailAdapter: EmailAdapter = ({ payload }) => ({ 7 | name: 'test-email-adapter', 8 | defaultFromAddress: 'dev@payloadcms.com', 9 | defaultFromName: 'Payload Test', 10 | sendEmail: async (message) => { 11 | const stringifiedTo = getStringifiedToAddress(message) 12 | const res = `Test email to: '${stringifiedTo}', Subject: '${message.subject}'` 13 | payload.logger.info({ content: message, msg: res }) 14 | return Promise.resolve() 15 | }, 16 | }) 17 | 18 | function getStringifiedToAddress(message: SendEmailOptions): string | undefined { 19 | let stringifiedTo: string | undefined 20 | 21 | if (typeof message.to === 'string') { 22 | stringifiedTo = message.to 23 | } else if (Array.isArray(message.to)) { 24 | stringifiedTo = message.to 25 | .map((to: { address: string } | string) => { 26 | if (typeof to === 'string') { 27 | return to 28 | } else if (to.address) { 29 | return to.address 30 | } 31 | return '' 32 | }) 33 | .join(', ') 34 | } else if (message.to?.address) { 35 | stringifiedTo = message.to.address 36 | } 37 | return stringifiedTo 38 | } 39 | -------------------------------------------------------------------------------- /admin-search/dev/src/payload.config.ts: -------------------------------------------------------------------------------- 1 | import { adminSearchPlugin } from '@jhb.software/payload-admin-search' 2 | import { mongooseAdapter } from '@payloadcms/db-mongodb' 3 | import { searchPlugin } from '@payloadcms/plugin-search' 4 | import { lexicalEditor } from '@payloadcms/richtext-lexical' 5 | import path from 'path' 6 | import { buildConfig } from 'payload' 7 | import { fileURLToPath } from 'url' 8 | 9 | import { authorsSchema } from './collections/authors' 10 | import { mediaSchema } from './collections/media' 11 | import { pagesSchema } from './collections/pages' 12 | import { postsSchema } from './collections/posts' 13 | import { seed } from './seed' 14 | 15 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 16 | 17 | export default buildConfig({ 18 | admin: { 19 | autoLogin: { 20 | email: 'dev@payloadcms.com', 21 | password: 'test', 22 | }, 23 | user: 'users', 24 | }, 25 | collections: [ 26 | pagesSchema, 27 | postsSchema, 28 | authorsSchema, 29 | mediaSchema, 30 | { 31 | slug: 'users', 32 | auth: true, 33 | fields: [], 34 | }, 35 | ], 36 | db: mongooseAdapter({ 37 | url: process.env.DATABASE_URI!, 38 | }), 39 | 40 | editor: lexicalEditor(), 41 | 42 | secret: process.env.PAYLOAD_SECRET || 'secret', 43 | 44 | typescript: { 45 | outputFile: path.resolve(__dirname, '../payload-types.ts'), 46 | }, 47 | 48 | async onInit(payload) { 49 | const existingUsers = await payload.find({ 50 | collection: 'users', 51 | limit: 1, 52 | }) 53 | 54 | if (existingUsers.docs.length === 0) { 55 | await payload.create({ 56 | collection: 'users', 57 | data: { 58 | email: 'dev@payloadcms.com', 59 | password: 'test', 60 | }, 61 | }) 62 | } 63 | 64 | await seed(payload) 65 | }, 66 | 67 | plugins: [ 68 | adminSearchPlugin({ headerSearchComponentStyle: 'bar' }), 69 | searchPlugin({ 70 | beforeSync: ({ originalDoc, searchDoc }) => { 71 | return { 72 | ...searchDoc, 73 | title: searchDoc.doc.relationTo === 'authors' ? originalDoc.name : originalDoc.title, 74 | } 75 | }, 76 | collections: ['pages', 'posts', 'authors'], 77 | }), 78 | ], 79 | }) 80 | -------------------------------------------------------------------------------- /admin-search/dev/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "outDir": "./dist", 10 | "rootDir": ".", 11 | "jsx": "preserve", 12 | "paths": { 13 | "@payload-config": ["./src/payload.config.ts"], 14 | "payload/generated-types": ["./src/payload-types.ts"] 15 | }, 16 | "noEmit": true, 17 | "incremental": true, 18 | "module": "ESNext", 19 | "moduleResolution": "Bundler", 20 | "resolveJsonModule": true, 21 | "isolatedModules": true, 22 | "plugins": [ 23 | { 24 | "name": "next" 25 | } 26 | ] 27 | }, 28 | "include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "vite.config.ts"], 29 | "exclude": ["node_modules", "dist", "build"] 30 | } 31 | -------------------------------------------------------------------------------- /admin-search/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import payloadEsLintConfig from '@payloadcms/eslint-config' 4 | 5 | export const defaultESLintIgnores = [ 6 | '**/.temp', 7 | '**/.*', // ignore all dotfiles 8 | '**/.git', 9 | '**/.hg', 10 | '**/.pnp.*', 11 | '**/.svn', 12 | '**/playwright.config.ts', 13 | '**/vitest.config.js', 14 | '**/tsconfig.tsbuildinfo', 15 | '**/README.md', 16 | '**/eslint.config.js', 17 | '**/payload-types.ts', 18 | '**/dist/', 19 | '**/.yarn/', 20 | '**/build/', 21 | '**/node_modules/', 22 | '**/temp/', 23 | ] 24 | 25 | export default [ 26 | ...payloadEsLintConfig, 27 | { 28 | rules: { 29 | 'no-restricted-exports': 'off', 30 | }, 31 | }, 32 | { 33 | languageOptions: { 34 | parserOptions: { 35 | sourceType: 'module', 36 | ecmaVersion: 'latest', 37 | projectService: { 38 | maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING: 40, 39 | allowDefaultProject: ['scripts/*.ts', '*.js', '*.mjs', '*.spec.ts', '*.d.ts'], 40 | }, 41 | // projectService: true, 42 | tsconfigRootDir: import.meta.dirname, 43 | }, 44 | }, 45 | }, 46 | ] 47 | -------------------------------------------------------------------------------- /admin-search/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - '.' 3 | - 'dev' -------------------------------------------------------------------------------- /admin-search/src/components/SearchBar/SearchBar.css: -------------------------------------------------------------------------------- 1 | .search-bar { 2 | padding: 0; 3 | max-width: 270px; 4 | } 5 | 6 | .search-bar .btn__label { 7 | width: 100%; 8 | } 9 | 10 | .search-bar .shortcut-key, 11 | .search-bar .search-filter__input { 12 | display: inline; 13 | } 14 | 15 | .search-bar__wrap { 16 | display: flex; 17 | width: 100%; 18 | align-items: center; 19 | background-color: var(--theme-elevation-50); 20 | border-radius: var(--style-radius-m); 21 | padding: 6px 12px; 22 | gap: 12px; 23 | } 24 | 25 | .search-bar__wrap svg { 26 | font-weight: 800; 27 | height: 30px; 28 | width: 30px; 29 | } 30 | 31 | .search-bar.position-actions { 32 | margin-right: 100px; 33 | } 34 | 35 | @media (max-width: 780px) { 36 | .search-bar.position-actions { 37 | margin-right: 0px; 38 | } 39 | 40 | .search-bar.position-actions .shortcut-key, 41 | .search-bar.position-actions .search-filter__input { 42 | display: none; 43 | } 44 | } -------------------------------------------------------------------------------- /admin-search/src/components/SearchBar/SearchBar.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import type React from 'react' 3 | 4 | import { Button, Pill, SearchIcon, useHotkey } from '@payloadcms/ui' 5 | import { useEffect, useState } from 'react' 6 | 7 | import { SearchModal } from '../SearchModal/SearchModal.js' 8 | import './SearchBar.css' 9 | 10 | const baseClass = 'search-bar' 11 | 12 | export function SearchBar(): React.ReactElement { 13 | const [isModalOpen, setIsModalOpen] = useState(false) 14 | const [shortcutKey, setShortcutKey] = useState('Ctrl') 15 | 16 | useEffect(() => { 17 | const isMac = typeof window !== 'undefined' && /Mac/i.test(navigator.platform) 18 | setShortcutKey(isMac ? '⌘' : 'Ctrl') 19 | }, []) 20 | 21 | useHotkey( 22 | { 23 | cmdCtrlKey: true, 24 | editDepth: 1, 25 | keyCodes: ['k'], 26 | }, 27 | (e) => { 28 | e.preventDefault() 29 | setIsModalOpen(true) 30 | }, 31 | ) 32 | 33 | return ( 34 | <> 35 | 51 | 52 | {isModalOpen && setIsModalOpen(false)} />} 53 | 54 | ) 55 | } -------------------------------------------------------------------------------- /admin-search/src/components/SearchButton/SearchButton.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import type React from 'react' 3 | 4 | import { Button, SearchIcon, useHotkey } from '@payloadcms/ui' 5 | import { useState } from 'react' 6 | 7 | import { SearchModal } from '../SearchModal/SearchModal.js' 8 | 9 | export function SearchButton(): React.ReactElement { 10 | const [isModalOpen, setIsModalOpen] = useState(false) 11 | 12 | useHotkey( 13 | { 14 | cmdCtrlKey: true, 15 | editDepth: 1, 16 | keyCodes: ['k'], 17 | }, 18 | (e) => { 19 | e.preventDefault() 20 | setIsModalOpen(true) 21 | }, 22 | ) 23 | 24 | return ( 25 | <> 26 | 34 | 35 | {isModalOpen && setIsModalOpen(false)} />} 36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /admin-search/src/components/SearchModal/SearchModalSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface SearchModalSkeletonProps { 4 | count?: number 5 | } 6 | 7 | export const SearchModalSkeleton: React.FC = ({ count = 5 }) => { 8 | return ( 9 |
    10 | {Array.from({ length: count }).map((_, index) => ( 11 |
  • 12 |
    13 |
    14 |
    15 |
    16 |
    17 |
    18 |
  • 19 | ))} 20 |
21 | ) 22 | } -------------------------------------------------------------------------------- /admin-search/src/components/SearchWrapper/SearchWrapper.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import type React from 'react' 3 | 4 | import { SearchBar } from '../SearchBar/SearchBar.js' 5 | import { SearchButton } from '../SearchButton/SearchButton.js' 6 | 7 | interface SearchWrapperProps { 8 | style?: 'bar' | 'button' 9 | } 10 | 11 | export function SearchWrapper({ style = 'button' }: SearchWrapperProps): React.ReactElement { 12 | if (style === 'bar') { 13 | return 14 | } 15 | return 16 | } -------------------------------------------------------------------------------- /admin-search/src/exports/client.ts: -------------------------------------------------------------------------------- 1 | export { SearchBar } from '../components/SearchBar/SearchBar.js' 2 | export { SearchButton } from '../components/SearchButton/SearchButton.js' 3 | export { SearchWrapper } from '../components/SearchWrapper/SearchWrapper.js' 4 | -------------------------------------------------------------------------------- /admin-search/src/index.ts: -------------------------------------------------------------------------------- 1 | export { adminSearchPlugin } from './plugin.js' 2 | export type { AdminSearchPluginConfig } from './types/AdminSearchPluginConfig.js' 3 | export type { SearchResult } from './types/SearchResult.js' 4 | -------------------------------------------------------------------------------- /admin-search/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'payload' 2 | 3 | import type { AdminSearchPluginConfig } from './types/AdminSearchPluginConfig.js' 4 | 5 | export const adminSearchPlugin = 6 | (pluginOptions: AdminSearchPluginConfig) => 7 | (incomingConfig: Config): Config => { 8 | if (pluginOptions.enabled === false) { 9 | return incomingConfig 10 | } 11 | 12 | const { headerSearchComponentStyle = 'button' } = pluginOptions 13 | 14 | return { 15 | ...incomingConfig, 16 | admin: { 17 | ...incomingConfig.admin, 18 | components: { 19 | ...incomingConfig.admin?.components, 20 | actions: [ 21 | ...(incomingConfig.admin?.components?.actions || []), 22 | { 23 | clientProps: { 24 | style: headerSearchComponentStyle, 25 | }, 26 | path: '@jhb.software/payload-admin-search/client#SearchWrapper', 27 | }, 28 | ], 29 | }, 30 | }, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /admin-search/src/types/AdminSearchPluginConfig.ts: -------------------------------------------------------------------------------- 1 | export type AdminSearchPluginConfig = { 2 | enabled?: boolean 3 | headerSearchComponentStyle?: 'bar' | 'button' 4 | } 5 | -------------------------------------------------------------------------------- /admin-search/src/types/SearchResult.ts: -------------------------------------------------------------------------------- 1 | export interface SearchResult { 2 | doc: { 3 | relationTo: string 4 | value: string 5 | } 6 | id: string 7 | title: string 8 | } 9 | -------------------------------------------------------------------------------- /admin-search/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 5 | "rootDir": "./", 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "noImplicitAny": true, 12 | "esModuleInterop": true, 13 | "module": "NodeNext", 14 | "moduleResolution": "nodenext", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "incremental": true, 19 | "emitDeclarationOnly": true, 20 | "target": "ES2022", 21 | "composite": true, 22 | "plugins": [ 23 | { 24 | "name": "next" 25 | } 26 | ] 27 | }, 28 | "include": ["./src/**/*.ts", "./src/**/*.tsx", "./dev/next-env.d.ts"] 29 | } 30 | -------------------------------------------------------------------------------- /cloudinary/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /dist 3 | tsconfig.tsbuildinfo 4 | -------------------------------------------------------------------------------- /cloudinary/.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhb-software/payload-plugins/efb8fdb99155a824327e56c7f42a7712c2796054/cloudinary/.npmignore -------------------------------------------------------------------------------- /cloudinary/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "parser": "typescript", 4 | "semi": false, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "overrides": [ 8 | { 9 | "files": ["**/importMap.js", "**/payload-types.ts", "**/**.yaml"], 10 | "options": { 11 | "requirePragma": true 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /cloudinary/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 JHB Software 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cloudinary/dev/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | /media 4 | node_modules 5 | .DS_Store 6 | .env 7 | -------------------------------------------------------------------------------- /cloudinary/dev/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /cloudinary/dev/next.config.mjs: -------------------------------------------------------------------------------- 1 | import { withPayload } from '@payloadcms/next/withPayload' 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = { 5 | // Your Next.js config here 6 | 7 | // Allow for ESM .js import statements 8 | webpack: (webpackConfig) => { 9 | webpackConfig.resolve.extensionAlias = { 10 | '.cjs': ['.cts', '.cjs'], 11 | '.js': ['.ts', '.tsx', '.js', '.jsx'], 12 | '.mjs': ['.mts', '.mjs'], 13 | } 14 | 15 | return webpackConfig 16 | }, 17 | 18 | redirects: () => [ 19 | { 20 | source: '/', 21 | destination: '/admin', 22 | permanent: true, 23 | }, 24 | ], 25 | } 26 | 27 | export default withPayload(nextConfig) 28 | -------------------------------------------------------------------------------- /cloudinary/dev/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nodemon.json", 3 | "ext": "ts", 4 | "exec": "ts-node src/server.ts -- -I", 5 | "stdin": false 6 | } -------------------------------------------------------------------------------- /cloudinary/dev/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "payload-plugin-test-app", 3 | "description": "A test app for the plugin", 4 | "version": "0.0.1", 5 | "license": "MIT", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "cross-env NODE_OPTIONS=\"${NODE_OPTIONS} --no-deprecation\" next dev", 9 | "devturbo": "pnpm dev --turbo", 10 | "devsafe": "rm -rf .next && pnpm dev", 11 | "build": "cross-env NODE_OPTIONS=--no-deprecation next build", 12 | "start": "cross-env NODE_OPTIONS=--no-deprecation next start", 13 | "format": "prettier --write src", 14 | "payload": "payload", 15 | "generate:types": "payload generate:types", 16 | "generate:schema": "payload-graphql generate:schema", 17 | "generate:importmap": "payload generate:importmap" 18 | }, 19 | "dependencies": { 20 | "@jhb.software/payload-cloudinary-plugin": "workspace:*", 21 | "@payloadcms/db-mongodb": "^3.59.1", 22 | "@payloadcms/next": "^3.59.1", 23 | "next": "15.5.5", 24 | "payload": "^3.59.1", 25 | "react": "19.2.0", 26 | "react-dom": "19.2.0" 27 | }, 28 | "devDependencies": { 29 | "copyfiles": "^2.4.1", 30 | "cross-env": "^10.1.0", 31 | "dotenv": "^17.2.3", 32 | "typescript": "5.9.3" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /cloudinary/dev/plugin.spec.ts: -------------------------------------------------------------------------------- 1 | import type { Server } from 'http' 2 | import mongoose from 'mongoose' 3 | import payload from 'payload' 4 | import { start } from './src/server' 5 | 6 | describe('Plugin tests', () => { 7 | let server: Server 8 | 9 | beforeAll(async () => { 10 | await start({ local: true }) 11 | }) 12 | 13 | afterAll(async () => { 14 | await mongoose.connection.dropDatabase() 15 | await mongoose.connection.close() 16 | server.close() 17 | }) 18 | 19 | // Add tests to ensure that the plugin works as expected 20 | 21 | // Example test to check for seeded data 22 | it('seeds data accordingly', async () => { 23 | const newCollectionQuery = await payload.find({ 24 | collection: 'new-collection', 25 | sort: 'createdAt', 26 | }) 27 | 28 | expect(newCollectionQuery.totalDocs).toEqual(1) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /cloudinary/dev/src/app/(payload)/admin/[[...segments]]/not-found.tsx: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import type { Metadata } from 'next' 4 | 5 | import config from '@payload-config' 6 | import { generatePageMetadata, NotFoundPage } from '@payloadcms/next/views' 7 | 8 | import { importMap } from '../importMap.js' 9 | 10 | type Args = { 11 | params: Promise<{ 12 | segments: string[] 13 | }> 14 | searchParams: Promise<{ 15 | [key: string]: string | string[] 16 | }> 17 | } 18 | 19 | export const generateMetadata = ({ params, searchParams }: Args): Promise => 20 | generatePageMetadata({ config, params, searchParams }) 21 | 22 | const NotFound = ({ params, searchParams }: Args) => 23 | NotFoundPage({ config, importMap, params, searchParams }) 24 | 25 | export default NotFound 26 | -------------------------------------------------------------------------------- /cloudinary/dev/src/app/(payload)/admin/[[...segments]]/page.tsx: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import type { Metadata } from 'next' 4 | 5 | import config from '@payload-config' 6 | import { generatePageMetadata, RootPage } from '@payloadcms/next/views' 7 | 8 | import { importMap } from '../importMap.js' 9 | 10 | type Args = { 11 | params: Promise<{ 12 | segments: string[] 13 | }> 14 | searchParams: Promise<{ 15 | [key: string]: string | string[] 16 | }> 17 | } 18 | 19 | export const generateMetadata = ({ params, searchParams }: Args): Promise => 20 | generatePageMetadata({ config, params, searchParams }) 21 | 22 | const Page = ({ params, searchParams }: Args) => 23 | RootPage({ config, importMap, params, searchParams }) 24 | 25 | export default Page 26 | -------------------------------------------------------------------------------- /cloudinary/dev/src/app/(payload)/admin/importMap.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export const importMap = { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /cloudinary/dev/src/app/(payload)/api/[...slug]/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes' 5 | 6 | export const GET = REST_GET(config) 7 | export const POST = REST_POST(config) 8 | export const DELETE = REST_DELETE(config) 9 | export const PATCH = REST_PATCH(config) 10 | export const OPTIONS = REST_OPTIONS(config) 11 | -------------------------------------------------------------------------------- /cloudinary/dev/src/app/(payload)/api/graphql-playground/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes' 5 | 6 | export const GET = GRAPHQL_PLAYGROUND_GET(config) 7 | -------------------------------------------------------------------------------- /cloudinary/dev/src/app/(payload)/api/graphql/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import { GRAPHQL_POST } from '@payloadcms/next/routes' 5 | 6 | export const POST = GRAPHQL_POST(config) 7 | -------------------------------------------------------------------------------- /cloudinary/dev/src/app/(payload)/custom.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhb-software/payload-plugins/efb8fdb99155a824327e56c7f42a7712c2796054/cloudinary/dev/src/app/(payload)/custom.scss -------------------------------------------------------------------------------- /cloudinary/dev/src/app/(payload)/layout.tsx: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import '@payloadcms/next/css' 5 | import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts' 6 | import type { ServerFunctionClient } from 'payload' 7 | import React from 'react' 8 | 9 | import { importMap } from './admin/importMap.js' 10 | import './custom.scss' 11 | 12 | type Args = { 13 | children: React.ReactNode 14 | } 15 | 16 | const serverFunction: ServerFunctionClient = async function (args) { 17 | 'use server' 18 | return handleServerFunctions({ 19 | ...args, 20 | config, 21 | importMap, 22 | }) 23 | } 24 | 25 | const Layout = ({ children }: Args) => ( 26 | 27 | {children} 28 | 29 | ) 30 | 31 | export default Layout 32 | -------------------------------------------------------------------------------- /cloudinary/dev/src/collections/images.ts: -------------------------------------------------------------------------------- 1 | import { CollectionConfig } from 'payload' 2 | 3 | export const Images: CollectionConfig = { 4 | slug: 'images', 5 | labels: { 6 | singular: 'Image', 7 | plural: 'Images', 8 | }, 9 | upload: { 10 | mimeTypes: ['image/*'], 11 | }, 12 | fields: [ 13 | // The other fields are automatically added by the plugin. 14 | { 15 | name: 'alt', 16 | type: 'text', 17 | }, 18 | ], 19 | } 20 | -------------------------------------------------------------------------------- /cloudinary/dev/src/collections/videos.ts: -------------------------------------------------------------------------------- 1 | import { CollectionConfig } from 'payload' 2 | 3 | export const Videos: CollectionConfig = { 4 | slug: 'videos', 5 | labels: { 6 | singular: 'Video', 7 | plural: 'Videos', 8 | }, 9 | upload: { 10 | mimeTypes: ['video/*'], 11 | }, 12 | fields: [ 13 | // The other fields are automatically added by the plugin. 14 | { 15 | name: 'description', 16 | type: 'textarea', 17 | }, 18 | ], 19 | } 20 | -------------------------------------------------------------------------------- /cloudinary/dev/src/payload.config.ts: -------------------------------------------------------------------------------- 1 | import { payloadCloudinaryPlugin } from '@jhb.software/payload-cloudinary-plugin' 2 | import { mongooseAdapter } from '@payloadcms/db-mongodb' 3 | import path from 'path' 4 | import { buildConfig } from 'payload' 5 | import { fileURLToPath } from 'url' 6 | import { Images } from './collections/images' 7 | import { Videos } from './collections/videos' 8 | 9 | const filename = fileURLToPath(import.meta.url) 10 | const dirname = path.dirname(filename) 11 | 12 | export default buildConfig({ 13 | admin: { 14 | autoLogin: { 15 | email: 'dev@payloadcms.com', 16 | password: 'test', 17 | }, 18 | user: 'users', 19 | }, 20 | collections: [ 21 | Videos, 22 | Images, 23 | { 24 | slug: 'users', 25 | auth: true, 26 | fields: [], 27 | }, 28 | ], 29 | db: mongooseAdapter({ 30 | url: process.env.DATABASE_URI!, 31 | }), 32 | secret: process.env.PAYLOAD_SECRET!, 33 | typescript: { 34 | outputFile: path.resolve(dirname, 'payload-types.ts'), 35 | }, 36 | plugins: [ 37 | payloadCloudinaryPlugin({ 38 | credentials: { 39 | apiKey: process.env.CLOUDINARY_API_KEY!, 40 | apiSecret: process.env.CLOUDINARY_API_SECRET!, 41 | }, 42 | cloudinary: { 43 | cloudName: process.env.CLOUDINARY_CLOUD_NAME!, 44 | folder: 'payload-cloudinary-plugin', 45 | }, 46 | uploadCollections: ['images', 'videos'], 47 | uploadOptions: { 48 | useFilename: true, 49 | }, 50 | }), 51 | ], 52 | async onInit(payload) { 53 | const existingUsers = await payload.find({ 54 | collection: 'users', 55 | limit: 1, 56 | }) 57 | 58 | if (existingUsers.docs.length === 0) { 59 | await payload.create({ 60 | collection: 'users', 61 | data: { 62 | email: 'dev@payloadcms.com', 63 | password: 'test', 64 | }, 65 | }) 66 | } 67 | }, 68 | }) 69 | -------------------------------------------------------------------------------- /cloudinary/dev/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "outDir": "./dist", 10 | "rootDir": ".", 11 | "jsx": "preserve", 12 | "paths": { 13 | "@payload-config": ["./src/payload.config.ts"], 14 | "payload/generated-types": ["./src/payload-types.ts"] 15 | }, 16 | "noEmit": true, 17 | "incremental": true, 18 | "module": "ESNext", 19 | "moduleResolution": "Bundler", 20 | "resolveJsonModule": true, 21 | "isolatedModules": true, 22 | "plugins": [ 23 | { 24 | "name": "next" 25 | } 26 | ] 27 | }, 28 | "include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 29 | "exclude": ["node_modules", "dist", "build"] 30 | } 31 | -------------------------------------------------------------------------------- /cloudinary/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jhb.software/payload-cloudinary-plugin", 3 | "version": "0.2.2", 4 | "description": "Payload CMS plugin that adds Cloudinary image upload and management capabilities to media collections.", 5 | "bugs": "https://github.com/jhb-software/payload-plugins/issues", 6 | "repository": "https://github.com/jhb-software/payload-plugins", 7 | "keywords": [ 8 | "payload", 9 | "plugin", 10 | "cloudinary" 11 | ], 12 | "author": "JHB Software", 13 | "license": "MIT", 14 | "type": "module", 15 | "main": "dist/index.js", 16 | "types": "dist/index.d.ts", 17 | "scripts": { 18 | "build": "tsc", 19 | "dev": "tsc -w", 20 | "format": "prettier --write src", 21 | "test": "jest", 22 | "lint": "eslint .", 23 | "prepublish": "tsc" 24 | }, 25 | "dependencies": { 26 | "cloudinary": "^2.7.0" 27 | }, 28 | "peerDependencies": { 29 | "payload": "^3.59.1" 30 | }, 31 | "devDependencies": { 32 | "prettier": "^3.6.2", 33 | "typescript": "5.9.3" 34 | }, 35 | "files": [ 36 | "dist" 37 | ], 38 | "publishConfig": { 39 | "main": "./dist/index.js", 40 | "registry": "https://registry.npmjs.org/", 41 | "types": "./dist/index.d.ts", 42 | "access": "public", 43 | "exports": { 44 | ".": { 45 | "import": "./dist/index.js", 46 | "types": "./dist/index.d.ts", 47 | "default": "./dist/index.js" 48 | } 49 | } 50 | }, 51 | "exports": { 52 | ".": { 53 | "import": "./src/index.ts", 54 | "default": "./src/index.ts", 55 | "types": "./src/index.ts" 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /cloudinary/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - './' # Plugin at the root 3 | - 'dev/' # Testing environment 4 | -------------------------------------------------------------------------------- /cloudinary/src/hooks/afterDelete.ts: -------------------------------------------------------------------------------- 1 | import { APIError, CollectionAfterDeleteHook } from 'payload' 2 | import { v2 as cloudinary } from 'cloudinary' 3 | 4 | /** Hooks which deletes the file from Cloudinary */ 5 | const afterDeleteHook: CollectionAfterDeleteHook = async ({ doc }) => { 6 | type ReturnType = { 7 | result?: 'ok' | 'not-found' | any 8 | } 9 | 10 | let resource_type: 'video' | 'image' | 'raw' | undefined = undefined 11 | if (doc.mimeType?.startsWith('video/')) { 12 | // for videos, the resource_type must explicitly be set to 'video', otherwise Cloudinary will not find the file 13 | resource_type = 'video' 14 | } 15 | 16 | const result = (await cloudinary.uploader.destroy(doc.cloudinaryPublicId, { 17 | resource_type: resource_type, 18 | })) as ReturnType 19 | 20 | if (result?.result === 'ok') { 21 | return doc 22 | } else if (result?.result === 'not found') { 23 | throw new APIError( 24 | 'File to delete not found in Cloudinary', // message 25 | 500, // status 26 | result, // data 27 | true, // isPublic 28 | ) 29 | } else { 30 | throw new APIError( 31 | 'Error deleting file from Cloudinary', // message 32 | 500, // status 33 | result, // data 34 | true, // isPublic 35 | ) 36 | } 37 | } 38 | 39 | export default afterDeleteHook 40 | -------------------------------------------------------------------------------- /cloudinary/src/hooks/beforeChange.ts: -------------------------------------------------------------------------------- 1 | import { CollectionBeforeChangeHook } from 'payload' 2 | import { CloudinaryPluginConfig } from '../types/CloudinaryPluginConfig' 3 | import { streamUpload } from '../utils/streamUpload' 4 | 5 | const beforeChangeHook = (pluginConfig: CloudinaryPluginConfig) => { 6 | const hook: CollectionBeforeChangeHook = async ({ data, req, operation }) => { 7 | if (operation === 'create' && !req.file) { 8 | console.error('req.file undefined. Returning data unchanged.') 9 | return data 10 | } 11 | 12 | // The image of a media document can be updated via the admin UI. Therefore also check for operation === 'update'. 13 | if ((operation === 'create' || operation === 'update') && req.file) { 14 | const streamUploadFunction = streamUpload(pluginConfig) 15 | 16 | const result = await streamUploadFunction(req.file, data.cloudinaryPublicId) 17 | 18 | return { 19 | ...data, 20 | cloudinaryPublicId: result.public_id, 21 | cloudinaryURL: result.secure_url, 22 | } 23 | } 24 | 25 | return data 26 | } 27 | return hook 28 | } 29 | 30 | export default beforeChangeHook 31 | -------------------------------------------------------------------------------- /cloudinary/src/index.ts: -------------------------------------------------------------------------------- 1 | export { payloadCloudinaryPlugin } from './plugin' 2 | export type { CloudinaryPluginConfig } from './types/CloudinaryPluginConfig' 3 | -------------------------------------------------------------------------------- /cloudinary/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import { v2 as cloudinary } from 'cloudinary' 2 | import type { Config } from 'payload' 3 | 4 | import type { CloudinaryPluginConfig } from './types/CloudinaryPluginConfig' 5 | import { extendUploadCollectionConfig } from './utils/extendUploadCollectionConfig' 6 | 7 | /** Payload plugin which integrates cloudinary for hosting media collection items. */ 8 | export const payloadCloudinaryPlugin = 9 | (pluginOptions: CloudinaryPluginConfig) => 10 | (incomingConfig: Config): Config => { 11 | // If the plugin is disabled, return the config without modifying it 12 | if (pluginOptions.enabled === false) { 13 | return incomingConfig 14 | } 15 | 16 | cloudinary.config({ 17 | cloud_name: pluginOptions.cloudinary.cloudName, 18 | api_key: pluginOptions.credentials.apiKey, 19 | api_secret: pluginOptions.credentials.apiSecret, 20 | }) 21 | 22 | return { 23 | ...incomingConfig, 24 | collections: incomingConfig.collections?.map((collection) => { 25 | if (pluginOptions.uploadCollections?.includes(collection.slug)) { 26 | return extendUploadCollectionConfig({ 27 | config: collection, 28 | pluginConfig: pluginOptions, 29 | }) 30 | } 31 | return collection 32 | }), 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /cloudinary/src/types/CloudinaryPluginConfig.ts: -------------------------------------------------------------------------------- 1 | import { CollectionSlug } from 'payload' 2 | 3 | /** Configuration options for the cloudinary plugin. */ 4 | export type CloudinaryPluginConfig = { 5 | /** Whether the cloudinary plugin is enabled. */ 6 | enabled?: boolean 7 | 8 | /** 9 | * Upload collections which should be integrated with cloudinary. 10 | */ 11 | uploadCollections?: ({} | CollectionSlug)[] 12 | 13 | uploadOptions?: { 14 | /** Whether cloudinary will use the file name of the uploaded image for the public ID. Defaults to false. */ 15 | useFilename?: boolean 16 | 17 | /** 18 | * The size of the chunks to upload the file in. 19 | * See https://cloudinary.com/documentation/upload_images#chunked_asset_upload 20 | */ 21 | chunkSize?: number 22 | } 23 | 24 | /** 25 | * API credentials for the cloudinary account. 26 | */ 27 | credentials: { 28 | apiKey: string 29 | apiSecret: string 30 | } 31 | 32 | /** 33 | * General cloudinary configuration. 34 | */ 35 | cloudinary: { 36 | cloudName: string 37 | folder?: string 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /cloudinary/src/utils/streamUpload.ts: -------------------------------------------------------------------------------- 1 | import { v2 as cloudinary, UploadApiOptions, UploadApiResponse, UploadStream } from 'cloudinary' 2 | import type { PayloadRequest } from 'payload' 3 | import { Readable } from 'stream' 4 | import { CloudinaryPluginConfig } from '../types/CloudinaryPluginConfig' 5 | 6 | type File = NonNullable 7 | 8 | export const streamUpload = 9 | (pluginConfig: CloudinaryPluginConfig) => 10 | (file: File, id?: string): Promise => { 11 | const readStream = Readable.from(file.data) 12 | 13 | return new Promise((resolve, reject) => { 14 | const options: UploadApiOptions = { 15 | // @ts-ignore 16 | folder: pluginConfig.cloudinary.folder, 17 | chunk_size: pluginConfig.uploadOptions?.chunkSize, 18 | ...(pluginConfig.uploadOptions?.useFilename && { 19 | use_filename: true, 20 | filename_override: file.name, 21 | }), 22 | 23 | invalidate: true, 24 | resource_type: 'auto', 25 | 26 | // In case of updating the image, the public_id will be needed, 27 | // but not the folder as it's already in the URL and if we pass 28 | // the value then it will create file in sub-folder instead of updating. 29 | ...(id && { public_id: id, folder: null }), 30 | } 31 | 32 | const stream: UploadStream = cloudinary.uploader.upload_stream(options, (error, result) => 33 | result ? resolve(result) : reject(error), 34 | ) 35 | 36 | readStream.pipe(stream) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /cloudinary/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "noEmit": false, 5 | "emitDeclarationOnly": false, 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "declaration": true, 9 | "declarationMap": true, 10 | "allowJs": true, 11 | "checkJs": false, 12 | "esModuleInterop": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "jsx": "preserve", 15 | "lib": ["dom", "dom.iterable", "esnext"], 16 | "skipLibCheck": true, 17 | "moduleResolution": "Bundler", 18 | "module": "ES6", 19 | "sourceMap": true, 20 | "strict": true, 21 | "incremental": true, 22 | "isolatedModules": true, 23 | "target": "ESNext" 24 | }, 25 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"], 26 | "exclude": ["dist", "build", "node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /geocoding/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | max_line_length = null 11 | -------------------------------------------------------------------------------- /geocoding/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "parser": "typescript", 4 | "semi": false, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "overrides": [ 8 | { 9 | "files": ["**/importMap.js", "**/payload-types.ts", "**/**.yaml"], 10 | "options": { 11 | "requirePragma": true 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /geocoding/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "npm.packageManager": "pnpm", 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "[typescript]": { 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "editor.formatOnSave": true, 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll.eslint": "explicit" 9 | } 10 | }, 11 | "[typescriptreact]": { 12 | "editor.defaultFormatter": "esbenp.prettier-vscode", 13 | "editor.formatOnSave": true, 14 | "editor.codeActionsOnSave": { 15 | "source.fixAll.eslint": "explicit" 16 | } 17 | }, 18 | "[javascript]": { 19 | "editor.defaultFormatter": "esbenp.prettier-vscode", 20 | "editor.formatOnSave": true, 21 | "editor.codeActionsOnSave": { 22 | "source.fixAll.eslint": "explicit" 23 | } 24 | }, 25 | "[json]": { 26 | "editor.defaultFormatter": "esbenp.prettier-vscode", 27 | "editor.formatOnSave": true 28 | }, 29 | "[jsonc]": { 30 | "editor.defaultFormatter": "esbenp.prettier-vscode", 31 | "editor.formatOnSave": true 32 | }, 33 | "editor.formatOnSaveMode": "file", 34 | "eslint.rules.customizations": [ 35 | // Default all ESLint errors to 'warn' to differentate from TypeScript's 'error' level 36 | { 37 | "rule": "*", 38 | "severity": "warn" 39 | }, 40 | // Silence some warnings that will get auto-fixed 41 | { 42 | "rule": "perfectionist/*", 43 | "severity": "off", 44 | "fixable": true 45 | }, 46 | { 47 | "rule": "curly", 48 | "severity": "off", 49 | "fixable": true 50 | }, 51 | { 52 | "rule": "object-shorthand", 53 | "severity": "off", 54 | "fixable": true 55 | } 56 | ], 57 | } 58 | -------------------------------------------------------------------------------- /geocoding/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.1.6 4 | 5 | - fix: display asterisk for required geodata fields in UI ([87413ba](https://github.com/jhb-software/payload-plugins/commit/87413bac8c3f03ec257ac47de979413930816ee8)) 6 | 7 | ## 0.1.0 8 | 9 | - Initial release 10 | -------------------------------------------------------------------------------- /geocoding/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 JHB Software 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /geocoding/dev/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | /media 4 | node_modules 5 | .DS_Store 6 | .env 7 | -------------------------------------------------------------------------------- /geocoding/dev/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /geocoding/dev/next.config.mjs: -------------------------------------------------------------------------------- 1 | import { withPayload } from '@payloadcms/next/withPayload' 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = { 5 | // Your Next.js config here 6 | 7 | // Allow for ESM .js import statements 8 | webpack: (webpackConfig) => { 9 | webpackConfig.resolve.extensionAlias = { 10 | '.cjs': ['.cts', '.cjs'], 11 | '.js': ['.ts', '.tsx', '.js', '.jsx'], 12 | '.mjs': ['.mts', '.mjs'], 13 | } 14 | 15 | return webpackConfig 16 | }, 17 | 18 | redirects: () => [ 19 | { 20 | source: '/', 21 | destination: '/admin', 22 | permanent: true, 23 | }, 24 | ], 25 | } 26 | 27 | export default withPayload(nextConfig) 28 | -------------------------------------------------------------------------------- /geocoding/dev/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nodemon.json", 3 | "ext": "ts", 4 | "exec": "ts-node src/server.ts -- -I", 5 | "stdin": false 6 | } -------------------------------------------------------------------------------- /geocoding/dev/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "payload-plugin-test-app", 3 | "description": "A test app for the plugin", 4 | "version": "0.0.1", 5 | "license": "MIT", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "cross-env NODE_OPTIONS=\"${NODE_OPTIONS} --no-deprecation\" next dev", 9 | "devturbo": "pnpm dev --turbo", 10 | "devsafe": "rm -rf .next && pnpm dev", 11 | "build": "cross-env NODE_OPTIONS=--no-deprecation next build", 12 | "start": "cross-env NODE_OPTIONS=--no-deprecation next start", 13 | "format": "prettier --write src", 14 | "payload": "payload", 15 | "generate:types": "payload generate:types", 16 | "generate:schema": "payload-graphql generate:schema", 17 | "generate:importmap": "payload generate:importmap" 18 | }, 19 | "dependencies": { 20 | "@payloadcms/db-mongodb": "^3.27.0", 21 | "@payloadcms/next": "^3.27.0", 22 | "@payloadcms/ui": "^3.27.0", 23 | "next": "15.2.1", 24 | "payload": "^3.27.0", 25 | "@jhb.software/payload-geocoding-plugin": "workspace:*", 26 | "react": "19.0.0", 27 | "react-dom": "19.0.0" 28 | }, 29 | "devDependencies": { 30 | "copyfiles": "^2.4.1", 31 | "cross-env": "^7.0.3", 32 | "dotenv": "^16.4.7", 33 | "typescript": "5.8.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /geocoding/dev/plugin.spec.ts: -------------------------------------------------------------------------------- 1 | import type { Server } from 'http' 2 | import mongoose from 'mongoose' 3 | import payload from 'payload' 4 | import { start } from './src/server' 5 | 6 | describe('Plugin tests', () => { 7 | let server: Server 8 | 9 | beforeAll(async () => { 10 | await start({ local: true }) 11 | }) 12 | 13 | afterAll(async () => { 14 | await mongoose.connection.dropDatabase() 15 | await mongoose.connection.close() 16 | server.close() 17 | }) 18 | 19 | // Add tests to ensure that the plugin works as expected 20 | 21 | // Example test to check for seeded data 22 | it('seeds data accordingly', async () => { 23 | const newCollectionQuery = await payload.find({ 24 | collection: 'new-collection', 25 | sort: 'createdAt', 26 | }) 27 | 28 | expect(newCollectionQuery.totalDocs).toEqual(1) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /geocoding/dev/src/app/(payload)/admin/[[...segments]]/not-found.tsx: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import type { Metadata } from 'next' 4 | 5 | import config from '@payload-config' 6 | import { generatePageMetadata, NotFoundPage } from '@payloadcms/next/views' 7 | 8 | import { importMap } from '../importMap.js' 9 | 10 | type Args = { 11 | params: Promise<{ 12 | segments: string[] 13 | }> 14 | searchParams: Promise<{ 15 | [key: string]: string | string[] 16 | }> 17 | } 18 | 19 | export const generateMetadata = ({ params, searchParams }: Args): Promise => 20 | generatePageMetadata({ config, params, searchParams }) 21 | 22 | const NotFound = ({ params, searchParams }: Args) => 23 | NotFoundPage({ config, importMap, params, searchParams }) 24 | 25 | export default NotFound 26 | -------------------------------------------------------------------------------- /geocoding/dev/src/app/(payload)/admin/[[...segments]]/page.tsx: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import type { Metadata } from 'next' 4 | 5 | import config from '@payload-config' 6 | import { generatePageMetadata, RootPage } from '@payloadcms/next/views' 7 | 8 | import { importMap } from '../importMap.js' 9 | 10 | type Args = { 11 | params: Promise<{ 12 | segments: string[] 13 | }> 14 | searchParams: Promise<{ 15 | [key: string]: string | string[] 16 | }> 17 | } 18 | 19 | export const generateMetadata = ({ params, searchParams }: Args): Promise => 20 | generatePageMetadata({ config, params, searchParams }) 21 | 22 | const Page = ({ params, searchParams }: Args) => 23 | RootPage({ config, importMap, params, searchParams }) 24 | 25 | export default Page 26 | -------------------------------------------------------------------------------- /geocoding/dev/src/app/(payload)/admin/importMap.js: -------------------------------------------------------------------------------- 1 | import { GeocodingFieldComponent as GeocodingFieldComponent_f511c7a8656c9889db9f167d5c72bd78 } from '@jhb.software/payload-geocoding-plugin/client' 2 | 3 | export const importMap = { 4 | "@jhb.software/payload-geocoding-plugin/client#GeocodingFieldComponent": GeocodingFieldComponent_f511c7a8656c9889db9f167d5c72bd78 5 | } 6 | -------------------------------------------------------------------------------- /geocoding/dev/src/app/(payload)/api/[...slug]/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes' 5 | 6 | export const GET = REST_GET(config) 7 | export const POST = REST_POST(config) 8 | export const DELETE = REST_DELETE(config) 9 | export const PATCH = REST_PATCH(config) 10 | export const OPTIONS = REST_OPTIONS(config) 11 | -------------------------------------------------------------------------------- /geocoding/dev/src/app/(payload)/api/graphql-playground/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes' 5 | 6 | export const GET = GRAPHQL_PLAYGROUND_GET(config) 7 | -------------------------------------------------------------------------------- /geocoding/dev/src/app/(payload)/api/graphql/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import { GRAPHQL_POST } from '@payloadcms/next/routes' 5 | 6 | export const POST = GRAPHQL_POST(config) 7 | -------------------------------------------------------------------------------- /geocoding/dev/src/app/(payload)/custom.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhb-software/payload-plugins/efb8fdb99155a824327e56c7f42a7712c2796054/geocoding/dev/src/app/(payload)/custom.scss -------------------------------------------------------------------------------- /geocoding/dev/src/app/(payload)/layout.tsx: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import '@payloadcms/next/css' 5 | import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts' 6 | import type { ServerFunctionClient } from 'payload' 7 | import React from 'react' 8 | 9 | import { importMap } from './admin/importMap.js' 10 | import './custom.scss' 11 | 12 | type Args = { 13 | children: React.ReactNode 14 | } 15 | 16 | const serverFunction: ServerFunctionClient = async function (args) { 17 | 'use server' 18 | return handleServerFunctions({ 19 | ...args, 20 | config, 21 | importMap, 22 | }) 23 | } 24 | 25 | const Layout = ({ children }: Args) => ( 26 | 27 | {children} 28 | 29 | ) 30 | 31 | export default Layout 32 | -------------------------------------------------------------------------------- /geocoding/dev/src/emailAdapter.ts: -------------------------------------------------------------------------------- 1 | import type { EmailAdapter, SendEmailOptions } from 'payload' 2 | 3 | /** 4 | * Logs all emails to stdout 5 | */ 6 | export const testEmailAdapter: EmailAdapter = ({ payload }) => ({ 7 | name: 'test-email-adapter', 8 | defaultFromAddress: 'dev@payloadcms.com', 9 | defaultFromName: 'Payload Test', 10 | sendEmail: async (message) => { 11 | const stringifiedTo = getStringifiedToAddress(message) 12 | const res = `Test email to: '${stringifiedTo}', Subject: '${message.subject}'` 13 | payload.logger.info({ msg: res, content: message }) 14 | return Promise.resolve() 15 | }, 16 | }) 17 | 18 | function getStringifiedToAddress(message: SendEmailOptions): string | undefined { 19 | let stringifiedTo: string | undefined 20 | 21 | if (typeof message.to === 'string') { 22 | stringifiedTo = message.to 23 | } else if (Array.isArray(message.to)) { 24 | stringifiedTo = message.to 25 | .map((to: string | { address: string }) => { 26 | if (typeof to === 'string') { 27 | return to 28 | } else if (to.address) { 29 | return to.address 30 | } 31 | return '' 32 | }) 33 | .join(', ') 34 | } else if (message.to.address) { 35 | stringifiedTo = message.to.address 36 | } 37 | return stringifiedTo 38 | } 39 | -------------------------------------------------------------------------------- /geocoding/dev/src/payload.config.ts: -------------------------------------------------------------------------------- 1 | import { payloadGeocodingPlugin } from '@jhb.software/payload-geocoding-plugin' 2 | import { mongooseAdapter } from '@payloadcms/db-mongodb' 3 | import path from 'path' 4 | import { buildConfig } from 'payload' 5 | import { fileURLToPath } from 'url' 6 | import { Pages } from './collection/pages' 7 | import { testEmailAdapter } from './emailAdapter' 8 | 9 | const filename = fileURLToPath(import.meta.url) 10 | const dirname = path.dirname(filename) 11 | 12 | export default buildConfig({ 13 | admin: { 14 | autoLogin: { 15 | email: 'dev@payloadcms.com', 16 | password: 'test', 17 | }, 18 | user: 'users', 19 | }, 20 | collections: [ 21 | { 22 | slug: 'users', 23 | auth: true, 24 | fields: [], 25 | }, 26 | Pages, 27 | ], 28 | db: mongooseAdapter({ 29 | url: process.env.DATABASE_URI!, 30 | }), 31 | email: testEmailAdapter, 32 | secret: process.env.PAYLOAD_SECRET!, 33 | typescript: { 34 | outputFile: path.resolve(dirname, 'payload-types.ts'), 35 | }, 36 | plugins: [payloadGeocodingPlugin({})], 37 | async onInit(payload) { 38 | const existingUsers = await payload.find({ 39 | collection: 'users', 40 | limit: 1, 41 | }) 42 | 43 | if (existingUsers.docs.length === 0) { 44 | await payload.create({ 45 | collection: 'users', 46 | data: { 47 | email: 'dev@payloadcms.com', 48 | password: 'test', 49 | }, 50 | }) 51 | } 52 | }, 53 | }) 54 | -------------------------------------------------------------------------------- /geocoding/dev/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "outDir": "./dist", 10 | "rootDir": ".", 11 | "jsx": "preserve", 12 | "paths": { 13 | "@payload-config": ["./src/payload.config.ts"], 14 | "payload/generated-types": ["./src/payload-types.ts"] 15 | }, 16 | "noEmit": true, 17 | "incremental": true, 18 | "module": "ESNext", 19 | "moduleResolution": "Bundler", 20 | "resolveJsonModule": true, 21 | "isolatedModules": true, 22 | "plugins": [ 23 | { 24 | "name": "next" 25 | } 26 | ] 27 | }, 28 | "include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 29 | "exclude": ["node_modules", "dist", "build"] 30 | } 31 | -------------------------------------------------------------------------------- /geocoding/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import payloadEsLintConfig from '@payloadcms/eslint-config' 4 | 5 | export const defaultESLintIgnores = [ 6 | '**/.temp', 7 | '**/.*', // ignore all dotfiles 8 | '**/.git', 9 | '**/.hg', 10 | '**/.pnp.*', 11 | '**/.svn', 12 | '**/playwright.config.ts', 13 | '**/jest.config.js', 14 | '**/tsconfig.tsbuildinfo', 15 | '**/README.md', 16 | '**/eslint.config.js', 17 | '**/payload-types.ts', 18 | '**/dist/', 19 | '**/.yarn/', 20 | '**/build/', 21 | '**/node_modules/', 22 | '**/temp/', 23 | ] 24 | 25 | export default [ 26 | ...payloadEsLintConfig, 27 | { 28 | // Modify any rules from payload's config that you want to override/disable 29 | 'no-restricted-exports': 'off', 30 | }, 31 | // TODO: Bring in '@payloadcms/eslint-plugin' for 'payload/proper-payload-logger-usage' rule 32 | { 33 | ignores: defaultESLintIgnores, 34 | }, 35 | { 36 | languageOptions: { 37 | parserOptions: { 38 | sourceType: 'module', 39 | ecmaVersion: 'latest', 40 | projectService: { 41 | maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING: 40, 42 | allowDefaultProject: ['scripts/*.ts', '*.js', '*.mjs', '*.spec.ts', '*.d.ts'], 43 | }, 44 | // projectService: true, 45 | tsconfigRootDir: import.meta.dirname, 46 | }, 47 | }, 48 | }, 49 | ] 50 | -------------------------------------------------------------------------------- /geocoding/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - './' # Plugin at the root 3 | - 'dev/' # Testing environment 4 | -------------------------------------------------------------------------------- /geocoding/src/components/GeocodingFieldComponent.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { FieldError, FieldLabel, useField } from '@payloadcms/ui' 3 | import { SelectFieldClientComponent } from 'payload' 4 | import GooglePlacesAutocomplete, { 5 | geocodeByPlaceId, 6 | getLatLng, 7 | } from 'react-google-places-autocomplete' 8 | 9 | /** 10 | * A custom client component that shows the Google Places Autocomplete component and 11 | * fills the point and geodata fields with the received data from the Google Places API. 12 | */ 13 | export const GeocodingFieldComponent: SelectFieldClientComponent = ({ field, path }) => { 14 | const API_KEY = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY 15 | const pointFieldPath = path.replace('_googlePlacesData', '') 16 | 17 | const { value: geoData, setValue: setGeoData } = useField({ 18 | path: path, 19 | }) 20 | const { setValue: setPoint } = useField>({ path: pointFieldPath }) 21 | 22 | return ( 23 |
24 |
25 | 26 | 27 |
28 | { 34 | if (geoData) { 35 | const placeId = geoData?.value.place_id 36 | const geocode = (await geocodeByPlaceId(placeId)).at(0) 37 | 38 | if (!geocode) return 39 | const latLng = await getLatLng(geocode) 40 | 41 | setPoint([latLng.lng, latLng.lat]) 42 | setGeoData(geoData) 43 | } else { 44 | // reset the fields when it was cleared 45 | setPoint([]) 46 | setGeoData(null) 47 | } 48 | }, 49 | }} 50 | /> 51 |
52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /geocoding/src/exports/client.ts: -------------------------------------------------------------------------------- 1 | export { GeocodingFieldComponent } from '../components/GeocodingFieldComponent' 2 | -------------------------------------------------------------------------------- /geocoding/src/fields/geocodingField.ts: -------------------------------------------------------------------------------- 1 | import { Field } from 'payload' 2 | import { GeoCodingFieldConfig } from '../types/GeoCodingFieldConfig' 3 | 4 | /** 5 | * Creates a row field containing: 6 | * 1. The provided point field for storing the coordinates from the Google Places API 7 | * 2. A JSON field that stores the raw Google Places API geocoding data 8 | */ 9 | export const geocodingField = (config: GeoCodingFieldConfig): Field => { 10 | return { 11 | type: 'row', 12 | admin: { 13 | position: config.pointField.admin?.position ?? undefined, 14 | }, 15 | fields: [ 16 | { 17 | name: config.pointField.name + '_googlePlacesData', 18 | type: 'json', 19 | label: config.geoDataFieldOverride?.label ?? 'Location', 20 | access: config.geoDataFieldOverride?.access ?? {}, 21 | required: config.geoDataFieldOverride?.required, 22 | admin: { 23 | // overridable props: 24 | readOnly: true, 25 | 26 | ...config.geoDataFieldOverride?.admin, 27 | 28 | // non-overridable props: 29 | components: { 30 | Field: '@jhb.software/payload-geocoding-plugin/client#GeocodingFieldComponent', 31 | }, 32 | }, 33 | }, 34 | config.pointField, 35 | ], 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /geocoding/src/index.ts: -------------------------------------------------------------------------------- 1 | export { geocodingField } from './fields/geocodingField' 2 | export { payloadGeocodingPlugin } from './plugin.js' 3 | export type { GeocodingPluginConfig } from './types/GeoCodingPluginConfig' 4 | export type { GeoCodingFieldConfig } from './types/GeoCodingFieldConfig' 5 | -------------------------------------------------------------------------------- /geocoding/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'payload' 2 | 3 | import type { GeocodingPluginConfig } from './types/GeoCodingPluginConfig' 4 | 5 | /** Payload plugin which extends the point field with geocoding functionality. */ 6 | export const payloadGeocodingPlugin = 7 | (pluginOptions: GeocodingPluginConfig) => 8 | (incomingConfig: Config): Config => { 9 | const config = { ...incomingConfig } 10 | 11 | // If the plugin is disabled, return the config without modifying it 12 | if (pluginOptions.enabled === false) { 13 | return config 14 | } 15 | 16 | config.onInit = async (payload) => { 17 | if (incomingConfig.onInit) { 18 | await incomingConfig.onInit(payload) 19 | } 20 | 21 | const neededEnvVars = ['NEXT_PUBLIC_GOOGLE_MAPS_API_KEY'] 22 | 23 | const missingEnvVars = neededEnvVars.filter((envVar) => !process.env[envVar]) 24 | if (missingEnvVars.length > 0) { 25 | throw new Error( 26 | `The following environment variables are required for the geocoding plugin but not defined: ${missingEnvVars.join( 27 | ', ', 28 | )}`, 29 | ) 30 | } 31 | } 32 | 33 | return config 34 | } 35 | -------------------------------------------------------------------------------- /geocoding/src/types/GeoCodingFieldConfig.ts: -------------------------------------------------------------------------------- 1 | import { JSONField, PointField } from 'payload' 2 | 3 | /** Configuration for the geocoding fields. */ 4 | export type GeoCodingFieldConfig = { 5 | pointField: PointField 6 | geoDataFieldOverride?: { 7 | required?: boolean 8 | label?: string 9 | access?: JSONField['access'] 10 | admin?: JSONField['admin'] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /geocoding/src/types/GeoCodingPluginConfig.ts: -------------------------------------------------------------------------------- 1 | /** Configuration options for the geocoding plugin. */ 2 | export type GeocodingPluginConfig = { 3 | /** Whether the geocoding plugin is enabled. */ 4 | enabled?: boolean 5 | } 6 | -------------------------------------------------------------------------------- /geocoding/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "declarationDir": "./dist", 5 | "outDir": "./dist", 6 | "rootDir": "./src", 7 | "lib": ["dom", "dom.iterable", "esnext"], 8 | "target": "es5", 9 | "allowJs": true, 10 | "jsx": "preserve", 11 | "module": "ESNext", 12 | "moduleResolution": "Bundler", 13 | "sourceMap": true, 14 | "esModuleInterop": true, 15 | "declaration": true, 16 | "skipLibCheck": true, 17 | "strict": true 18 | }, 19 | "include": ["src/**/*"] 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "payload-plugins", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build:all": "pnpm build:cloudinary && pnpm build:pages && pnpm build:seo && pnpm build:geocoding", 7 | "publish:all": "pnpm publish:cloudinary && pnpm publish:pages && pnpm publish:seo && pnpm publish:geocoding", 8 | "build:cloudinary": "cd ./cloudinary && pnpm build", 9 | "build:pages": "cd ./pages && pnpm build", 10 | "build:seo": "cd ./seo && pnpm build", 11 | "publish:cloudinary": "cd ./cloudinary && pnpm publish", 12 | "publish:pages": "cd ./pages && pnpm publish", 13 | "publish:seo": "cd ./seo && pnpm publish", 14 | "build:geocoding": "cd ./geocoding && pnpm build", 15 | "publish:geocoding": "cd ./geocoding && pnpm publish" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /pages/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /dist 3 | tsconfig.tsbuildinfo 4 | .next 5 | *.db -------------------------------------------------------------------------------- /pages/.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhb-software/payload-plugins/efb8fdb99155a824327e56c7f42a7712c2796054/pages/.npmignore -------------------------------------------------------------------------------- /pages/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "parser": "typescript", 4 | "semi": false, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "overrides": [ 8 | { 9 | "files": ["**/importMap.js", "**/payload-types.ts", "**/**.yaml"], 10 | "options": { 11 | "requirePragma": true 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /pages/.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/swcrc", 3 | "sourceMaps": true, 4 | "jsc": { 5 | "target": "esnext", 6 | "parser": { 7 | "syntax": "typescript", 8 | "tsx": true, 9 | "dts": true 10 | }, 11 | "transform": { 12 | "react": { 13 | "runtime": "automatic", 14 | "pragmaFrag": "React.Fragment", 15 | "throwIfNamespace": true, 16 | "development": false, 17 | "useBuiltins": true 18 | } 19 | } 20 | }, 21 | "module": { 22 | "type": "es6" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pages/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 JHB Software 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pages/dev/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | /media 4 | node_modules 5 | .DS_Store 6 | .env 7 | -------------------------------------------------------------------------------- /pages/dev/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /pages/dev/next.config.mjs: -------------------------------------------------------------------------------- 1 | import { withPayload } from '@payloadcms/next/withPayload' 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = { 5 | // Your Next.js config here 6 | 7 | // Allow for ESM .js import statements 8 | webpack: (webpackConfig) => { 9 | webpackConfig.resolve.extensionAlias = { 10 | '.cjs': ['.cts', '.cjs'], 11 | '.js': ['.ts', '.tsx', '.js', '.jsx'], 12 | '.mjs': ['.mts', '.mjs'], 13 | } 14 | 15 | return webpackConfig 16 | }, 17 | 18 | redirects: () => [ 19 | { 20 | source: '/', 21 | destination: '/admin', 22 | permanent: true, 23 | }, 24 | ], 25 | } 26 | 27 | export default withPayload(nextConfig) 28 | -------------------------------------------------------------------------------- /pages/dev/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nodemon.json", 3 | "ext": "ts", 4 | "exec": "ts-node src/server.ts -- -I", 5 | "stdin": false 6 | } -------------------------------------------------------------------------------- /pages/dev/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pages-plugin-localized-test-app", 3 | "description": "A test app for the pages plugin with localized pages", 4 | "version": "0.0.1", 5 | "license": "MIT", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "cross-env NODE_OPTIONS=\"${NODE_OPTIONS} --no-deprecation\" next dev", 9 | "devturbo": "pnpm dev --turbo", 10 | "devsafe": "rm -rf .next && pnpm dev", 11 | "build": "cross-env NODE_OPTIONS=--no-deprecation next build", 12 | "start": "cross-env NODE_OPTIONS=--no-deprecation next start", 13 | "format": "prettier --write src", 14 | "payload": "payload", 15 | "generate:types": "payload generate:types", 16 | "generate:schema": "payload-graphql generate:schema", 17 | "generate:importmap": "payload generate:importmap", 18 | "test": "vitest run", 19 | "test:watch": "vitest watch" 20 | }, 21 | "dependencies": { 22 | "@jhb.software/payload-pages-plugin": "workspace:*", 23 | "@payloadcms/db-mongodb": "3.50.0", 24 | "@payloadcms/db-sqlite": "3.50.0", 25 | "@payloadcms/next": "3.50.0", 26 | "@payloadcms/translations": "^3.50.0", 27 | "@payloadcms/ui": "3.50.0", 28 | "next": "15.4.6", 29 | "payload": "3.50.0", 30 | "react": "19.1.1", 31 | "react-dom": "19.1.1" 32 | }, 33 | "devDependencies": { 34 | "copyfiles": "^2.4.1", 35 | "cross-env": "^10.0.0", 36 | "dotenv": "^17.2.1", 37 | "vite": "^7.1.2", 38 | "vitest": "^3.2.4" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pages/dev/src/app/(payload)/admin/[[...segments]]/not-found.tsx: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import type { Metadata } from 'next' 4 | 5 | import config from '@payload-config' 6 | import { generatePageMetadata, NotFoundPage } from '@payloadcms/next/views' 7 | 8 | import { importMap } from '../importMap.js' 9 | 10 | type Args = { 11 | params: Promise<{ 12 | segments: string[] 13 | }> 14 | searchParams: Promise<{ 15 | [key: string]: string | string[] 16 | }> 17 | } 18 | 19 | export const generateMetadata = ({ params, searchParams }: Args): Promise => 20 | generatePageMetadata({ config, params, searchParams }) 21 | 22 | const NotFound = ({ params, searchParams }: Args) => 23 | NotFoundPage({ config, importMap, params, searchParams }) 24 | 25 | export default NotFound 26 | -------------------------------------------------------------------------------- /pages/dev/src/app/(payload)/admin/[[...segments]]/page.tsx: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import type { Metadata } from 'next' 4 | 5 | import config from '@payload-config' 6 | import { generatePageMetadata, RootPage } from '@payloadcms/next/views' 7 | 8 | import { importMap } from '../importMap.js' 9 | 10 | type Args = { 11 | params: Promise<{ 12 | segments: string[] 13 | }> 14 | searchParams: Promise<{ 15 | [key: string]: string | string[] 16 | }> 17 | } 18 | 19 | export const generateMetadata = ({ params, searchParams }: Args): Promise => 20 | generatePageMetadata({ config, params, searchParams }) 21 | 22 | const Page = ({ params, searchParams }: Args) => 23 | RootPage({ config, importMap, params, searchParams }) 24 | 25 | export default Page 26 | -------------------------------------------------------------------------------- /pages/dev/src/app/(payload)/admin/importMap.js: -------------------------------------------------------------------------------- 1 | import { IsRootPageField as IsRootPageField_817212d6f65b4eb37176541413db3f8c } from '@jhb.software/payload-pages-plugin/server' 2 | import { SlugFieldWrapper as SlugFieldWrapper_817212d6f65b4eb37176541413db3f8c } from '@jhb.software/payload-pages-plugin/server' 3 | import { ParentField as ParentField_817212d6f65b4eb37176541413db3f8c } from '@jhb.software/payload-pages-plugin/server' 4 | import { PathField as PathField_e6458422044c3374e7ca411c92428566 } from '@jhb.software/payload-pages-plugin/client' 5 | import { BreadcrumbsField as BreadcrumbsField_e6458422044c3374e7ca411c92428566 } from '@jhb.software/payload-pages-plugin/client' 6 | import { PreviewButtonField as PreviewButtonField_e6458422044c3374e7ca411c92428566 } from '@jhb.software/payload-pages-plugin/client' 7 | 8 | export const importMap = { 9 | "@jhb.software/payload-pages-plugin/server#IsRootPageField": IsRootPageField_817212d6f65b4eb37176541413db3f8c, 10 | "@jhb.software/payload-pages-plugin/server#SlugFieldWrapper": SlugFieldWrapper_817212d6f65b4eb37176541413db3f8c, 11 | "@jhb.software/payload-pages-plugin/server#ParentField": ParentField_817212d6f65b4eb37176541413db3f8c, 12 | "@jhb.software/payload-pages-plugin/client#PathField": PathField_e6458422044c3374e7ca411c92428566, 13 | "@jhb.software/payload-pages-plugin/client#BreadcrumbsField": BreadcrumbsField_e6458422044c3374e7ca411c92428566, 14 | "@jhb.software/payload-pages-plugin/client#PreviewButtonField": PreviewButtonField_e6458422044c3374e7ca411c92428566 15 | } 16 | -------------------------------------------------------------------------------- /pages/dev/src/app/(payload)/api/[...slug]/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes' 5 | 6 | export const GET = REST_GET(config) 7 | export const POST = REST_POST(config) 8 | export const DELETE = REST_DELETE(config) 9 | export const PATCH = REST_PATCH(config) 10 | export const OPTIONS = REST_OPTIONS(config) 11 | -------------------------------------------------------------------------------- /pages/dev/src/app/(payload)/api/graphql-playground/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes' 5 | 6 | export const GET = GRAPHQL_PLAYGROUND_GET(config) 7 | -------------------------------------------------------------------------------- /pages/dev/src/app/(payload)/api/graphql/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import { GRAPHQL_POST } from '@payloadcms/next/routes' 5 | 6 | export const POST = GRAPHQL_POST(config) 7 | -------------------------------------------------------------------------------- /pages/dev/src/app/(payload)/custom.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhb-software/payload-plugins/efb8fdb99155a824327e56c7f42a7712c2796054/pages/dev/src/app/(payload)/custom.scss -------------------------------------------------------------------------------- /pages/dev/src/app/(payload)/layout.tsx: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import '@payloadcms/next/css' 5 | import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts' 6 | import type { ServerFunctionClient } from 'payload' 7 | import React from 'react' 8 | 9 | import { importMap } from './admin/importMap.js' 10 | import './custom.scss' 11 | 12 | type Args = { 13 | children: React.ReactNode 14 | } 15 | 16 | const serverFunction: ServerFunctionClient = async function (args) { 17 | 'use server' 18 | return handleServerFunctions({ 19 | ...args, 20 | config, 21 | importMap, 22 | }) 23 | } 24 | 25 | const Layout = ({ children }: Args) => ( 26 | 27 | {children} 28 | 29 | ) 30 | 31 | export default Layout 32 | -------------------------------------------------------------------------------- /pages/dev/src/collections/authors.ts: -------------------------------------------------------------------------------- 1 | import { alternatePathsField, PageCollectionConfig } from '@jhb.software/payload-pages-plugin' 2 | 3 | export const Authors: PageCollectionConfig = { 4 | slug: 'authors', 5 | admin: { 6 | useAsTitle: 'name', 7 | }, 8 | page: { 9 | parent: { 10 | collection: 'pages', 11 | name: 'parent', 12 | sharedDocument: true, 13 | }, 14 | breadcrumbs: { 15 | labelField: 'name', 16 | }, 17 | }, 18 | fields: [ 19 | { 20 | name: 'name', 21 | type: 'text', 22 | required: true, 23 | localized: true, 24 | }, 25 | { 26 | name: 'content', 27 | type: 'textarea', 28 | required: true, 29 | localized: true, 30 | }, 31 | { 32 | // normally this group would be generated by the seo plugin 33 | name: 'meta', 34 | type: 'group', 35 | fields: [alternatePathsField()], 36 | }, 37 | ], 38 | } 39 | -------------------------------------------------------------------------------- /pages/dev/src/collections/blogpost-categories.ts: -------------------------------------------------------------------------------- 1 | import { slugField } from '@jhb.software/payload-pages-plugin' 2 | import { CollectionConfig } from 'payload' 3 | 4 | // This collection is designed to test the functionality of the slugField in a non-page context. 5 | export const BlogpostCategories: CollectionConfig = { 6 | slug: 'blogpost-categories', 7 | admin: { 8 | useAsTitle: 'title', 9 | }, 10 | fields: [ 11 | { 12 | name: 'title', 13 | type: 'text', 14 | required: true, 15 | localized: true, 16 | }, 17 | slugField({ fallbackField: 'title' }), 18 | ], 19 | } 20 | -------------------------------------------------------------------------------- /pages/dev/src/collections/blogposts.ts: -------------------------------------------------------------------------------- 1 | import { PageCollectionConfig } from '@jhb.software/payload-pages-plugin' 2 | 3 | export const Blogposts: PageCollectionConfig = { 4 | slug: 'blogposts', 5 | admin: { 6 | useAsTitle: 'title', 7 | }, 8 | page: { 9 | parent: { 10 | collection: 'authors', 11 | name: 'author', 12 | sharedDocument: false, 13 | }, 14 | breadcrumbs: { 15 | labelField: 'shortTitle', 16 | }, 17 | }, 18 | fields: [ 19 | { 20 | name: 'title', 21 | type: 'text', 22 | required: true, 23 | localized: true, 24 | }, 25 | { 26 | name: 'shortTitle', 27 | type: 'text', 28 | required: true, 29 | localized: true, 30 | }, 31 | { 32 | name: 'content', 33 | type: 'textarea', 34 | required: true, 35 | localized: true, 36 | }, 37 | ], 38 | } 39 | -------------------------------------------------------------------------------- /pages/dev/src/collections/countries.ts: -------------------------------------------------------------------------------- 1 | import { alternatePathsField, PageCollectionConfig } from '@jhb.software/payload-pages-plugin' 2 | 3 | export const Countries: PageCollectionConfig = { 4 | slug: 'countries', 5 | admin: { 6 | useAsTitle: 'title', 7 | }, 8 | page: { 9 | parent: { 10 | collection: 'pages', 11 | name: 'parent', 12 | sharedDocument: true, 13 | }, 14 | }, 15 | versions: { 16 | drafts: true, 17 | }, 18 | fields: [ 19 | { 20 | name: 'title', 21 | type: 'text', 22 | required: true, 23 | localized: true, 24 | }, 25 | { 26 | name: 'content', 27 | type: 'textarea', 28 | required: true, 29 | localized: true, 30 | }, 31 | { 32 | // normally this group would be generated by the seo plugin 33 | name: 'meta', 34 | type: 'group', 35 | fields: [alternatePathsField()], 36 | }, 37 | ], 38 | } 39 | -------------------------------------------------------------------------------- /pages/dev/src/collections/country-travel-tips.ts: -------------------------------------------------------------------------------- 1 | import { alternatePathsField, PageCollectionConfig } from '@jhb.software/payload-pages-plugin' 2 | 3 | export const CountryTravelTips: PageCollectionConfig = { 4 | slug: 'country-travel-tips', 5 | admin: { 6 | useAsTitle: 'title', 7 | }, 8 | page: { 9 | parent: { 10 | collection: 'countries', 11 | name: 'country', 12 | }, 13 | slug: { 14 | unique: false, 15 | staticValue: { 16 | de: 'reisetipps', 17 | en: 'travel-tips', 18 | }, 19 | }, 20 | }, 21 | versions: { 22 | drafts: { 23 | autosave: true, 24 | }, 25 | }, 26 | fields: [ 27 | { 28 | name: 'title', 29 | type: 'text', 30 | required: true, 31 | localized: true, 32 | }, 33 | { 34 | name: 'content', 35 | type: 'textarea', 36 | required: true, 37 | localized: true, 38 | }, 39 | { 40 | // normally this group would be generated by the seo plugin 41 | name: 'meta', 42 | type: 'group', 43 | fields: [alternatePathsField()], 44 | }, 45 | ], 46 | } 47 | -------------------------------------------------------------------------------- /pages/dev/src/collections/pages.ts: -------------------------------------------------------------------------------- 1 | import { alternatePathsField, PageCollectionConfig } from '@jhb.software/payload-pages-plugin' 2 | 3 | export const Pages: PageCollectionConfig = { 4 | slug: 'pages', 5 | admin: { 6 | useAsTitle: 'title', 7 | }, 8 | access: { 9 | update: () => false, 10 | create: () => false, 11 | delete: () => false, 12 | }, 13 | page: { 14 | parent: { 15 | collection: 'pages', 16 | name: 'parent', 17 | }, 18 | isRootCollection: true, 19 | }, 20 | versions: { 21 | drafts: { 22 | autosave: true, 23 | }, 24 | }, 25 | fields: [ 26 | { 27 | name: 'title', 28 | type: 'text', 29 | required: true, 30 | localized: true, 31 | }, 32 | { 33 | name: 'content', 34 | type: 'textarea', 35 | required: true, 36 | localized: true, 37 | }, 38 | { 39 | // normally this group would be generated by the seo plugin 40 | name: 'meta', 41 | type: 'group', 42 | fields: [alternatePathsField()], 43 | }, 44 | ], 45 | } 46 | -------------------------------------------------------------------------------- /pages/dev/src/collections/redirects.ts: -------------------------------------------------------------------------------- 1 | import { RedirectsCollectionConfig } from '@jhb.software/payload-pages-plugin' 2 | 3 | export const Redirects: RedirectsCollectionConfig = { 4 | slug: 'redirects', 5 | admin: { 6 | defaultColumns: ['sourcePath', 'destinationPath', 'permanent', 'createdAt'], 7 | listSearchableFields: ['sourcePath', 'destinationPath'], 8 | }, 9 | redirects: {}, 10 | fields: [ 11 | // the fields are added by the plugin automatically 12 | ], 13 | } 14 | -------------------------------------------------------------------------------- /pages/dev/src/utils/generatePageURL.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate the full URL to a frontend page. 3 | * This is a helper function used by the dev environment. 4 | */ 5 | export const generatePageURL = ({ 6 | path, 7 | preview, 8 | }: { 9 | path: string 10 | preview: boolean 11 | }): string => { 12 | const domain = process.env.NEXT_PUBLIC_FRONTEND_URL 13 | 14 | if (!domain) { 15 | throw new Error('NEXT_PUBLIC_FRONTEND_URL environment variable is required') 16 | } 17 | 18 | if (!path) { 19 | console.error('generatePageURL received an empty path:', path) 20 | return '' 21 | } 22 | 23 | if (!path.startsWith('/')) { 24 | throw new Error('Path must start with a slash: ' + path) 25 | } 26 | 27 | return `${domain}${preview ? '/preview' : ''}${path}` 28 | } 29 | -------------------------------------------------------------------------------- /pages/dev/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "outDir": "./dist", 10 | "rootDir": ".", 11 | "jsx": "preserve", 12 | "paths": { 13 | "@payload-config": ["./src/payload.config.ts"], 14 | "payload/generated-types": ["./src/payload-types.ts"] 15 | }, 16 | "noEmit": true, 17 | "incremental": true, 18 | "module": "ESNext", 19 | "moduleResolution": "Bundler", 20 | "resolveJsonModule": true, 21 | "isolatedModules": true, 22 | "plugins": [ 23 | { 24 | "name": "next" 25 | } 26 | ] 27 | }, 28 | "include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "vite.config.ts"], 29 | "exclude": ["node_modules", "dist", "build"] 30 | } 31 | -------------------------------------------------------------------------------- /pages/dev/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { loadEnv } from 'vite' 2 | import { defineConfig } from 'vitest/config' 3 | 4 | export default defineConfig(({ mode }) => { 5 | return { 6 | test: { 7 | env: loadEnv(mode, process.cwd(), ''), // Load environment variables 8 | }, 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /pages/dev_multi_tenant/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | /media 4 | node_modules 5 | .DS_Store 6 | .env 7 | -------------------------------------------------------------------------------- /pages/dev_multi_tenant/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /pages/dev_multi_tenant/next.config.mjs: -------------------------------------------------------------------------------- 1 | import { withPayload } from '@payloadcms/next/withPayload' 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = { 5 | // Your Next.js config here 6 | 7 | // Allow for ESM .js import statements 8 | webpack: (webpackConfig) => { 9 | webpackConfig.resolve.extensionAlias = { 10 | '.cjs': ['.cts', '.cjs'], 11 | '.js': ['.ts', '.tsx', '.js', '.jsx'], 12 | '.mjs': ['.mts', '.mjs'], 13 | } 14 | 15 | return webpackConfig 16 | }, 17 | 18 | redirects: () => [ 19 | { 20 | source: '/', 21 | destination: '/admin', 22 | permanent: true, 23 | }, 24 | ], 25 | } 26 | 27 | export default withPayload(nextConfig) 28 | -------------------------------------------------------------------------------- /pages/dev_multi_tenant/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nodemon.json", 3 | "ext": "ts", 4 | "exec": "ts-node src/server.ts -- -I", 5 | "stdin": false 6 | } -------------------------------------------------------------------------------- /pages/dev_multi_tenant/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pages-plugin-unlocalized-test-app", 3 | "description": "A test app for the pages plugin with unlocalized pages", 4 | "version": "0.0.1", 5 | "license": "MIT", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "cross-env NODE_OPTIONS=\"${NODE_OPTIONS} --no-deprecation\" next dev", 9 | "devturbo": "pnpm dev --turbo", 10 | "devsafe": "rm -rf .next && pnpm dev", 11 | "build": "cross-env NODE_OPTIONS=--no-deprecation next build", 12 | "start": "cross-env NODE_OPTIONS=--no-deprecation next start", 13 | "format": "prettier --write src", 14 | "payload": "payload", 15 | "generate:types": "payload generate:types", 16 | "generate:schema": "payload-graphql generate:schema", 17 | "generate:importmap": "payload generate:importmap", 18 | "test": "vitest run", 19 | "test:watch": "vitest watch" 20 | }, 21 | "dependencies": { 22 | "@jhb.software/payload-pages-plugin": "workspace:*", 23 | "@payloadcms/db-mongodb": "3.50.0", 24 | "@payloadcms/db-sqlite": "3.50.0", 25 | "@payloadcms/next": "3.50.0", 26 | "@payloadcms/plugin-multi-tenant": "3.50.0", 27 | "@payloadcms/ui": "3.50.0", 28 | "next": "15.4.6", 29 | "payload": "3.50.0", 30 | "react": "19.1.1", 31 | "react-dom": "19.1.1" 32 | }, 33 | "devDependencies": { 34 | "copyfiles": "^2.4.1", 35 | "cross-env": "^10.0.0", 36 | "dotenv": "^17.2.1", 37 | "vite": "^7.1.2", 38 | "vitest": "^3.2.4" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pages/dev_multi_tenant/src/app/(payload)/admin/[[...segments]]/not-found.tsx: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import type { Metadata } from 'next' 4 | 5 | import config from '@payload-config' 6 | import { generatePageMetadata, NotFoundPage } from '@payloadcms/next/views' 7 | 8 | import { importMap } from '../importMap.js' 9 | 10 | type Args = { 11 | params: Promise<{ 12 | segments: string[] 13 | }> 14 | searchParams: Promise<{ 15 | [key: string]: string | string[] 16 | }> 17 | } 18 | 19 | export const generateMetadata = ({ params, searchParams }: Args): Promise => 20 | generatePageMetadata({ config, params, searchParams }) 21 | 22 | const NotFound = ({ params, searchParams }: Args) => 23 | NotFoundPage({ config, importMap, params, searchParams }) 24 | 25 | export default NotFound 26 | -------------------------------------------------------------------------------- /pages/dev_multi_tenant/src/app/(payload)/admin/[[...segments]]/page.tsx: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import type { Metadata } from 'next' 4 | 5 | import config from '@payload-config' 6 | import { generatePageMetadata, RootPage } from '@payloadcms/next/views' 7 | 8 | import { importMap } from '../importMap.js' 9 | 10 | type Args = { 11 | params: Promise<{ 12 | segments: string[] 13 | }> 14 | searchParams: Promise<{ 15 | [key: string]: string | string[] 16 | }> 17 | } 18 | 19 | export const generateMetadata = ({ params, searchParams }: Args): Promise => 20 | generatePageMetadata({ config, params, searchParams }) 21 | 22 | const Page = ({ params, searchParams }: Args) => 23 | RootPage({ config, importMap, params, searchParams }) 24 | 25 | export default Page 26 | -------------------------------------------------------------------------------- /pages/dev_multi_tenant/src/app/(payload)/api/[...slug]/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes' 5 | 6 | export const GET = REST_GET(config) 7 | export const POST = REST_POST(config) 8 | export const DELETE = REST_DELETE(config) 9 | export const PATCH = REST_PATCH(config) 10 | export const OPTIONS = REST_OPTIONS(config) 11 | -------------------------------------------------------------------------------- /pages/dev_multi_tenant/src/app/(payload)/api/graphql-playground/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes' 5 | 6 | export const GET = GRAPHQL_PLAYGROUND_GET(config) 7 | -------------------------------------------------------------------------------- /pages/dev_multi_tenant/src/app/(payload)/api/graphql/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import { GRAPHQL_POST } from '@payloadcms/next/routes' 5 | 6 | export const POST = GRAPHQL_POST(config) 7 | -------------------------------------------------------------------------------- /pages/dev_multi_tenant/src/app/(payload)/custom.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhb-software/payload-plugins/efb8fdb99155a824327e56c7f42a7712c2796054/pages/dev_multi_tenant/src/app/(payload)/custom.scss -------------------------------------------------------------------------------- /pages/dev_multi_tenant/src/app/(payload)/layout.tsx: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import '@payloadcms/next/css' 5 | import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts' 6 | import type { ServerFunctionClient } from 'payload' 7 | import React from 'react' 8 | 9 | import { importMap } from './admin/importMap.js' 10 | import './custom.scss' 11 | 12 | type Args = { 13 | children: React.ReactNode 14 | } 15 | 16 | const serverFunction: ServerFunctionClient = async function (args) { 17 | 'use server' 18 | return handleServerFunctions({ 19 | ...args, 20 | config, 21 | importMap, 22 | }) 23 | } 24 | 25 | const Layout = ({ children }: Args) => ( 26 | 27 | {children} 28 | 29 | ) 30 | 31 | export default Layout 32 | -------------------------------------------------------------------------------- /pages/dev_multi_tenant/src/collections/authors.ts: -------------------------------------------------------------------------------- 1 | import { PageCollectionConfig } from '@jhb.software/payload-pages-plugin' 2 | 3 | export const Authors: PageCollectionConfig = { 4 | slug: 'authors', 5 | admin: { 6 | useAsTitle: 'name', 7 | }, 8 | page: { 9 | parent: { 10 | collection: 'pages', 11 | name: 'parent', 12 | sharedDocument: true, 13 | }, 14 | breadcrumbs: { 15 | labelField: 'name', 16 | }, 17 | slug: { 18 | // Disable the slug uniqueness because of the multi-tenant setup (see indexes below) 19 | unique: false, 20 | }, 21 | }, 22 | indexes: [ 23 | { 24 | fields: ['slug', 'tenant'], 25 | unique: true, 26 | }, 27 | ], 28 | fields: [ 29 | { 30 | name: 'name', 31 | type: 'text', 32 | required: true, 33 | }, 34 | { 35 | name: 'content', 36 | type: 'textarea', 37 | required: true, 38 | }, 39 | ], 40 | } 41 | -------------------------------------------------------------------------------- /pages/dev_multi_tenant/src/collections/blogpost-categories.ts: -------------------------------------------------------------------------------- 1 | import { slugField } from '@jhb.software/payload-pages-plugin' 2 | import { CollectionConfig } from 'payload' 3 | 4 | // This collection is designed to test the functionality of the slugField in a non-page context. 5 | export const BlogpostCategories: CollectionConfig = { 6 | slug: 'blogpost-categories', 7 | admin: { 8 | useAsTitle: 'title', 9 | }, 10 | fields: [ 11 | { 12 | name: 'title', 13 | type: 'text', 14 | required: true, 15 | }, 16 | slugField({ fallbackField: 'title' }), 17 | ], 18 | } 19 | -------------------------------------------------------------------------------- /pages/dev_multi_tenant/src/collections/blogposts.ts: -------------------------------------------------------------------------------- 1 | import { PageCollectionConfig } from '@jhb.software/payload-pages-plugin' 2 | 3 | export const Blogposts: PageCollectionConfig = { 4 | slug: 'blogposts', 5 | admin: { 6 | useAsTitle: 'title', 7 | }, 8 | page: { 9 | parent: { 10 | collection: 'authors', 11 | name: 'author', 12 | sharedDocument: false, 13 | }, 14 | breadcrumbs: { 15 | labelField: 'shortTitle', 16 | }, 17 | slug: { 18 | // Disable the slug uniqueness because of the multi-tenant setup (see indexes below) 19 | unique: false, 20 | }, 21 | }, 22 | indexes: [ 23 | { 24 | fields: ['slug', 'tenant'], 25 | unique: true, 26 | }, 27 | ], 28 | fields: [ 29 | { 30 | name: 'title', 31 | type: 'text', 32 | required: true, 33 | }, 34 | { 35 | name: 'shortTitle', 36 | type: 'text', 37 | required: true, 38 | }, 39 | { 40 | name: 'content', 41 | type: 'textarea', 42 | required: true, 43 | }, 44 | ], 45 | } 46 | -------------------------------------------------------------------------------- /pages/dev_multi_tenant/src/collections/countries.ts: -------------------------------------------------------------------------------- 1 | import { PageCollectionConfig } from '@jhb.software/payload-pages-plugin' 2 | 3 | export const Countries: PageCollectionConfig = { 4 | slug: 'countries', 5 | admin: { 6 | useAsTitle: 'title', 7 | }, 8 | page: { 9 | parent: { 10 | collection: 'pages', 11 | name: 'parent', 12 | sharedDocument: true, 13 | }, 14 | slug: { 15 | // Disable the slug uniqueness because of the multi-tenant setup (see indexes below) 16 | unique: false, 17 | }, 18 | }, 19 | versions: { 20 | drafts: true, 21 | }, 22 | indexes: [ 23 | { 24 | fields: ['slug', 'tenant'], 25 | unique: true, 26 | }, 27 | ], 28 | fields: [ 29 | { 30 | name: 'title', 31 | type: 'text', 32 | required: true, 33 | }, 34 | { 35 | name: 'content', 36 | type: 'textarea', 37 | required: true, 38 | }, 39 | ], 40 | } 41 | -------------------------------------------------------------------------------- /pages/dev_multi_tenant/src/collections/country-travel-tips.ts: -------------------------------------------------------------------------------- 1 | import { PageCollectionConfig } from '@jhb.software/payload-pages-plugin' 2 | 3 | export const CountryTravelTips: PageCollectionConfig = { 4 | slug: 'country-travel-tips', 5 | admin: { 6 | useAsTitle: 'title', 7 | }, 8 | page: { 9 | parent: { 10 | collection: 'countries', 11 | name: 'country', 12 | }, 13 | slug: { 14 | // Disable the slug uniqueness because of the multi-tenant setup (see indexes below) 15 | unique: false, 16 | staticValue: 'reisetipps', 17 | }, 18 | }, 19 | indexes: [ 20 | { 21 | fields: ['slug', 'tenant'], 22 | unique: true, 23 | }, 24 | ], 25 | versions: { 26 | drafts: { 27 | autosave: true, 28 | }, 29 | }, 30 | fields: [ 31 | { 32 | name: 'title', 33 | type: 'text', 34 | required: true, 35 | }, 36 | { 37 | name: 'content', 38 | type: 'textarea', 39 | required: true, 40 | }, 41 | ], 42 | } 43 | -------------------------------------------------------------------------------- /pages/dev_multi_tenant/src/collections/pages.ts: -------------------------------------------------------------------------------- 1 | import { PageCollectionConfig } from '@jhb.software/payload-pages-plugin' 2 | 3 | export const Pages: PageCollectionConfig = { 4 | slug: 'pages', 5 | admin: { 6 | useAsTitle: 'title', 7 | }, 8 | page: { 9 | parent: { 10 | collection: 'pages', 11 | name: 'parent', 12 | }, 13 | isRootCollection: true, 14 | slug: { 15 | // Disable the slug uniqueness because of the multi-tenant setup (see indexes below) 16 | unique: false, 17 | }, 18 | }, 19 | versions: { 20 | drafts: true, 21 | }, 22 | indexes: [ 23 | { 24 | fields: ['slug', 'tenant'], 25 | unique: true, 26 | }, 27 | ], 28 | fields: [ 29 | { 30 | name: 'title', 31 | type: 'text', 32 | required: true, 33 | }, 34 | { 35 | name: 'content', 36 | type: 'textarea', 37 | required: true, 38 | }, 39 | ], 40 | } 41 | -------------------------------------------------------------------------------- /pages/dev_multi_tenant/src/collections/redirects.ts: -------------------------------------------------------------------------------- 1 | import { RedirectsCollectionConfig } from '@jhb.software/payload-pages-plugin' 2 | 3 | export const Redirects: RedirectsCollectionConfig = { 4 | slug: 'redirects', 5 | admin: { 6 | defaultColumns: ['sourcePath', 'destinationPath', 'permanent', 'createdAt'], 7 | listSearchableFields: ['sourcePath', 'destinationPath'], 8 | }, 9 | redirects: {}, 10 | fields: [ 11 | // the fields are added by the plugin automatically 12 | ], 13 | } 14 | -------------------------------------------------------------------------------- /pages/dev_multi_tenant/src/collections/tenants.ts: -------------------------------------------------------------------------------- 1 | import { CollectionConfig } from 'payload' 2 | 3 | const Tenants: CollectionConfig = { 4 | slug: 'tenants', 5 | admin: { 6 | useAsTitle: 'name', 7 | }, 8 | fields: [ 9 | { 10 | name: 'slug', 11 | type: 'text', 12 | required: true, 13 | unique: true, 14 | index: true, 15 | }, 16 | { 17 | name: 'name', 18 | type: 'text', 19 | required: true, 20 | }, 21 | ], 22 | } 23 | 24 | export default Tenants 25 | -------------------------------------------------------------------------------- /pages/dev_multi_tenant/src/utils/generatePageURL.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate the full URL to a frontend page. 3 | * This is a helper function used by the dev environment. 4 | */ 5 | export const generatePageURL = ({ 6 | path, 7 | preview, 8 | }: { 9 | path: string 10 | preview: boolean 11 | }): string => { 12 | const domain = process.env.NEXT_PUBLIC_FRONTEND_URL 13 | 14 | if (!domain) { 15 | throw new Error('NEXT_PUBLIC_FRONTEND_URL environment variable is required') 16 | } 17 | 18 | if (!path) { 19 | console.error('generatePageURL received an empty path:', path) 20 | return '' 21 | } 22 | 23 | if (!path.startsWith('/')) { 24 | throw new Error('Path must start with a slash: ' + path) 25 | } 26 | 27 | return `${domain}${preview ? '/preview' : ''}${path}` 28 | } 29 | -------------------------------------------------------------------------------- /pages/dev_multi_tenant/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "outDir": "./dist", 10 | "rootDir": ".", 11 | "jsx": "preserve", 12 | "paths": { 13 | "@payload-config": ["./src/payload.config.ts"], 14 | "payload/generated-types": ["./src/payload-types.ts"] 15 | }, 16 | "noEmit": true, 17 | "incremental": true, 18 | "module": "ESNext", 19 | "moduleResolution": "Bundler", 20 | "resolveJsonModule": true, 21 | "isolatedModules": true, 22 | "plugins": [ 23 | { 24 | "name": "next" 25 | } 26 | ] 27 | }, 28 | "include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "vite.config.ts"], 29 | "exclude": ["node_modules", "dist", "build"] 30 | } 31 | -------------------------------------------------------------------------------- /pages/dev_multi_tenant/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { loadEnv } from 'vite' 2 | import { defineConfig } from 'vitest/config' 3 | 4 | export default defineConfig(({ mode }) => { 5 | return { 6 | test: { 7 | env: loadEnv(mode, process.cwd(), ''), // Load environment variables 8 | }, 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /pages/dev_unlocalized/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | /media 4 | node_modules 5 | .DS_Store 6 | .env 7 | -------------------------------------------------------------------------------- /pages/dev_unlocalized/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /pages/dev_unlocalized/next.config.mjs: -------------------------------------------------------------------------------- 1 | import { withPayload } from '@payloadcms/next/withPayload' 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = { 5 | // Your Next.js config here 6 | 7 | // Allow for ESM .js import statements 8 | webpack: (webpackConfig) => { 9 | webpackConfig.resolve.extensionAlias = { 10 | '.cjs': ['.cts', '.cjs'], 11 | '.js': ['.ts', '.tsx', '.js', '.jsx'], 12 | '.mjs': ['.mts', '.mjs'], 13 | } 14 | 15 | return webpackConfig 16 | }, 17 | 18 | redirects: () => [ 19 | { 20 | source: '/', 21 | destination: '/admin', 22 | permanent: true, 23 | }, 24 | ], 25 | } 26 | 27 | export default withPayload(nextConfig) 28 | -------------------------------------------------------------------------------- /pages/dev_unlocalized/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nodemon.json", 3 | "ext": "ts", 4 | "exec": "ts-node src/server.ts -- -I", 5 | "stdin": false 6 | } -------------------------------------------------------------------------------- /pages/dev_unlocalized/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pages-plugin-unlocalized-test-app", 3 | "description": "A test app for the pages plugin with unlocalized pages", 4 | "version": "0.0.1", 5 | "license": "MIT", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "cross-env NODE_OPTIONS=\"${NODE_OPTIONS} --no-deprecation\" next dev", 9 | "devturbo": "pnpm dev --turbo", 10 | "devsafe": "rm -rf .next && pnpm dev", 11 | "build": "cross-env NODE_OPTIONS=--no-deprecation next build", 12 | "start": "cross-env NODE_OPTIONS=--no-deprecation next start", 13 | "format": "prettier --write src", 14 | "payload": "payload", 15 | "generate:types": "payload generate:types", 16 | "generate:schema": "payload-graphql generate:schema", 17 | "generate:importmap": "payload generate:importmap", 18 | "test": "vitest run", 19 | "test:watch": "vitest watch" 20 | }, 21 | "dependencies": { 22 | "@jhb.software/payload-pages-plugin": "workspace:*", 23 | "@payloadcms/db-mongodb": "3.50.0", 24 | "@payloadcms/db-sqlite": "3.50.0", 25 | "@payloadcms/next": "3.50.0", 26 | "@payloadcms/ui": "3.50.0", 27 | "next": "15.4.6", 28 | "payload": "3.50.0", 29 | "react": "19.1.1", 30 | "react-dom": "19.1.1" 31 | }, 32 | "devDependencies": { 33 | "copyfiles": "^2.4.1", 34 | "cross-env": "^10.0.0", 35 | "dotenv": "^17.2.1", 36 | "vite": "^7.1.2", 37 | "vitest": "^3.2.4" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pages/dev_unlocalized/src/app/(payload)/admin/[[...segments]]/not-found.tsx: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import type { Metadata } from 'next' 4 | 5 | import config from '@payload-config' 6 | import { generatePageMetadata, NotFoundPage } from '@payloadcms/next/views' 7 | 8 | import { importMap } from '../importMap.js' 9 | 10 | type Args = { 11 | params: Promise<{ 12 | segments: string[] 13 | }> 14 | searchParams: Promise<{ 15 | [key: string]: string | string[] 16 | }> 17 | } 18 | 19 | export const generateMetadata = ({ params, searchParams }: Args): Promise => 20 | generatePageMetadata({ config, params, searchParams }) 21 | 22 | const NotFound = ({ params, searchParams }: Args) => 23 | NotFoundPage({ config, importMap, params, searchParams }) 24 | 25 | export default NotFound 26 | -------------------------------------------------------------------------------- /pages/dev_unlocalized/src/app/(payload)/admin/[[...segments]]/page.tsx: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import type { Metadata } from 'next' 4 | 5 | import config from '@payload-config' 6 | import { generatePageMetadata, RootPage } from '@payloadcms/next/views' 7 | 8 | import { importMap } from '../importMap.js' 9 | 10 | type Args = { 11 | params: Promise<{ 12 | segments: string[] 13 | }> 14 | searchParams: Promise<{ 15 | [key: string]: string | string[] 16 | }> 17 | } 18 | 19 | export const generateMetadata = ({ params, searchParams }: Args): Promise => 20 | generatePageMetadata({ config, params, searchParams }) 21 | 22 | const Page = ({ params, searchParams }: Args) => 23 | RootPage({ config, importMap, params, searchParams }) 24 | 25 | export default Page 26 | -------------------------------------------------------------------------------- /pages/dev_unlocalized/src/app/(payload)/admin/importMap.js: -------------------------------------------------------------------------------- 1 | import { IsRootPageField as IsRootPageField_817212d6f65b4eb37176541413db3f8c } from '@jhb.software/payload-pages-plugin/server' 2 | import { SlugField as SlugField_e6458422044c3374e7ca411c92428566 } from '@jhb.software/payload-pages-plugin/client' 3 | import { ParentField as ParentField_817212d6f65b4eb37176541413db3f8c } from '@jhb.software/payload-pages-plugin/server' 4 | import { PathField as PathField_e6458422044c3374e7ca411c92428566 } from '@jhb.software/payload-pages-plugin/client' 5 | import { BreadcrumbsField as BreadcrumbsField_e6458422044c3374e7ca411c92428566 } from '@jhb.software/payload-pages-plugin/client' 6 | import { PreviewButtonField as PreviewButtonField_e6458422044c3374e7ca411c92428566 } from '@jhb.software/payload-pages-plugin/client' 7 | 8 | export const importMap = { 9 | "@jhb.software/payload-pages-plugin/server#IsRootPageField": IsRootPageField_817212d6f65b4eb37176541413db3f8c, 10 | "@jhb.software/payload-pages-plugin/client#SlugField": SlugField_e6458422044c3374e7ca411c92428566, 11 | "@jhb.software/payload-pages-plugin/server#ParentField": ParentField_817212d6f65b4eb37176541413db3f8c, 12 | "@jhb.software/payload-pages-plugin/client#PathField": PathField_e6458422044c3374e7ca411c92428566, 13 | "@jhb.software/payload-pages-plugin/client#BreadcrumbsField": BreadcrumbsField_e6458422044c3374e7ca411c92428566, 14 | "@jhb.software/payload-pages-plugin/client#PreviewButtonField": PreviewButtonField_e6458422044c3374e7ca411c92428566 15 | } 16 | -------------------------------------------------------------------------------- /pages/dev_unlocalized/src/app/(payload)/api/[...slug]/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes' 5 | 6 | export const GET = REST_GET(config) 7 | export const POST = REST_POST(config) 8 | export const DELETE = REST_DELETE(config) 9 | export const PATCH = REST_PATCH(config) 10 | export const OPTIONS = REST_OPTIONS(config) 11 | -------------------------------------------------------------------------------- /pages/dev_unlocalized/src/app/(payload)/api/graphql-playground/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes' 5 | 6 | export const GET = GRAPHQL_PLAYGROUND_GET(config) 7 | -------------------------------------------------------------------------------- /pages/dev_unlocalized/src/app/(payload)/api/graphql/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import { GRAPHQL_POST } from '@payloadcms/next/routes' 5 | 6 | export const POST = GRAPHQL_POST(config) 7 | -------------------------------------------------------------------------------- /pages/dev_unlocalized/src/app/(payload)/custom.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhb-software/payload-plugins/efb8fdb99155a824327e56c7f42a7712c2796054/pages/dev_unlocalized/src/app/(payload)/custom.scss -------------------------------------------------------------------------------- /pages/dev_unlocalized/src/app/(payload)/layout.tsx: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import '@payloadcms/next/css' 5 | import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts' 6 | import type { ServerFunctionClient } from 'payload' 7 | import React from 'react' 8 | 9 | import { importMap } from './admin/importMap.js' 10 | import './custom.scss' 11 | 12 | type Args = { 13 | children: React.ReactNode 14 | } 15 | 16 | const serverFunction: ServerFunctionClient = async function (args) { 17 | 'use server' 18 | return handleServerFunctions({ 19 | ...args, 20 | config, 21 | importMap, 22 | }) 23 | } 24 | 25 | const Layout = ({ children }: Args) => ( 26 | 27 | {children} 28 | 29 | ) 30 | 31 | export default Layout 32 | -------------------------------------------------------------------------------- /pages/dev_unlocalized/src/collections/authors.ts: -------------------------------------------------------------------------------- 1 | import { PageCollectionConfig } from '@jhb.software/payload-pages-plugin' 2 | 3 | export const Authors: PageCollectionConfig = { 4 | slug: 'authors', 5 | admin: { 6 | useAsTitle: 'name', 7 | }, 8 | page: { 9 | parent: { 10 | collection: 'pages', 11 | name: 'parent', 12 | sharedDocument: true, 13 | }, 14 | breadcrumbs: { 15 | labelField: 'name', 16 | }, 17 | }, 18 | fields: [ 19 | { 20 | name: 'name', 21 | type: 'text', 22 | required: true, 23 | }, 24 | { 25 | name: 'content', 26 | type: 'textarea', 27 | required: true, 28 | }, 29 | ], 30 | } 31 | -------------------------------------------------------------------------------- /pages/dev_unlocalized/src/collections/blogpost-categories.ts: -------------------------------------------------------------------------------- 1 | import { slugField } from '@jhb.software/payload-pages-plugin' 2 | import { CollectionConfig } from 'payload' 3 | 4 | // This collection is designed to test the functionality of the slugField in a non-page context. 5 | export const BlogpostCategories: CollectionConfig = { 6 | slug: 'blogpost-categories', 7 | admin: { 8 | useAsTitle: 'title', 9 | }, 10 | fields: [ 11 | { 12 | name: 'title', 13 | type: 'text', 14 | required: true, 15 | }, 16 | slugField({ fallbackField: 'title' }), 17 | ], 18 | } 19 | -------------------------------------------------------------------------------- /pages/dev_unlocalized/src/collections/blogposts.ts: -------------------------------------------------------------------------------- 1 | import { PageCollectionConfig } from '@jhb.software/payload-pages-plugin' 2 | 3 | export const Blogposts: PageCollectionConfig = { 4 | slug: 'blogposts', 5 | admin: { 6 | useAsTitle: 'title', 7 | }, 8 | page: { 9 | parent: { 10 | collection: 'authors', 11 | name: 'author', 12 | sharedDocument: false, 13 | }, 14 | breadcrumbs: { 15 | labelField: 'shortTitle', 16 | }, 17 | }, 18 | fields: [ 19 | { 20 | name: 'title', 21 | type: 'text', 22 | required: true, 23 | }, 24 | { 25 | name: 'shortTitle', 26 | type: 'text', 27 | required: true, 28 | }, 29 | { 30 | name: 'content', 31 | type: 'textarea', 32 | required: true, 33 | }, 34 | ], 35 | } 36 | -------------------------------------------------------------------------------- /pages/dev_unlocalized/src/collections/countries.ts: -------------------------------------------------------------------------------- 1 | import { PageCollectionConfig } from '@jhb.software/payload-pages-plugin' 2 | 3 | export const Countries: PageCollectionConfig = { 4 | slug: 'countries', 5 | admin: { 6 | useAsTitle: 'title', 7 | }, 8 | page: { 9 | parent: { 10 | collection: 'pages', 11 | name: 'parent', 12 | sharedDocument: true, 13 | }, 14 | }, 15 | versions: { 16 | drafts: true, 17 | }, 18 | fields: [ 19 | { 20 | name: 'title', 21 | type: 'text', 22 | required: true, 23 | }, 24 | { 25 | name: 'content', 26 | type: 'textarea', 27 | required: true, 28 | }, 29 | ], 30 | } 31 | -------------------------------------------------------------------------------- /pages/dev_unlocalized/src/collections/country-travel-tips.ts: -------------------------------------------------------------------------------- 1 | import { PageCollectionConfig } from '@jhb.software/payload-pages-plugin' 2 | 3 | export const CountryTravelTips: PageCollectionConfig = { 4 | slug: 'country-travel-tips', 5 | admin: { 6 | useAsTitle: 'title', 7 | }, 8 | page: { 9 | parent: { 10 | collection: 'countries', 11 | name: 'country', 12 | }, 13 | slug: { 14 | unique: false, 15 | staticValue: 'reisetipps', 16 | }, 17 | }, 18 | versions: { 19 | drafts: { 20 | autosave: true, 21 | }, 22 | }, 23 | fields: [ 24 | { 25 | name: 'title', 26 | type: 'text', 27 | required: true, 28 | }, 29 | { 30 | name: 'content', 31 | type: 'textarea', 32 | required: true, 33 | }, 34 | ], 35 | } 36 | -------------------------------------------------------------------------------- /pages/dev_unlocalized/src/collections/pages.ts: -------------------------------------------------------------------------------- 1 | import { PageCollectionConfig } from '@jhb.software/payload-pages-plugin' 2 | 3 | export const Pages: PageCollectionConfig = { 4 | slug: 'pages', 5 | admin: { 6 | useAsTitle: 'title', 7 | }, 8 | page: { 9 | parent: { 10 | collection: 'pages', 11 | name: 'parent', 12 | }, 13 | isRootCollection: true, 14 | }, 15 | versions: { 16 | drafts: true, 17 | }, 18 | fields: [ 19 | { 20 | name: 'title', 21 | type: 'text', 22 | required: true, 23 | }, 24 | { 25 | name: 'content', 26 | type: 'textarea', 27 | required: true, 28 | }, 29 | ], 30 | } 31 | -------------------------------------------------------------------------------- /pages/dev_unlocalized/src/collections/redirects.ts: -------------------------------------------------------------------------------- 1 | import { RedirectsCollectionConfig } from '@jhb.software/payload-pages-plugin' 2 | 3 | export const Redirects: RedirectsCollectionConfig = { 4 | slug: 'redirects', 5 | admin: { 6 | defaultColumns: ['sourcePath', 'destinationPath', 'permanent', 'createdAt'], 7 | listSearchableFields: ['sourcePath', 'destinationPath'], 8 | }, 9 | redirects: {}, 10 | fields: [ 11 | // the fields are added by the plugin automatically 12 | ], 13 | } 14 | -------------------------------------------------------------------------------- /pages/dev_unlocalized/src/utils/generatePageURL.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate the full URL to a frontend page. 3 | * This is a helper function used by the dev environment. 4 | */ 5 | export const generatePageURL = ({ 6 | path, 7 | preview, 8 | }: { 9 | path: string 10 | preview: boolean 11 | }): string => { 12 | const domain = process.env.NEXT_PUBLIC_FRONTEND_URL 13 | 14 | if (!domain) { 15 | throw new Error('NEXT_PUBLIC_FRONTEND_URL environment variable is required') 16 | } 17 | 18 | if (!path) { 19 | console.error('generatePageURL received an empty path:', path) 20 | return '' 21 | } 22 | 23 | if (!path.startsWith('/')) { 24 | throw new Error('Path must start with a slash: ' + path) 25 | } 26 | 27 | return `${domain}${preview ? '/preview' : ''}${path}` 28 | } 29 | -------------------------------------------------------------------------------- /pages/dev_unlocalized/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "outDir": "./dist", 10 | "rootDir": ".", 11 | "jsx": "preserve", 12 | "paths": { 13 | "@payload-config": ["./src/payload.config.ts"], 14 | "payload/generated-types": ["./src/payload-types.ts"] 15 | }, 16 | "noEmit": true, 17 | "incremental": true, 18 | "module": "ESNext", 19 | "moduleResolution": "Bundler", 20 | "resolveJsonModule": true, 21 | "isolatedModules": true, 22 | "plugins": [ 23 | { 24 | "name": "next" 25 | } 26 | ] 27 | }, 28 | "include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "vite.config.ts"], 29 | "exclude": ["node_modules", "dist", "build"] 30 | } 31 | -------------------------------------------------------------------------------- /pages/dev_unlocalized/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { loadEnv } from 'vite' 2 | import { defineConfig } from 'vitest/config' 3 | 4 | export default defineConfig(({ mode }) => { 5 | return { 6 | test: { 7 | env: loadEnv(mode, process.cwd(), ''), // Load environment variables 8 | }, 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /pages/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - './' 3 | - './dev' 4 | - './dev_unlocalized' 5 | - './dev_multi_tenant' 6 | -------------------------------------------------------------------------------- /pages/src/components/client/BreadcrumbsField.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { ArrayField, Drawer, Button, useModal } from '@payloadcms/ui' 4 | import { ArrayFieldClientComponent } from 'payload' 5 | 6 | const breadcrumbsModalSlug = 'breadcrumbs-drawer' 7 | 8 | export const BreadcrumbsFieldModalButton: React.FC = () => { 9 | const { toggleModal } = useModal() 10 | 11 | return ( 12 |