├── .editorconfig ├── .eslintrc.js ├── .github └── workflows │ └── publish.yml ├── .gitignore ├── .nvmrc ├── .prettierrc.cjs ├── .swcrc ├── LICENSE ├── README.md ├── dev ├── .editorconfig ├── .env.example ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.json ├── .vscode │ ├── extensions.json │ ├── launch.json │ └── settings.json ├── Dockerfile ├── README.md ├── components.json ├── docker-compose.yml ├── eslint.config.mjs ├── next-env.d.ts ├── next-sitemap.config.cjs ├── next.config.js ├── package.json ├── postcss.config.js ├── public │ ├── favicon.ico │ ├── favicon.svg │ └── website-template-OG.webp ├── redirects.js ├── src │ ├── Footer │ │ ├── Component.tsx │ │ ├── RowLabel.tsx │ │ ├── config.ts │ │ └── hooks │ │ │ └── revalidateFooter.ts │ ├── Header │ │ ├── Component.client.tsx │ │ ├── Component.tsx │ │ ├── Nav │ │ │ └── index.tsx │ │ ├── RowLabel.tsx │ │ ├── config.ts │ │ └── hooks │ │ │ └── revalidateHeader.ts │ ├── access │ │ ├── anyone.ts │ │ ├── authenticated.ts │ │ └── authenticatedOrPublished.ts │ ├── app │ │ ├── (frontend) │ │ │ ├── (sitemaps) │ │ │ │ ├── pages-sitemap.xml │ │ │ │ │ └── route.ts │ │ │ │ └── posts-sitemap.xml │ │ │ │ │ └── route.ts │ │ │ ├── [slug] │ │ │ │ ├── page.client.tsx │ │ │ │ └── page.tsx │ │ │ ├── globals.css │ │ │ ├── layout.tsx │ │ │ ├── next │ │ │ │ ├── exit-preview │ │ │ │ │ ├── GET.ts │ │ │ │ │ └── route.ts │ │ │ │ ├── preview │ │ │ │ │ └── route.ts │ │ │ │ └── seed │ │ │ │ │ └── route.ts │ │ │ ├── not-found.tsx │ │ │ ├── page.tsx │ │ │ ├── posts │ │ │ │ ├── [slug] │ │ │ │ │ ├── page.client.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── page.client.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── page │ │ │ │ │ └── [pageNumber] │ │ │ │ │ ├── page.client.tsx │ │ │ │ │ └── page.tsx │ │ │ └── search │ │ │ │ ├── page.client.tsx │ │ │ │ └── page.tsx │ │ └── (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 │ ├── blocks │ │ ├── ArchiveBlock │ │ │ ├── Component.tsx │ │ │ └── config.ts │ │ ├── Banner │ │ │ ├── Component.tsx │ │ │ └── config.ts │ │ ├── CallToAction │ │ │ ├── Component.tsx │ │ │ └── config.ts │ │ ├── Code │ │ │ ├── Component.client.tsx │ │ │ ├── Component.tsx │ │ │ ├── CopyButton.tsx │ │ │ └── config.ts │ │ ├── Content │ │ │ ├── Component.tsx │ │ │ └── config.ts │ │ ├── Form │ │ │ ├── Checkbox │ │ │ │ └── index.tsx │ │ │ ├── Component.tsx │ │ │ ├── Country │ │ │ │ ├── index.tsx │ │ │ │ └── options.ts │ │ │ ├── Email │ │ │ │ └── index.tsx │ │ │ ├── Error │ │ │ │ └── index.tsx │ │ │ ├── Message │ │ │ │ └── index.tsx │ │ │ ├── Number │ │ │ │ └── index.tsx │ │ │ ├── Select │ │ │ │ └── index.tsx │ │ │ ├── State │ │ │ │ ├── index.tsx │ │ │ │ └── options.ts │ │ │ ├── Text │ │ │ │ └── index.tsx │ │ │ ├── Textarea │ │ │ │ └── index.tsx │ │ │ ├── Width │ │ │ │ └── index.tsx │ │ │ ├── buildInitialFormState.tsx │ │ │ ├── config.ts │ │ │ └── fields.tsx │ │ ├── MediaBlock │ │ │ ├── Component.tsx │ │ │ └── config.ts │ │ ├── RelatedPosts │ │ │ └── Component.tsx │ │ └── RenderBlocks.tsx │ ├── collections │ │ ├── Categories.ts │ │ ├── Media.ts │ │ ├── Pages │ │ │ ├── hooks │ │ │ │ └── revalidatePage.ts │ │ │ └── index.ts │ │ ├── Posts │ │ │ ├── hooks │ │ │ │ ├── populateAuthors.ts │ │ │ │ └── revalidatePost.ts │ │ │ └── index.ts │ │ └── Users │ │ │ └── index.ts │ ├── components │ │ ├── AdminBar │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── BeforeDashboard │ │ │ ├── SeedButton │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── BeforeLogin │ │ │ └── index.tsx │ │ ├── Card │ │ │ └── index.tsx │ │ ├── CollectionArchive │ │ │ └── index.tsx │ │ ├── Link │ │ │ └── index.tsx │ │ ├── LivePreviewListener │ │ │ └── index.tsx │ │ ├── Logo │ │ │ └── Logo.tsx │ │ ├── Media │ │ │ ├── ImageMedia │ │ │ │ └── index.tsx │ │ │ ├── VideoMedia │ │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ └── types.ts │ │ ├── PageRange │ │ │ └── index.tsx │ │ ├── Pagination │ │ │ └── index.tsx │ │ ├── PayloadRedirects │ │ │ └── index.tsx │ │ ├── RichText │ │ │ └── index.tsx │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── pagination.tsx │ │ │ ├── select.tsx │ │ │ └── textarea.tsx │ ├── cssVariables.js │ ├── endpoints │ │ └── seed │ │ │ ├── contact-form.ts │ │ │ ├── contact-page.ts │ │ │ ├── home-static.ts │ │ │ ├── home.ts │ │ │ ├── image-1.ts │ │ │ ├── image-2.ts │ │ │ ├── image-3.ts │ │ │ ├── image-hero-1.ts │ │ │ ├── image-hero1.webp │ │ │ ├── image-post1.webp │ │ │ ├── image-post2.webp │ │ │ ├── image-post3.webp │ │ │ ├── index.ts │ │ │ ├── post-1.ts │ │ │ ├── post-2.ts │ │ │ └── post-3.ts │ ├── environment.d.ts │ ├── fields │ │ ├── defaultLexical.ts │ │ ├── link.ts │ │ ├── linkGroup.ts │ │ └── slug │ │ │ ├── SlugComponent.tsx │ │ │ ├── formatSlug.ts │ │ │ ├── index.scss │ │ │ └── index.ts │ ├── heros │ │ ├── HighImpact │ │ │ └── index.tsx │ │ ├── LowImpact │ │ │ └── index.tsx │ │ ├── MediumImpact │ │ │ └── index.tsx │ │ ├── PostHero │ │ │ └── index.tsx │ │ ├── RenderHero.tsx │ │ └── config.ts │ ├── hooks │ │ ├── formatSlug.ts │ │ ├── populatePublishedAt.ts │ │ └── revalidateRedirects.ts │ ├── payload-types.ts │ ├── payload.config.ts │ ├── plugins │ │ └── index.ts │ ├── providers │ │ ├── HeaderTheme │ │ │ └── index.tsx │ │ ├── Theme │ │ │ ├── InitTheme │ │ │ │ └── index.tsx │ │ │ ├── ThemeSelector │ │ │ │ ├── index.tsx │ │ │ │ └── types.ts │ │ │ ├── index.tsx │ │ │ ├── shared.ts │ │ │ └── types.ts │ │ └── index.tsx │ ├── search │ │ ├── Component.tsx │ │ ├── beforeSync.ts │ │ └── fieldOverrides.ts │ └── utilities │ │ ├── canUseDOM.ts │ │ ├── deepMerge.ts │ │ ├── formatAuthors.ts │ │ ├── formatDateTime.ts │ │ ├── generateMeta.ts │ │ ├── generatePreviewPath.ts │ │ ├── getDocument.ts │ │ ├── getGlobals.ts │ │ ├── getMeUser.ts │ │ ├── getRedirects.ts │ │ ├── getURL.ts │ │ ├── mergeOpenGraph.ts │ │ ├── toKebabCase.ts │ │ ├── ui.ts │ │ ├── useClickableCard.ts │ │ └── useDebounce.ts ├── start-database.sh ├── tailwind.config.mjs └── tsconfig.json ├── images ├── payload-plugin-tree-list.gif └── payload-plugin-tree-list.png ├── package.json ├── src ├── TreeListView │ ├── Table │ │ ├── Table.tsx │ │ └── generateTreeList.ts │ ├── ToogleButtons │ │ ├── ToggleButtons.tsx │ │ └── icons │ │ │ ├── TableListIcon.tsx │ │ │ └── TreeListIcon.tsx │ ├── TreeListView.tsx │ └── styles.scss ├── exports │ └── server │ │ └── index.ts └── index.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | quote_type = double 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | 16 | [*.md,*.mdx,*.css,*.scss,*.yaml,*.yml,*rc] 17 | indent_size = 4 18 | 19 | [Makefile] 20 | indent_style = tab 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | import payloadEsLintConfig from "@payloadcms/eslint-config"; 2 | import payloadPlugin from "@payloadcms/eslint-plugin"; 3 | 4 | export const defaultESLintIgnores = [ 5 | "**/.temp", 6 | "**/.*", // ignore all dotfiles 7 | "**/.git", 8 | "**/.hg", 9 | "**/.pnp.*", 10 | "**/.svn", 11 | "**/playwright.config.ts", 12 | "**/jest.config.js", 13 | "**/tsconfig.tsbuildinfo", 14 | "**/README.md", 15 | "**/eslint.config.js", 16 | "**/payload-types.ts", 17 | "**/dist/", 18 | "**/.yarn/", 19 | "**/build/", 20 | "**/node_modules/", 21 | "**/temp/", 22 | ]; 23 | 24 | /** @typedef {import('eslint').Linter.FlatConfig} */ 25 | let FlatConfig; 26 | 27 | export const rootParserOptions = { 28 | sourceType: "module", 29 | ecmaVersion: "latest", 30 | projectService: { 31 | maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING: 40, 32 | allowDefaultProject: ["scripts/*.ts", "*.js", "*.mjs", "*.spec.ts", "*.d.ts"], 33 | }, 34 | }; 35 | 36 | /** @type {FlatConfig[]} */ 37 | export const rootEslintConfig = [ 38 | ...payloadEsLintConfig, 39 | { 40 | ignores: [ 41 | ...defaultESLintIgnores, 42 | "plugins/eslint-*/**", 43 | "test/live-preview/next-app", 44 | "plugins/**/*.spec.tsx", 45 | "plugins/**/*.test.tsx", 46 | "ui/**/*.spec.tsx", 47 | "ui/**/*.test.tsx", 48 | "templates/**", 49 | ], 50 | }, 51 | { 52 | plugins: { 53 | payload: payloadPlugin, 54 | }, 55 | rules: { 56 | "payload/no-jsx-import-statements": "warn", 57 | "payload/no-relative-monorepo-imports": "error", 58 | "payload/no-imports-from-exports-dir": "error", 59 | "payload/no-imports-from-self": "error", 60 | }, 61 | }, 62 | { 63 | files: ["./**/*.ts"], 64 | rules: { 65 | "@typescript-eslint/no-unused-vars": "off", 66 | "no-console": "off", 67 | "operator-linebreak": ["error", "before"], 68 | "perfectionist/sort-object-types": "off", 69 | "perfectionist/sort-objects": "off", 70 | }, 71 | }, 72 | ]; 73 | 74 | export default [ 75 | ...rootEslintConfig, 76 | { 77 | languageOptions: { 78 | parserOptions: { 79 | ...rootParserOptions, 80 | projectService: true, 81 | tsconfigRootDir: import.meta.dirname, 82 | }, 83 | }, 84 | }, 85 | { 86 | files: ["plugins/eslint-config/**/*.ts"], 87 | rules: { 88 | "perfectionist/sort-objects": "off", 89 | }, 90 | }, 91 | { 92 | files: ["templates/vercel-postgres/**"], 93 | rules: { 94 | "no-restricted-exports": "off", 95 | }, 96 | }, 97 | ]; 98 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | matrix: 13 | node-version: [20.x] 14 | 15 | steps: 16 | - name: Check out the code 17 | uses: actions/checkout@v3 18 | 19 | - name: Set up Node.js 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | cache: "yarn" 24 | 25 | - name: Install Yarn 1.22.22 26 | run: | 27 | npm install --global yarn@1.22.22 28 | 29 | - name: Install dependencies 30 | run: yarn install 31 | 32 | - name: Build the project 33 | run: yarn build 34 | 35 | - name: Log in to npm 36 | uses: actions/configure-npm@v3 37 | with: 38 | npm-token: ${{ secrets.NPM_TOKEN }} 39 | 40 | - name: Publish to npm 41 | run: yarn publish --non-interactive --access public 42 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | >=20 2 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | parser: "typescript", 4 | semi: true, 5 | singleQuote: false, 6 | trailingComma: "all", 7 | arrowParens: "avoid", 8 | }; 9 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Italo Devoto Ramella 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Payload Tree List Plugin 2 | 3 | > [!IMPORTANT] 4 | > This plugin is under development and is subject to BREAKING CHANGES. It is not yet recommended to use this in a production environment. 5 | 6 | 7 | A plugin for [Payload CMS](https://github.com/payloadcms/payload) that adds a collapsible Tree list view. This plugin depends on the [Payload Nested Docs Plugin](https://github.com/payloadcms/payload/tree/beta/packages/plugin-nested-docs) for its parent implementation. 8 | 9 | This plugin is compatible only with Payload CMS version 3.x. 10 | 11 | ![Payload Tree List Plugin Example with some items open and other closed](./images/payload-plugin-tree-list.gif) 12 | 13 | ## Installation / How to use 14 | 15 | **Minimum required payload version: 3.x** 16 | 17 | Install the plugin using `yarn add @payloadcms/plugin-nested-docs payload-plugin-tree-list`. Then, add the plugin to your Payload configuration file: 18 | 19 | payload.config.ts: 20 | 21 | ```ts 22 | import { buildConfig } from "payload"; 23 | import { nestedDocsPlugin } from "@payloadcms/plugin-nested-docs"; 24 | import { treeListPlugin } from "payload-plugin-tree-list"; 25 | 26 | 27 | export default buildConfig({ 28 | ... 29 | plugins: [ 30 | nestedDocsPlugin({ collections: ["posts"] }), 31 | treeListPlugin({ collections: ["posts"] }), 32 | ], 33 | ... 34 | }); 35 | 36 | ``` 37 | 38 | ## Testing locally 39 | 40 | This project use `yarn` workspaces. 41 | Install all dependencies from root and build the plugin 42 | 43 | ```sh 44 | yarn i 45 | yarn build 46 | ``` 47 | 48 | 49 | To start the postgres database and the development server, run the following commands: 50 | 51 | ```sh 52 | cd dev 53 | cp .env.example .env 54 | sh start-database.sh 55 | yarn generate:importmap 56 | yarn dev 57 | ``` 58 | 59 | Access http://localhost:3000/admin 60 | 61 | The plugin is configured for `pages` collection 62 | 63 | 64 | -------------------------------------------------------------------------------- /dev/.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 | -------------------------------------------------------------------------------- /dev/.env.example: -------------------------------------------------------------------------------- 1 | # Database connection string 2 | DATABASE_URI=postgres://postgres:postgres123@127.0.0.1:5432/postgres_db 3 | 4 | # Or use a PG connection string 5 | #DATABASE_URI=postgresql://127.0.0.1:5432/your-database-name 6 | 7 | # Used to encrypt JWT tokens 8 | PAYLOAD_SECRET=YOUR_SECRET_HERE 9 | 10 | # Used to configure CORS, format links and more. No trailing slash 11 | NEXT_PUBLIC_SERVER_URL=http://localhost:3000 12 | 13 | # Secret used to authenticate cron jobs 14 | CRON_SECRET=YOUR_CRON_SECRET_HERE 15 | -------------------------------------------------------------------------------- /dev/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist / media 3 | node_modules 4 | .DS_Store 5 | .env 6 | .next 7 | .vercel 8 | 9 | # Payload default media upload directory 10 | public/media/ 11 | 12 | public/robots.txt 13 | public/sitemap*.xml 14 | -------------------------------------------------------------------------------- /dev/.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | enable-pre-post-scripts=true 3 | -------------------------------------------------------------------------------- /dev/.prettierignore: -------------------------------------------------------------------------------- 1 | **/payload-types.ts 2 | .tmp 3 | **/.git 4 | **/.hg 5 | **/.pnp.* 6 | **/.svn 7 | **/.yarn/** 8 | **/build 9 | **/dist/** 10 | **/node_modules 11 | **/temp 12 | **/docs/** 13 | tsconfig.json 14 | 15 | -------------------------------------------------------------------------------- /dev/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 100, 5 | "semi": false 6 | } 7 | -------------------------------------------------------------------------------- /dev/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /dev/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Next.js: debug full stack", 9 | "type": "node", 10 | "request": "launch", 11 | "program": "${workspaceFolder}/node_modules/next/dist/bin/next", 12 | "runtimeArgs": ["--inspect"], 13 | "skipFiles": ["/**"], 14 | "serverReadyAction": { 15 | "action": "debugWithChrome", 16 | "killOnServerStop": true, 17 | "pattern": "- Local:.+(https?://.+)", 18 | "uriFormat": "%s", 19 | "webRoot": "${workspaceFolder}" 20 | }, 21 | "cwd": "${workspaceFolder}" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /dev/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "npm.packageManager": "pnpm", 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "[typescript]": { 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "editor.formatOnSave": true, 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll.eslint": "explicit" 9 | } 10 | }, 11 | "[typescriptreact]": { 12 | "editor.defaultFormatter": "esbenp.prettier-vscode", 13 | "editor.formatOnSave": true, 14 | "editor.codeActionsOnSave": { 15 | "source.fixAll.eslint": "explicit" 16 | } 17 | }, 18 | "[javascript]": { 19 | "editor.defaultFormatter": "esbenp.prettier-vscode", 20 | "editor.formatOnSave": true, 21 | "editor.codeActionsOnSave": { 22 | "source.fixAll.eslint": "explicit" 23 | } 24 | }, 25 | "[json]": { 26 | "editor.defaultFormatter": "esbenp.prettier-vscode", 27 | "editor.formatOnSave": true 28 | }, 29 | "[jsonc]": { 30 | "editor.defaultFormatter": "esbenp.prettier-vscode", 31 | "editor.formatOnSave": true 32 | }, 33 | "editor.formatOnSaveMode": "file", 34 | "typescript.tsdk": "node_modules/typescript/lib", 35 | "[javascript][typescript][typescriptreact]": { 36 | "editor.codeActionsOnSave": { 37 | "source.fixAll.eslint": "explicit" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /dev/Dockerfile: -------------------------------------------------------------------------------- 1 | # To use this Dockerfile, you have to set `output: 'standalone'` in your next.config.js file. 2 | # From https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile 3 | 4 | FROM node:22.12.0-alpine AS base 5 | 6 | # Install dependencies only when needed 7 | FROM base AS deps 8 | # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. 9 | RUN apk add --no-cache libc6-compat 10 | WORKDIR /app 11 | 12 | # Install dependencies based on the preferred package manager 13 | COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ 14 | RUN \ 15 | if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ 16 | elif [ -f package-lock.json ]; then npm ci; \ 17 | elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \ 18 | else echo "Lockfile not found." && exit 1; \ 19 | fi 20 | 21 | 22 | # Rebuild the source code only when needed 23 | FROM base AS builder 24 | WORKDIR /app 25 | COPY --from=deps /app/node_modules ./node_modules 26 | COPY . . 27 | 28 | # Next.js collects completely anonymous telemetry data about general usage. 29 | # Learn more here: https://nextjs.org/telemetry 30 | # Uncomment the following line in case you want to disable telemetry during the build. 31 | # ENV NEXT_TELEMETRY_DISABLED 1 32 | 33 | RUN \ 34 | if [ -f yarn.lock ]; then yarn run build; \ 35 | elif [ -f package-lock.json ]; then npm run build; \ 36 | elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \ 37 | else echo "Lockfile not found." && exit 1; \ 38 | fi 39 | 40 | # Production image, copy all the files and run next 41 | FROM base AS runner 42 | WORKDIR /app 43 | 44 | ENV NODE_ENV production 45 | # Uncomment the following line in case you want to disable telemetry during runtime. 46 | # ENV NEXT_TELEMETRY_DISABLED 1 47 | 48 | RUN addgroup --system --gid 1001 nodejs 49 | RUN adduser --system --uid 1001 nextjs 50 | 51 | # Remove this line if you do not have this folder 52 | COPY --from=builder /app/public ./public 53 | 54 | # Set the correct permission for prerender cache 55 | RUN mkdir .next 56 | RUN chown nextjs:nodejs .next 57 | 58 | # Automatically leverage output traces to reduce image size 59 | # https://nextjs.org/docs/advanced-features/output-file-tracing 60 | COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ 61 | COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static 62 | 63 | USER nextjs 64 | 65 | EXPOSE 3000 66 | 67 | ENV PORT 3000 68 | 69 | # server.js is created by next build from the standalone output 70 | # https://nextjs.org/docs/pages/api-reference/next-config-js/output 71 | CMD HOSTNAME="0.0.0.0" node server.js 72 | -------------------------------------------------------------------------------- /dev/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/app/(frontend)/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/utilities/ui" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /dev/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | payload: 5 | image: node:18-alpine 6 | ports: 7 | - '3000:3000' 8 | volumes: 9 | - .:/home/node/app 10 | - node_modules:/home/node/app/node_modules 11 | working_dir: /home/node/app/ 12 | command: sh -c "yarn install && yarn dev" 13 | depends_on: 14 | - mongo 15 | env_file: 16 | - .env 17 | 18 | mongo: 19 | image: mongo:latest 20 | ports: 21 | - '27017:27017' 22 | command: 23 | - --storageEngine=wiredTiger 24 | volumes: 25 | - data:/data/db 26 | logging: 27 | driver: none 28 | 29 | volumes: 30 | data: 31 | node_modules: 32 | -------------------------------------------------------------------------------- /dev/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from 'path' 2 | import { fileURLToPath } from 'url' 3 | import { FlatCompat } from '@eslint/eslintrc' 4 | 5 | const __filename = fileURLToPath(import.meta.url) 6 | const __dirname = dirname(__filename) 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }) 11 | 12 | const eslintConfig = [ 13 | ...compat.extends('next/core-web-vitals', 'next/typescript'), 14 | { 15 | rules: { 16 | '@typescript-eslint/ban-ts-comment': 'warn', 17 | '@typescript-eslint/no-empty-object-type': 'warn', 18 | '@typescript-eslint/no-explicit-any': 'warn', 19 | '@typescript-eslint/no-unused-vars': [ 20 | 'warn', 21 | { 22 | vars: 'all', 23 | args: 'after-used', 24 | ignoreRestSiblings: false, 25 | argsIgnorePattern: '^_', 26 | varsIgnorePattern: '^_', 27 | destructuredArrayIgnorePattern: '^_', 28 | caughtErrorsIgnorePattern: '^(_|ignore)', 29 | }, 30 | ], 31 | }, 32 | }, 33 | { 34 | ignores: ['.next/'], 35 | }, 36 | ] 37 | 38 | export default eslintConfig 39 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /dev/next-sitemap.config.cjs: -------------------------------------------------------------------------------- 1 | const SITE_URL = 2 | process.env.NEXT_PUBLIC_SERVER_URL || 3 | process.env.VERCEL_PROJECT_PRODUCTION_URL || 4 | 'https://example.com' 5 | 6 | /** @type {import('next-sitemap').IConfig} */ 7 | module.exports = { 8 | siteUrl: SITE_URL, 9 | generateRobotsTxt: true, 10 | exclude: ['/posts-sitemap.xml', '/pages-sitemap.xml', '/*', '/posts/*'], 11 | robotsTxtOptions: { 12 | policies: [ 13 | { 14 | userAgent: '*', 15 | disallow: '/admin/*', 16 | }, 17 | ], 18 | additionalSitemaps: [`${SITE_URL}/pages-sitemap.xml`, `${SITE_URL}/posts-sitemap.xml`], 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /dev/next.config.js: -------------------------------------------------------------------------------- 1 | import { withPayload } from '@payloadcms/next/withPayload' 2 | 3 | import redirects from './redirects.js' 4 | 5 | const NEXT_PUBLIC_SERVER_URL = process.env.VERCEL_PROJECT_PRODUCTION_URL 6 | ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}` 7 | : undefined || process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:3000' 8 | 9 | /** @type {import('next').NextConfig} */ 10 | const nextConfig = { 11 | images: { 12 | remotePatterns: [ 13 | ...[NEXT_PUBLIC_SERVER_URL /* 'https://example.com' */].map((item) => { 14 | const url = new URL(item) 15 | 16 | return { 17 | hostname: url.hostname, 18 | protocol: url.protocol.replace(':', ''), 19 | } 20 | }), 21 | ], 22 | }, 23 | reactStrictMode: true, 24 | redirects, 25 | } 26 | 27 | export default withPayload(nextConfig) 28 | -------------------------------------------------------------------------------- /dev/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "payload-plugin-tree-list-dev", 3 | "version": "1.0.0", 4 | "description": "Website template for Payload", 5 | "license": "MIT", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "cross-env NODE_OPTIONS=--no-deprecation next dev", 9 | "generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap", 10 | "generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types", 11 | "ii": "cross-env NODE_OPTIONS=--no-deprecation yarn --ignore-workspace install", 12 | "lint": "cross-env NODE_OPTIONS=--no-deprecation next lint", 13 | "lint:fix": "cross-env NODE_OPTIONS=--no-deprecation next lint --fix", 14 | "payload": "cross-env NODE_OPTIONS=--no-deprecation payload", 15 | "reinstall": "cross-env NODE_OPTIONS=--no-deprecation rm -rf node_modules && rm yarn.lock && yarn --ignore-workspace install", 16 | "start": "cross-env NODE_OPTIONS=--no-deprecation next start" 17 | }, 18 | "dependencies": { 19 | "@payloadcms/live-preview-react": "latest", 20 | "@payloadcms/next": "latest", 21 | "@payloadcms/payload-cloud": "latest", 22 | "@payloadcms/plugin-form-builder": "latest", 23 | "@payloadcms/plugin-nested-docs": "latest", 24 | "@payloadcms/plugin-redirects": "latest", 25 | "@payloadcms/plugin-search": "latest", 26 | "@payloadcms/plugin-seo": "latest", 27 | "@payloadcms/richtext-lexical": "latest", 28 | "@payloadcms/ui": "latest", 29 | "@radix-ui/react-checkbox": "^1.0.4", 30 | "@radix-ui/react-label": "^2.0.2", 31 | "@radix-ui/react-select": "^2.0.0", 32 | "@radix-ui/react-slot": "^1.0.2", 33 | "class-variance-authority": "^0.7.0", 34 | "clsx": "^2.1.1", 35 | "cross-env": "^7.0.3", 36 | "geist": "^1.3.0", 37 | "graphql": "^16.8.2", 38 | "lucide-react": "^0.378.0", 39 | "next": "^15.1.5", 40 | "next-sitemap": "^4.2.3", 41 | "payload": "latest", 42 | "payload-admin-bar": "^1.0.6", 43 | "payload-plugin-tree-list": "workspace: *", 44 | "prism-react-renderer": "^2.3.1", 45 | "react": "^19.0.0", 46 | "react-dom": "^19.0.0", 47 | "react-hook-form": "7.45.4", 48 | "sharp": "0.32.6", 49 | "tailwind-merge": "^2.3.0", 50 | "tailwindcss-animate": "^1.0.7", 51 | "@payloadcms/db-postgres": "latest" 52 | }, 53 | "devDependencies": { 54 | "@eslint/eslintrc": "^3.2.0", 55 | "@tailwindcss/typography": "^0.5.13", 56 | "@types/escape-html": "^1.0.2", 57 | "@types/node": "22.5.4", 58 | "@types/react": "19.0.7", 59 | "@types/react-dom": "19.0.3", 60 | "autoprefixer": "^10.4.19", 61 | "copyfiles": "^2.4.1", 62 | "eslint": "^9.16.0", 63 | "eslint-config-next": "15.1.5", 64 | "postcss": "^8.4.38", 65 | "prettier": "^3.4.2", 66 | "tailwindcss": "^3.4.3", 67 | "typescript": "5.7.3" 68 | }, 69 | "engines": { 70 | "node": "^18.20.2 || >=20.9.0" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /dev/postcss.config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | 8 | export default config 9 | -------------------------------------------------------------------------------- /dev/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italodr/payload-plugin-tree-list/f2646a4ed96fb7a4d4dba9512155027592089d5b/dev/public/favicon.ico -------------------------------------------------------------------------------- /dev/public/website-template-OG.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italodr/payload-plugin-tree-list/f2646a4ed96fb7a4d4dba9512155027592089d5b/dev/public/website-template-OG.webp -------------------------------------------------------------------------------- /dev/redirects.js: -------------------------------------------------------------------------------- 1 | const redirects = async () => { 2 | const internetExplorerRedirect = { 3 | destination: '/ie-incompatible.html', 4 | has: [ 5 | { 6 | type: 'header', 7 | key: 'user-agent', 8 | value: '(.*Trident.*)', // all ie browsers 9 | }, 10 | ], 11 | permanent: false, 12 | source: '/:path((?!ie-incompatible.html$).*)', // all pages except the incompatibility page 13 | } 14 | 15 | const redirects = [internetExplorerRedirect] 16 | 17 | return redirects 18 | } 19 | 20 | export default redirects 21 | -------------------------------------------------------------------------------- /dev/src/Footer/Component.tsx: -------------------------------------------------------------------------------- 1 | import { getCachedGlobal } from '@/utilities/getGlobals' 2 | import Link from 'next/link' 3 | import React from 'react' 4 | 5 | import type { Footer } from '@/payload-types' 6 | 7 | import { ThemeSelector } from '@/providers/Theme/ThemeSelector' 8 | import { CMSLink } from '@/components/Link' 9 | import { Logo } from '@/components/Logo/Logo' 10 | 11 | export async function Footer() { 12 | const footerData: Footer = await getCachedGlobal('footer', 1)() 13 | 14 | const navItems = footerData?.navItems || [] 15 | 16 | return ( 17 |
18 |
19 | 20 | 21 | 22 | 23 |
24 | 25 | 30 |
31 |
32 |
33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /dev/src/Footer/RowLabel.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { Header } from '@/payload-types' 3 | import { RowLabelProps, useRowLabel } from '@payloadcms/ui' 4 | 5 | export const RowLabel: React.FC = () => { 6 | const data = useRowLabel[number]>() 7 | 8 | const label = data?.data?.link?.label 9 | ? `Nav item ${data.rowNumber !== undefined ? data.rowNumber + 1 : ''}: ${data?.data?.link?.label}` 10 | : 'Row' 11 | 12 | return
{label}
13 | } 14 | -------------------------------------------------------------------------------- /dev/src/Footer/config.ts: -------------------------------------------------------------------------------- 1 | import type { GlobalConfig } from 'payload' 2 | 3 | import { link } from '@/fields/link' 4 | import { revalidateFooter } from './hooks/revalidateFooter' 5 | 6 | export const Footer: GlobalConfig = { 7 | slug: 'footer', 8 | access: { 9 | read: () => true, 10 | }, 11 | fields: [ 12 | { 13 | name: 'navItems', 14 | type: 'array', 15 | fields: [ 16 | link({ 17 | appearances: false, 18 | }), 19 | ], 20 | maxRows: 6, 21 | admin: { 22 | initCollapsed: true, 23 | components: { 24 | RowLabel: '@/Footer/RowLabel#RowLabel', 25 | }, 26 | }, 27 | }, 28 | ], 29 | hooks: { 30 | afterChange: [revalidateFooter], 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /dev/src/Footer/hooks/revalidateFooter.ts: -------------------------------------------------------------------------------- 1 | import type { GlobalAfterChangeHook } from 'payload' 2 | 3 | import { revalidateTag } from 'next/cache' 4 | 5 | export const revalidateFooter: GlobalAfterChangeHook = ({ doc, req: { payload, context } }) => { 6 | if (!context.disableRevalidate) { 7 | payload.logger.info(`Revalidating footer`) 8 | 9 | revalidateTag('global_footer') 10 | } 11 | 12 | return doc 13 | } 14 | -------------------------------------------------------------------------------- /dev/src/Header/Component.client.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useHeaderTheme } from '@/providers/HeaderTheme' 3 | import Link from 'next/link' 4 | import { usePathname } from 'next/navigation' 5 | import React, { useEffect, useState } from 'react' 6 | 7 | import type { Header } from '@/payload-types' 8 | 9 | import { Logo } from '@/components/Logo/Logo' 10 | import { HeaderNav } from './Nav' 11 | 12 | interface HeaderClientProps { 13 | data: Header 14 | } 15 | 16 | export const HeaderClient: React.FC = ({ data }) => { 17 | /* Storing the value in a useState to avoid hydration errors */ 18 | const [theme, setTheme] = useState(null) 19 | const { headerTheme, setHeaderTheme } = useHeaderTheme() 20 | const pathname = usePathname() 21 | 22 | useEffect(() => { 23 | setHeaderTheme(null) 24 | // eslint-disable-next-line react-hooks/exhaustive-deps 25 | }, [pathname]) 26 | 27 | useEffect(() => { 28 | if (headerTheme && headerTheme !== theme) setTheme(headerTheme) 29 | // eslint-disable-next-line react-hooks/exhaustive-deps 30 | }, [headerTheme]) 31 | 32 | return ( 33 |
34 |
35 | 36 | 37 | 38 | 39 |
40 |
41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /dev/src/Header/Component.tsx: -------------------------------------------------------------------------------- 1 | import { HeaderClient } from './Component.client' 2 | import { getCachedGlobal } from '@/utilities/getGlobals' 3 | import React from 'react' 4 | 5 | import type { Header } from '@/payload-types' 6 | 7 | export async function Header() { 8 | const headerData: Header = await getCachedGlobal('header', 1)() 9 | 10 | return 11 | } 12 | -------------------------------------------------------------------------------- /dev/src/Header/Nav/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React from 'react' 4 | 5 | import type { Header as HeaderType } from '@/payload-types' 6 | 7 | import { CMSLink } from '@/components/Link' 8 | import Link from 'next/link' 9 | import { SearchIcon } from 'lucide-react' 10 | 11 | export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => { 12 | const navItems = data?.navItems || [] 13 | 14 | return ( 15 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /dev/src/Header/RowLabel.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { Header } from '@/payload-types' 3 | import { RowLabelProps, useRowLabel } from '@payloadcms/ui' 4 | 5 | export const RowLabel: React.FC = () => { 6 | const data = useRowLabel[number]>() 7 | 8 | const label = data?.data?.link?.label 9 | ? `Nav item ${data.rowNumber !== undefined ? data.rowNumber + 1 : ''}: ${data?.data?.link?.label}` 10 | : 'Row' 11 | 12 | return
{label}
13 | } 14 | -------------------------------------------------------------------------------- /dev/src/Header/config.ts: -------------------------------------------------------------------------------- 1 | import type { GlobalConfig } from 'payload' 2 | 3 | import { link } from '@/fields/link' 4 | import { revalidateHeader } from './hooks/revalidateHeader' 5 | 6 | export const Header: GlobalConfig = { 7 | slug: 'header', 8 | access: { 9 | read: () => true, 10 | }, 11 | fields: [ 12 | { 13 | name: 'navItems', 14 | type: 'array', 15 | fields: [ 16 | link({ 17 | appearances: false, 18 | }), 19 | ], 20 | maxRows: 6, 21 | admin: { 22 | initCollapsed: true, 23 | components: { 24 | RowLabel: '@/Header/RowLabel#RowLabel', 25 | }, 26 | }, 27 | }, 28 | ], 29 | hooks: { 30 | afterChange: [revalidateHeader], 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /dev/src/Header/hooks/revalidateHeader.ts: -------------------------------------------------------------------------------- 1 | import type { GlobalAfterChangeHook } from 'payload' 2 | 3 | import { revalidateTag } from 'next/cache' 4 | 5 | export const revalidateHeader: GlobalAfterChangeHook = ({ doc, req: { payload, context } }) => { 6 | if (!context.disableRevalidate) { 7 | payload.logger.info(`Revalidating header`) 8 | 9 | revalidateTag('global_header') 10 | } 11 | 12 | return doc 13 | } 14 | -------------------------------------------------------------------------------- /dev/src/access/anyone.ts: -------------------------------------------------------------------------------- 1 | import type { Access } from 'payload' 2 | 3 | export const anyone: Access = () => true 4 | -------------------------------------------------------------------------------- /dev/src/access/authenticated.ts: -------------------------------------------------------------------------------- 1 | import type { AccessArgs } from 'payload' 2 | 3 | import type { User } from '@/payload-types' 4 | 5 | type isAuthenticated = (args: AccessArgs) => boolean 6 | 7 | export const authenticated: isAuthenticated = ({ req: { user } }) => { 8 | return Boolean(user) 9 | } 10 | -------------------------------------------------------------------------------- /dev/src/access/authenticatedOrPublished.ts: -------------------------------------------------------------------------------- 1 | import type { Access } from 'payload' 2 | 3 | export const authenticatedOrPublished: Access = ({ req: { user } }) => { 4 | if (user) { 5 | return true 6 | } 7 | 8 | return { 9 | _status: { 10 | equals: 'published', 11 | }, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /dev/src/app/(frontend)/(sitemaps)/pages-sitemap.xml/route.ts: -------------------------------------------------------------------------------- 1 | import { getServerSideSitemap } from 'next-sitemap' 2 | import { getPayload } from 'payload' 3 | import config from '@payload-config' 4 | import { unstable_cache } from 'next/cache' 5 | 6 | const getPagesSitemap = unstable_cache( 7 | async () => { 8 | const payload = await getPayload({ config }) 9 | const SITE_URL = 10 | process.env.NEXT_PUBLIC_SERVER_URL || 11 | process.env.VERCEL_PROJECT_PRODUCTION_URL || 12 | 'https://example.com' 13 | 14 | const results = await payload.find({ 15 | collection: 'pages', 16 | overrideAccess: false, 17 | draft: false, 18 | depth: 0, 19 | limit: 1000, 20 | pagination: false, 21 | where: { 22 | _status: { 23 | equals: 'published', 24 | }, 25 | }, 26 | select: { 27 | slug: true, 28 | updatedAt: true, 29 | }, 30 | }) 31 | 32 | const dateFallback = new Date().toISOString() 33 | 34 | const defaultSitemap = [ 35 | { 36 | loc: `${SITE_URL}/search`, 37 | lastmod: dateFallback, 38 | }, 39 | { 40 | loc: `${SITE_URL}/posts`, 41 | lastmod: dateFallback, 42 | }, 43 | ] 44 | 45 | const sitemap = results.docs 46 | ? results.docs 47 | .filter((page) => Boolean(page?.slug)) 48 | .map((page) => { 49 | return { 50 | loc: page?.slug === 'home' ? `${SITE_URL}/` : `${SITE_URL}/${page?.slug}`, 51 | lastmod: page.updatedAt || dateFallback, 52 | } 53 | }) 54 | : [] 55 | 56 | return [...defaultSitemap, ...sitemap] 57 | }, 58 | ['pages-sitemap'], 59 | { 60 | tags: ['pages-sitemap'], 61 | }, 62 | ) 63 | 64 | export async function GET() { 65 | const sitemap = await getPagesSitemap() 66 | 67 | return getServerSideSitemap(sitemap) 68 | } 69 | -------------------------------------------------------------------------------- /dev/src/app/(frontend)/(sitemaps)/posts-sitemap.xml/route.ts: -------------------------------------------------------------------------------- 1 | import { getServerSideSitemap } from 'next-sitemap' 2 | import { getPayload } from 'payload' 3 | import config from '@payload-config' 4 | import { unstable_cache } from 'next/cache' 5 | 6 | const getPostsSitemap = unstable_cache( 7 | async () => { 8 | const payload = await getPayload({ config }) 9 | const SITE_URL = 10 | process.env.NEXT_PUBLIC_SERVER_URL || 11 | process.env.VERCEL_PROJECT_PRODUCTION_URL || 12 | 'https://example.com' 13 | 14 | const results = await payload.find({ 15 | collection: 'posts', 16 | overrideAccess: false, 17 | draft: false, 18 | depth: 0, 19 | limit: 1000, 20 | pagination: false, 21 | where: { 22 | _status: { 23 | equals: 'published', 24 | }, 25 | }, 26 | select: { 27 | slug: true, 28 | updatedAt: true, 29 | }, 30 | }) 31 | 32 | const dateFallback = new Date().toISOString() 33 | 34 | const sitemap = results.docs 35 | ? results.docs 36 | .filter((post) => Boolean(post?.slug)) 37 | .map((post) => ({ 38 | loc: `${SITE_URL}/posts/${post?.slug}`, 39 | lastmod: post.updatedAt || dateFallback, 40 | })) 41 | : [] 42 | 43 | return sitemap 44 | }, 45 | ['posts-sitemap'], 46 | { 47 | tags: ['posts-sitemap'], 48 | }, 49 | ) 50 | 51 | export async function GET() { 52 | const sitemap = await getPostsSitemap() 53 | 54 | return getServerSideSitemap(sitemap) 55 | } 56 | -------------------------------------------------------------------------------- /dev/src/app/(frontend)/[slug]/page.client.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useHeaderTheme } from '@/providers/HeaderTheme' 3 | import React, { useEffect } from 'react' 4 | 5 | const PageClient: React.FC = () => { 6 | /* Force the header to be dark mode while we have an image behind it */ 7 | const { setHeaderTheme } = useHeaderTheme() 8 | 9 | useEffect(() => { 10 | setHeaderTheme('light') 11 | }, [setHeaderTheme]) 12 | return 13 | } 14 | 15 | export default PageClient 16 | -------------------------------------------------------------------------------- /dev/src/app/(frontend)/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | h1, 7 | h2, 8 | h3, 9 | h4, 10 | h5, 11 | h6 { 12 | font-size: auto; 13 | font-weight: auto; 14 | } 15 | 16 | :root { 17 | --background: 0 0% 100%; 18 | --foreground: 222.2 84% 4.9%; 19 | 20 | --card: 240 5% 96%; 21 | --card-foreground: 222.2 84% 4.9%; 22 | 23 | --popover: 0 0% 100%; 24 | --popover-foreground: 222.2 84% 4.9%; 25 | 26 | --primary: 222.2 47.4% 11.2%; 27 | --primary-foreground: 210 40% 98%; 28 | 29 | --secondary: 210 40% 96.1%; 30 | --secondary-foreground: 222.2 47.4% 11.2%; 31 | 32 | --muted: 210 40% 96.1%; 33 | --muted-foreground: 215.4 16.3% 46.9%; 34 | 35 | --accent: 210 40% 96.1%; 36 | --accent-foreground: 222.2 47.4% 11.2%; 37 | 38 | --destructive: 0 84.2% 60.2%; 39 | --destructive-foreground: 210 40% 98%; 40 | 41 | --border: 240 6% 80%; 42 | --input: 214.3 31.8% 91.4%; 43 | --ring: 222.2 84% 4.9%; 44 | 45 | --radius: 0.2rem; 46 | 47 | --success: 196 52% 74%; 48 | --warning: 34 89% 85%; 49 | --error: 10 100% 86%; 50 | } 51 | 52 | [data-theme='dark'] { 53 | --background: 0 0% 0%; 54 | --foreground: 210 40% 98%; 55 | 56 | --card: 0 0% 4%; 57 | --card-foreground: 210 40% 98%; 58 | 59 | --popover: 222.2 84% 4.9%; 60 | --popover-foreground: 210 40% 98%; 61 | 62 | --primary: 210 40% 98%; 63 | --primary-foreground: 222.2 47.4% 11.2%; 64 | 65 | --secondary: 217.2 32.6% 17.5%; 66 | --secondary-foreground: 210 40% 98%; 67 | 68 | --muted: 217.2 32.6% 17.5%; 69 | --muted-foreground: 215 20.2% 65.1%; 70 | 71 | --accent: 217.2 32.6% 17.5%; 72 | --accent-foreground: 210 40% 98%; 73 | 74 | --destructive: 0 62.8% 30.6%; 75 | --destructive-foreground: 210 40% 98%; 76 | 77 | --border: 0, 0%, 15%, 0.8; 78 | --input: 217.2 32.6% 17.5%; 79 | --ring: 212.7 26.8% 83.9%; 80 | 81 | --success: 196 100% 14%; 82 | --warning: 34 51% 25%; 83 | --error: 10 39% 43%; 84 | } 85 | } 86 | 87 | @layer base { 88 | * { 89 | @apply border-border; 90 | } 91 | body { 92 | @apply bg-background text-foreground min-h-[100vh] flex flex-col; 93 | } 94 | } 95 | 96 | html { 97 | opacity: 0; 98 | } 99 | 100 | html[data-theme='dark'], 101 | html[data-theme='light'] { 102 | opacity: initial; 103 | } 104 | -------------------------------------------------------------------------------- /dev/src/app/(frontend)/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | 3 | import { cn } from '@/utilities/ui' 4 | import { GeistMono } from 'geist/font/mono' 5 | import { GeistSans } from 'geist/font/sans' 6 | import React from 'react' 7 | 8 | import { AdminBar } from '@/components/AdminBar' 9 | import { Footer } from '@/Footer/Component' 10 | import { Header } from '@/Header/Component' 11 | import { Providers } from '@/providers' 12 | import { InitTheme } from '@/providers/Theme/InitTheme' 13 | import { mergeOpenGraph } from '@/utilities/mergeOpenGraph' 14 | import { draftMode } from 'next/headers' 15 | 16 | import './globals.css' 17 | import { getServerSideURL } from '@/utilities/getURL' 18 | 19 | export default async function RootLayout({ children }: { children: React.ReactNode }) { 20 | const { isEnabled } = await draftMode() 21 | 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 36 | 37 |
38 | {children} 39 |