1
0
Fork 0

feat: initial payload scaffolding

This commit is contained in:
Z8MB1E 2025-10-24 02:07:13 -04:00
parent e4a367489f
commit 4d639387f4
34 changed files with 948 additions and 339 deletions

View file

@ -1,6 +1,6 @@
{
"singleQuote": true,
"singleQuote": false,
"trailingComma": "all",
"printWidth": 100,
"semi": false
"semi": true
}

View file

@ -1,3 +1,6 @@
{
"recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}

8
.vscode/launch.json vendored
View file

@ -9,8 +9,12 @@
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/node_modules/next/dist/bin/next",
"runtimeArgs": ["--inspect"],
"skipFiles": ["<node_internals>/**"],
"runtimeArgs": [
"--inspect"
],
"skipFiles": [
"<node_internals>/**"
],
"serverReadyAction": {
"action": "debugWithChrome",
"killOnServerStop": true,

View file

@ -4,7 +4,8 @@ This template comes configured with the bare minimum to get started on anything
## Quick start
This template can be deployed directly from our Cloud hosting and it will setup MongoDB and cloud S3 object storage for media.
This template can be deployed directly from our Cloud hosting and it will setup MongoDB and cloud S3 object storage for
media.
## Quick Start - local setup
@ -12,21 +13,27 @@ To spin up this template locally, follow these steps:
### Clone
After you click the `Deploy` button above, you'll want to have standalone copy of this repo on your machine. If you've already cloned this repo, skip to [Development](#development).
After you click the `Deploy` button above, you'll want to have standalone copy of this repo on your machine. If you've
already cloned this repo, skip to [Development](#development).
### Development
1. First [clone the repo](#clone) if you have not done so already
2. `cd my-project && cp .env.example .env` to copy the example environment variables. You'll need to add the `MONGODB_URI` from your Cloud project to your `.env` if you want to use S3 storage and the MongoDB database that was created for you.
2. `cd my-project && cp .env.example .env` to copy the example environment variables. You'll need to add the
`MONGODB_URI` from your Cloud project to your `.env` if you want to use S3 storage and the MongoDB database that was
created for you.
3. `pnpm install && pnpm dev` to install dependencies and start the dev server
4. open `http://localhost:3000` to open the app in your browser
That's it! Changes made in `./src` will be reflected in your app. Follow the on-screen instructions to login and create your first admin user. Then check out [Production](#production) once you're ready to build and serve your app, and [Deployment](#deployment) when you're ready to go live.
That's it! Changes made in `./src` will be reflected in your app. Follow the on-screen instructions to login and create
your first admin user. Then check out [Production](#production) once you're ready to build and serve your app,
and [Deployment](#deployment) when you're ready to go live.
#### Docker (Optional)
If you prefer to use Docker for local development instead of a local MongoDB instance, the provided docker-compose.yml file can be used.
If you prefer to use Docker for local development instead of a local MongoDB instance, the provided docker-compose.yml
file can be used.
To do so, follow these steps:
@ -40,28 +47,35 @@ The Payload config is tailored specifically to the needs of most websites. It is
### Collections
See the [Collections](https://payloadcms.com/docs/configuration/collections) docs for details on how to extend this functionality.
See the [Collections](https://payloadcms.com/docs/configuration/collections) docs for details on how to extend this
functionality.
- #### Users (Authentication)
Users are auth-enabled collections that have access to the admin panel.
For additional help, see the official [Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth) or the [Authentication](https://payloadcms.com/docs/authentication/overview#authentication-overview) docs.
For additional help, see the official [Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth) or
the [Authentication](https://payloadcms.com/docs/authentication/overview#authentication-overview) docs.
- #### Media
This is the uploads enabled collection. It features pre-configured sizes, focal point and manual resizing to help you manage your pictures.
This is the uploads enabled collection. It features pre-configured sizes, focal point and manual resizing to help you
manage your pictures.
### Docker
Alternatively, you can use [Docker](https://www.docker.com) to spin up this template locally. To do so, follow these steps:
Alternatively, you can use [Docker](https://www.docker.com) to spin up this template locally. To do so, follow these
steps:
1. Follow [steps 1 and 2 from above](#development), the docker-compose file will automatically use the `.env` file in your project root
1. Follow [steps 1 and 2 from above](#development), the docker-compose file will automatically use the `.env` file in
your project root
1. Next run `docker-compose up`
1. Follow [steps 4 and 5 from above](#development) to login and create your first admin user
That's it! The Docker instance will help you get up and running quickly while also standardizing the development environment across your teams.
That's it! The Docker instance will help you get up and running quickly while also standardizing the development
environment across your teams.
## Questions
If you have any issues or questions, reach out to us on [Discord](https://discord.com/invite/payload) or start a [GitHub discussion](https://github.com/payloadcms/payload/discussions).
If you have any issues or questions, reach out to us on [Discord](https://discord.com/invite/payload) or start
a [GitHub discussion](https://github.com/payloadcms/payload/discussions).

View file

@ -1,38 +1,38 @@
import { dirname } from 'path'
import { fileURLToPath } from 'url'
import { FlatCompat } from '@eslint/eslintrc'
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
})
});
const eslintConfig = [
...compat.extends('next/core-web-vitals', 'next/typescript'),
...compat.extends("next/core-web-vitals", "next/typescript"),
{
rules: {
'@typescript-eslint/ban-ts-comment': 'warn',
'@typescript-eslint/no-empty-object-type': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': [
'warn',
"@typescript-eslint/ban-ts-comment": "warn",
"@typescript-eslint/no-empty-object-type": "warn",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": [
"warn",
{
vars: 'all',
args: 'after-used',
vars: "all",
args: "after-used",
ignoreRestSiblings: false,
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^(_|ignore)',
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
destructuredArrayIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^(_|ignore)",
},
],
},
},
{
ignores: ['.next/'],
ignores: [".next/"],
},
]
];
export default eslintConfig
export default eslintConfig;

View file

@ -1,17 +1,17 @@
import { withPayload } from '@payloadcms/next/withPayload'
import { withPayload } from "@payloadcms/next/withPayload";
/** @type {import('next').NextConfig} */
const nextConfig = {
// Your Next.js config here
webpack: (webpackConfig) => {
webpackConfig.resolve.extensionAlias = {
'.cjs': ['.cts', '.cjs'],
'.js': ['.ts', '.tsx', '.js', '.jsx'],
'.mjs': ['.mts', '.mjs'],
}
".cjs": [".cts", ".cjs"],
".js": [".ts", ".tsx", ".js", ".jsx"],
".mjs": [".mts", ".mjs"],
};
return webpackConfig
return webpackConfig;
},
}
};
export default withPayload(nextConfig, { devBundleServerPackages: false })
export default withPayload(nextConfig, { devBundleServerPackages: false });

View file

@ -1,16 +1,16 @@
import { defineConfig, devices } from '@playwright/test'
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
import 'dotenv/config'
import "dotenv/config";
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests/e2e',
testDir: "./tests/e2e",
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
@ -18,24 +18,24 @@ export default defineConfig({
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
trace: "on-first-retry",
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
webServer: {
command: 'pnpm dev',
command: "pnpm dev",
reuseExistingServer: true,
url: 'http://localhost:3000',
url: "http://localhost:3000",
},
})
});

View file

@ -1,13 +1,13 @@
import React from 'react'
import './styles.css'
import React from "react";
import "./styles.css";
export const metadata = {
description: 'A blank template using Payload in a Next.js app.',
title: 'Payload Blank Template',
}
description: "A blank template using Payload in a Next.js app.",
title: "Payload Blank Template",
};
export default async function RootLayout(props: { children: React.ReactNode }) {
const { children } = props
const { children } = props;
return (
<html lang="en">
@ -15,5 +15,5 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
<main>{children}</main>
</body>
</html>
)
);
}

View file

@ -1,19 +1,19 @@
import { headers as getHeaders } from 'next/headers.js'
import Image from 'next/image'
import { getPayload } from 'payload'
import React from 'react'
import { fileURLToPath } from 'url'
import { headers as getHeaders } from "next/headers.js";
import Image from "next/image";
import { getPayload } from "payload";
import React from "react";
import { fileURLToPath } from "url";
import config from '@/payload.config'
import './styles.css'
import config from "@/payload.config";
import "./styles.css";
export default async function HomePage() {
const headers = await getHeaders()
const payloadConfig = await config
const payload = await getPayload({ config: payloadConfig })
const { user } = await payload.auth({ headers })
const headers = await getHeaders();
const payloadConfig = await config;
const payload = await getPayload({ config: payloadConfig });
const { user } = await payload.auth({ headers });
const fileURL = `vscode://file/${fileURLToPath(import.meta.url)}`
const fileURL = `webstorm://file/${fileURLToPath(import.meta.url)}`;
return (
<div className="home">
@ -27,8 +27,10 @@ export default async function HomePage() {
width={65}
/>
</picture>
{!user && <h1>Welcome to your new project.</h1>}
{user && <h1>Welcome back, {user.email}</h1>}
<h1>Polaris Task Force</h1>
{/*{!user && <h1>Welcome to your new project.</h1>}
{user && <h1>Welcome back, {user.email}</h1>}*/}
<p>This site is under construction.</p>
<div className="links">
<a
className="admin"
@ -48,12 +50,12 @@ export default async function HomePage() {
</a>
</div>
</div>
<div className="footer">
{/* <div className="footer">
<p>Update this page by editing</p>
<a className="codeLink" href={fileURL}>
<code>app/(frontend)/page.tsx</code>
</a>
</div>
</div>*/}
</div>
)
);
}

View file

@ -1,164 +1,164 @@
:root {
--font-mono: 'Roboto Mono', monospace;
--font-mono: 'Roboto Mono', monospace;
}
* {
box-sizing: border-box;
box-sizing: border-box;
}
html {
font-size: 18px;
line-height: 32px;
font-size: 18px;
line-height: 32px;
background: rgb(0, 0, 0);
-webkit-font-smoothing: antialiased;
background: rgb(0, 0, 0);
-webkit-font-smoothing: antialiased;
}
html,
body,
#app {
height: 100%;
height: 100%;
}
body {
font-family: system-ui;
font-size: 18px;
line-height: 32px;
font-family: system-ui;
font-size: 18px;
line-height: 32px;
margin: 0;
color: rgb(1000, 1000, 1000);
margin: 0;
color: rgb(1000, 1000, 1000);
@media (max-width: 1024px) {
font-size: 15px;
line-height: 24px;
}
@media (max-width: 1024px) {
font-size: 15px;
line-height: 24px;
}
}
img {
max-width: 100%;
height: auto;
display: block;
max-width: 100%;
height: auto;
display: block;
}
h1 {
margin: 40px 0;
font-size: 64px;
line-height: 70px;
font-weight: bold;
margin: 40px 0;
font-size: 64px;
line-height: 70px;
font-weight: bold;
@media (max-width: 1024px) {
margin: 24px 0;
font-size: 42px;
line-height: 42px;
}
@media (max-width: 1024px) {
margin: 24px 0;
font-size: 42px;
line-height: 42px;
}
@media (max-width: 768px) {
font-size: 38px;
line-height: 38px;
}
@media (max-width: 768px) {
font-size: 38px;
line-height: 38px;
}
@media (max-width: 400px) {
font-size: 32px;
line-height: 32px;
}
@media (max-width: 400px) {
font-size: 32px;
line-height: 32px;
}
}
p {
margin: 24px 0;
margin: 24px 0;
@media (max-width: 1024px) {
margin: calc(var(--base) * 0.75) 0;
}
@media (max-width: 1024px) {
margin: calc(var(--base) * 0.75) 0;
}
}
a {
color: currentColor;
color: currentColor;
&:focus {
opacity: 0.8;
outline: none;
}
&:focus {
opacity: 0.8;
outline: none;
}
&:active {
opacity: 0.7;
outline: none;
}
&:active {
opacity: 0.7;
outline: none;
}
}
svg {
vertical-align: middle;
vertical-align: middle;
}
.home {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
height: 100vh;
padding: 45px;
max-width: 1024px;
margin: 0 auto;
overflow: hidden;
@media (max-width: 400px) {
padding: 24px;
}
.content {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
justify-content: center;
flex-grow: 1;
height: 100vh;
padding: 45px;
max-width: 1024px;
margin: 0 auto;
overflow: hidden;
h1 {
text-align: center;
}
}
.links {
display: flex;
align-items: center;
gap: 12px;
a {
text-decoration: none;
padding: 0.25rem 0.5rem;
border-radius: 4px;
@media (max-width: 400px) {
padding: 24px;
}
.admin {
color: rgb(0, 0, 0);
background: rgb(1000, 1000, 1000);
border: 1px solid rgb(0, 0, 0);
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex-grow: 1;
h1 {
text-align: center;
}
}
.docs {
color: rgb(1000, 1000, 1000);
background: rgb(0, 0, 0);
border: 1px solid rgb(1000, 1000, 1000);
}
}
.links {
display: flex;
align-items: center;
gap: 12px;
.footer {
display: flex;
align-items: center;
gap: 8px;
a {
text-decoration: none;
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
@media (max-width: 1024px) {
flex-direction: column;
gap: 6px;
.admin {
color: rgb(0, 0, 0);
background: rgb(1000, 1000, 1000);
border: 1px solid rgb(0, 0, 0);
}
.docs {
color: rgb(1000, 1000, 1000);
background: rgb(0, 0, 0);
border: 1px solid rgb(1000, 1000, 1000);
}
}
p {
margin: 0;
}
.footer {
display: flex;
align-items: center;
gap: 8px;
.codeLink {
text-decoration: none;
padding: 0 0.5rem;
background: rgb(60, 60, 60);
border-radius: 4px;
@media (max-width: 1024px) {
flex-direction: column;
gap: 6px;
}
p {
margin: 0;
}
.codeLink {
text-decoration: none;
padding: 0 0.5rem;
background: rgb(60, 60, 60);
border-radius: 4px;
}
}
}
}

View file

@ -1,24 +1,24 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from 'next'
import type { Metadata } from "next";
import config from '@payload-config'
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
import { importMap } from '../importMap'
import config from "@payload-config";
import { generatePageMetadata, NotFoundPage } from "@payloadcms/next/views";
import { importMap } from "../importMap";
type Args = {
params: Promise<{
segments: string[]
}>
segments: string[];
}>;
searchParams: Promise<{
[key: string]: string | string[]
}>
}
[key: string]: string | string[];
}>;
};
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
generatePageMetadata({ config, params, searchParams });
const NotFound = ({ params, searchParams }: Args) =>
NotFoundPage({ config, params, searchParams, importMap })
NotFoundPage({ config, params, searchParams, importMap });
export default NotFound
export default NotFound;

View file

@ -1,24 +1,24 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from 'next'
import type { Metadata } from "next";
import config from '@payload-config'
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
import { importMap } from '../importMap'
import config from "@payload-config";
import { generatePageMetadata, RootPage } from "@payloadcms/next/views";
import { importMap } from "../importMap";
type Args = {
params: Promise<{
segments: string[]
}>
segments: string[];
}>;
searchParams: Promise<{
[key: string]: string | string[]
}>
}
[key: string]: string | string[];
}>;
};
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
generatePageMetadata({ config, params, searchParams });
const Page = ({ params, searchParams }: Args) =>
RootPage({ config, params, searchParams, importMap })
RootPage({ config, params, searchParams, importMap });
export default Page
export default Page;

View file

@ -1 +1,49 @@
export const importMap = {}
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
export const importMap = {
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
"@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#UploadFeatureClient": UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#BlockquoteFeatureClient": BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#RelationshipFeatureClient": RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#ChecklistFeatureClient": ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#OrderedListFeatureClient": OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#UnorderedListFeatureClient": UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#IndentFeatureClient": IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#AlignFeatureClient": AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#InlineCodeFeatureClient": InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#SuperscriptFeatureClient": SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#SubscriptFeatureClient": SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864
}

View file

@ -1,19 +1,12 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import '@payloadcms/next/css'
import {
REST_DELETE,
REST_GET,
REST_OPTIONS,
REST_PATCH,
REST_POST,
REST_PUT,
} from '@payloadcms/next/routes'
import config from "@payload-config";
import "@payloadcms/next/css";
import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST, REST_PUT, } from "@payloadcms/next/routes";
export const GET = REST_GET(config)
export const POST = REST_POST(config)
export const DELETE = REST_DELETE(config)
export const PATCH = REST_PATCH(config)
export const PUT = REST_PUT(config)
export const OPTIONS = REST_OPTIONS(config)
export const GET = REST_GET(config);
export const POST = REST_POST(config);
export const DELETE = REST_DELETE(config);
export const PATCH = REST_PATCH(config);
export const PUT = REST_PUT(config);
export const OPTIONS = REST_OPTIONS(config);

View file

@ -4,4 +4,4 @@ import config from '@payload-config'
import '@payloadcms/next/css'
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
export const GET = GRAPHQL_PLAYGROUND_GET(config)
export const GET = GRAPHQL_PLAYGROUND_GET(config);

View file

@ -3,6 +3,6 @@
import config from '@payload-config'
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
export const POST = GRAPHQL_POST(config)
export const POST = GRAPHQL_POST(config);
export const OPTIONS = REST_OPTIONS(config)
export const OPTIONS = REST_OPTIONS(config);

View file

@ -1,31 +1,31 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import '@payloadcms/next/css'
import type { ServerFunctionClient } from 'payload'
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
import React from 'react'
import config from "@payload-config";
import "@payloadcms/next/css";
import type { ServerFunctionClient } from "payload";
import { handleServerFunctions, RootLayout } from "@payloadcms/next/layouts";
import React from "react";
import { importMap } from './admin/importMap.js'
import './custom.scss'
import { importMap } from "./admin/importMap.js";
import "./custom.scss";
type Args = {
children: React.ReactNode
}
children: React.ReactNode;
};
const serverFunction: ServerFunctionClient = async function (args) {
'use server'
"use server";
return handleServerFunctions({
...args,
config,
importMap,
})
}
});
};
const Layout = ({ children }: Args) => (
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
{children}
</RootLayout>
)
);
export default Layout
export default Layout;

View file

@ -1,12 +1,12 @@
import configPromise from '@payload-config'
import { getPayload } from 'payload'
import configPromise from "@payload-config";
import { getPayload } from "payload";
export const GET = async (request: Request) => {
const payload = await getPayload({
config: configPromise,
})
});
return Response.json({
message: 'This is an example of a custom route.',
})
}
message: "This is an example of a custom route.",
});
};

View file

@ -1,16 +1,16 @@
import type { CollectionConfig } from 'payload'
import type { CollectionConfig } from "payload";
export const Media: CollectionConfig = {
slug: 'media',
slug: "media",
access: {
read: () => true,
},
fields: [
{
name: 'alt',
type: 'text',
name: "alt",
type: "text",
required: true,
},
],
upload: true,
}
};

View file

@ -1,13 +0,0 @@
import type { CollectionConfig } from 'payload'
export const Users: CollectionConfig = {
slug: 'users',
admin: {
useAsTitle: 'email',
},
auth: true,
fields: [
// Email added by default
// Add more fields as needed
],
}

View file

@ -0,0 +1,57 @@
import { CollectionConfig } from "payload";
import { isAdmin } from "@/utils/access-control/isRole";
export const Awards: CollectionConfig = {
slug: "awards",
access: {
create: isAdmin,
update: isAdmin,
delete: isAdmin,
},
admin: {
useAsTitle: "name",
group: "Users",
},
fields: [
{
name: "name",
type: "text",
required: true,
unique: true,
},
{
name: "description",
type: "richText",
required: true,
},
{
name: "type",
type: "select",
options: [
{
label: "Commendation",
value: "commendation",
},
{
label: "Ribbon",
value: "ribbon",
},
{
label: "Medal",
value: "medal",
},
],
defaultValue: "commendation",
},
{
name: "image",
type: "upload",
relationTo: "media",
},
{
name: "experiencePoints",
type: "number",
min: 1,
},
],
};

View file

@ -0,0 +1,98 @@
import { CollectionConfig } from "payload";
import { Award } from "@/payload-types";
export const Profiles: CollectionConfig = {
slug: "profiles",
admin: {
useAsTitle: "profileTitle",
group: "Users",
defaultColumns: ["profileTitle", "user", "rank"],
},
fields: [
{
name: "user",
type: "relationship",
relationTo: "users",
},
{
name: "rank",
type: "relationship",
relationTo: "ranks",
},
{
name: "profileTitle",
type: "text",
virtual: "user.displayName",
admin: {
hidden: true,
},
label: "Operator",
},
{
name: "dossier",
label: "Personnel Dossier",
type: "group",
fields: [
{
name: "personalExcerpt",
type: "textarea",
},
{
name: "intelExcerpt",
label: "Intelligence Record",
type: "richText",
},
],
},
{
name: "progression",
label: "Progression & XP",
type: "group",
fields: [
{
name: "xp",
label: "Experience Points (XP)",
type: "number",
virtual: true,
defaultValue: 0,
admin: {
readOnly: true,
},
hooks: {
// beforeChange: [({ siblingData }) => delete siblingData["xp"]],
afterRead: [
async ({ req, siblingData }) => {
let xpAcc = 0;
await req.payload
.find({
collection: "awards",
where: {
id: {
in: (siblingData as { awards: number[] }).awards,
},
},
})
.then((res) => {
const awards: Award[] = res.docs;
xpAcc += awards.reduce((prev, cur) => prev + (cur.experiencePoints ?? 0), 0);
});
return xpAcc;
},
],
},
},
{
name: "awards",
type: "relationship",
relationTo: "awards",
hasMany: true,
admin: {
appearance: "drawer",
},
},
],
},
],
};

View file

@ -0,0 +1,40 @@
import { CollectionConfig } from "payload";
import { isAdmin } from "@/utils/access-control/isRole";
export const Ranks: CollectionConfig = {
slug: "ranks",
access: {
create: isAdmin,
update: isAdmin,
delete: isAdmin,
},
admin: {
useAsTitle: "name",
group: "Users"
},
orderable: true,
fields: [
{
name: "name",
type: "text",
required: true,
unique: true,
},
{
name: "abbreviation",
type: "text",
required: true,
unique: true,
},
{
name: "description",
type: "text",
required: true,
},
{
name: "insignia",
type: "upload",
relationTo: "media",
},
],
};

View file

@ -0,0 +1,64 @@
import type { CollectionConfig } from "payload";
import { isAdmin } from "@/utils/access-control/isRole";
export const Users: CollectionConfig = {
slug: "users",
access: {
admin: isAdmin,
unlock: isAdmin,
},
admin: {
useAsTitle: "username",
group: "Users",
},
auth: {
loginWithUsername: {
requireUsername: true,
allowEmailLogin: false,
requireEmail: false,
},
},
fields: [
// Email added by default by Payload, but essentially ignored in this application
{
name: "username",
type: "text",
required: true,
unique: true,
},
{
name: "discordUsername",
type: "text",
required: true,
unique: true,
},
{
name: "displayName",
type: "text",
required: true,
},
{
name: "roles",
type: "select",
hasMany: true,
options: [
{
label: "Guest",
value: "guest",
},
{
label: "User",
value: "user",
},
{
label: "Admin",
value: "admin",
},
{
label: "Developer",
value: "developer",
},
],
},
],
};

View file

@ -69,6 +69,9 @@ export interface Config {
collections: {
users: User;
media: Media;
ranks: Rank;
profiles: Profile;
awards: Award;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
@ -77,12 +80,15 @@ export interface Config {
collectionsSelect: {
users: UsersSelect<false> | UsersSelect<true>;
media: MediaSelect<false> | MediaSelect<true>;
ranks: RanksSelect<false> | RanksSelect<true>;
profiles: ProfilesSelect<false> | ProfilesSelect<true>;
awards: AwardsSelect<false> | AwardsSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: {
defaultIDType: string;
defaultIDType: number;
};
globals: {};
globalsSelect: {};
@ -97,20 +103,18 @@ export interface Config {
}
export interface UserAuthOperations {
forgotPassword: {
email: string;
password: string;
username: string;
};
login: {
email: string;
password: string;
username: string;
};
registerFirstUser: {
email: string;
password: string;
username: string;
};
unlock: {
email: string;
password: string;
username: string;
};
}
/**
@ -118,10 +122,14 @@ export interface UserAuthOperations {
* via the `definition` "users".
*/
export interface User {
id: string;
id: number;
discordUsername: string;
displayName: string;
roles?: ('guest' | 'user' | 'admin' | 'developer')[] | null;
updatedAt: string;
createdAt: string;
email: string;
email?: string | null;
username: string;
resetPasswordToken?: string | null;
resetPasswordExpiration?: string | null;
salt?: string | null;
@ -142,7 +150,7 @@ export interface User {
* via the `definition` "media".
*/
export interface Media {
id: string;
id: number;
alt: string;
updatedAt: string;
createdAt: string;
@ -156,25 +164,113 @@ export interface Media {
focalX?: number | null;
focalY?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "ranks".
*/
export interface Rank {
id: number;
_order?: string | null;
name: string;
abbreviation: string;
description: string;
insignia?: (number | null) | Media;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "profiles".
*/
export interface Profile {
id: number;
user?: (number | null) | User;
rank?: (number | null) | Rank;
profileTitle?: string | null;
dossier?: {
personalExcerpt?: string | null;
intelExcerpt?: {
root: {
type: string;
children: {
type: any;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
} | null;
};
progression?: {
xp?: number | null;
awards?: (number | Award)[] | null;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "awards".
*/
export interface Award {
id: number;
name: string;
description: {
root: {
type: string;
children: {
type: any;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
};
type?: ('commendation' | 'ribbon' | 'medal') | null;
image?: (number | null) | Media;
experiencePoints?: number | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents".
*/
export interface PayloadLockedDocument {
id: string;
id: number;
document?:
| ({
relationTo: 'users';
value: string | User;
value: number | User;
} | null)
| ({
relationTo: 'media';
value: string | Media;
value: number | Media;
} | null)
| ({
relationTo: 'ranks';
value: number | Rank;
} | null)
| ({
relationTo: 'profiles';
value: number | Profile;
} | null)
| ({
relationTo: 'awards';
value: number | Award;
} | null);
globalSlug?: string | null;
user: {
relationTo: 'users';
value: string | User;
value: number | User;
};
updatedAt: string;
createdAt: string;
@ -184,10 +280,10 @@ export interface PayloadLockedDocument {
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: string;
id: number;
user: {
relationTo: 'users';
value: string | User;
value: number | User;
};
key?: string | null;
value?:
@ -207,7 +303,7 @@ export interface PayloadPreference {
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration {
id: string;
id: number;
name?: string | null;
batch?: number | null;
updatedAt: string;
@ -218,9 +314,13 @@ export interface PayloadMigration {
* via the `definition` "users_select".
*/
export interface UsersSelect<T extends boolean = true> {
discordUsername?: T;
displayName?: T;
roles?: T;
updatedAt?: T;
createdAt?: T;
email?: T;
username?: T;
resetPasswordToken?: T;
resetPasswordExpiration?: T;
salt?: T;
@ -253,6 +353,55 @@ export interface MediaSelect<T extends boolean = true> {
focalX?: T;
focalY?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "ranks_select".
*/
export interface RanksSelect<T extends boolean = true> {
_order?: T;
name?: T;
abbreviation?: T;
description?: T;
insignia?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "profiles_select".
*/
export interface ProfilesSelect<T extends boolean = true> {
user?: T;
rank?: T;
profileTitle?: T;
dossier?:
| T
| {
personalExcerpt?: T;
intelExcerpt?: T;
};
progression?:
| T
| {
xp?: T;
awards?: T;
};
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "awards_select".
*/
export interface AwardsSelect<T extends boolean = true> {
name?: T;
description?: T;
type?: T;
image?: T;
experiencePoints?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".

View file

@ -1,17 +1,20 @@
// storage-adapter-import-placeholder
import { postgresAdapter } from '@payloadcms/db-postgres'
import { payloadCloudPlugin } from '@payloadcms/payload-cloud'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import path from 'path'
import { buildConfig } from 'payload'
import { fileURLToPath } from 'url'
import sharp from 'sharp'
import { postgresAdapter } from "@payloadcms/db-postgres";
import { payloadCloudPlugin } from "@payloadcms/payload-cloud";
import { lexicalEditor } from "@payloadcms/richtext-lexical";
import path from "path";
import { buildConfig } from "payload";
import { fileURLToPath } from "url";
import sharp from "sharp";
import { Users } from './collections/Users'
import { Media } from './collections/Media'
import { Users } from "@/collections/users/Users";
import { Media } from "@/collections/Media";
import { Profiles } from "@/collections/users/Profiles";
import { Ranks } from "@/collections/users/Ranks";
import { Awards } from "@/collections/users/Awards";
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);
export default buildConfig({
admin: {
@ -20,15 +23,15 @@ export default buildConfig({
baseDir: path.resolve(dirname),
},
},
collections: [Users, Media],
collections: [Users, Media, Ranks, Profiles, Awards],
editor: lexicalEditor(),
secret: process.env.PAYLOAD_SECRET || '',
secret: process.env.PAYLOAD_SECRET || "",
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
outputFile: path.resolve(dirname, "payload-types.ts"),
},
db: postgresAdapter({
pool: {
connectionString: process.env.DATABASE_URI || '',
connectionString: process.env.DATABASE_URI || "",
},
}),
sharp,
@ -36,4 +39,4 @@ export default buildConfig({
payloadCloudPlugin(),
// storage-adapter-placeholder
],
})
});

122
src/tools/seed/seedRanks.ts Normal file
View file

@ -0,0 +1,122 @@
import { getPayload } from "payload";
import config from "@payload-config";
const DEFAULT_USMC_RANKS = [
{
name: "Recruit",
abbreviation: "Rct",
},
{
name: "Private",
abbreviation: "Pvt",
},
{
name: "Private First Class",
abbreviation: "PFC",
},
{
name: "Lance Corporal",
abbreviation: "LCpl",
},
{
name: "Corporal",
abbreviation: "Cpl",
},
{
name: "Sergeant",
abbreviation: "Sgt",
},
{
name: "Staff Sergeant",
abbreviation: "SSgt",
},
{
name: "Gunnery Sergeant",
abbreviation: "GySgt",
},
{
name: "Master Sergeant",
abbreviation: "MSgt",
},
{
name: "First Sergeant",
abbreviation: "1stSgt",
},
{
name: "Sergeant Major",
abbreviation: "SgtMaj",
},
{
name: "Master Gunnery Sergeant",
abbreviation: "MGySgt",
},
{
name: "Second Lieutenant",
abbreviation: "2ndLt",
},
{
name: "First Lieutenant",
abbreviation: "1stLt",
},
{
name: "Captain",
abbreviation: "Cpt",
},
{
name: "Major",
abbreviation: "Maj",
},
{
name: "Lieutenant Colonel",
abbreviation: "LtCol",
},
{
name: "Colonel",
abbreviation: "Col",
},
{
name: "Brigadier General",
abbreviation: "BGen",
},
{
name: "Major General",
abbreviation: "MajGen",
},
{
name: "Lieutenant General",
abbreviation: "LtGen",
},
{
name: "General",
abbreviation: "Gen",
},
];
export const seedRanks = async () => {
const payload = await getPayload({ config });
payload.logger.info("Seeding rank data...");
payload.logger.warn("Deleting all existing ranks...");
await payload.delete({
collection: "ranks",
where: {
name: { exists: true },
},
});
payload.logger.info(`Creating ${DEFAULT_USMC_RANKS.length} new ranks...`);
for (const rank of DEFAULT_USMC_RANKS) {
const result = await payload.create({
collection: "ranks",
data: {
...rank,
description: "A rank that needs a description. ",
},
});
payload.logger.info(`Rank '${result.name}' created.`);
}
};
await seedRanks();

View file

@ -0,0 +1,7 @@
import { User } from "@/payload-types";
export default function hasRoles(roles: NonNullable<User['roles']>, user: User | null | undefined) {
if (!user) return false;
return roles.some((role) => user.roles?.some((urole) => urole === role || urole === "developer"));
}

View file

@ -0,0 +1,18 @@
import hasRoles from "@/utils/access-control/hasRoles";
import { PayloadRequest } from "payload";
export function isDeveloper({ req: { user } }: { req: PayloadRequest }) {
return hasRoles(["developer"], user);
}
export function isGuest({ req: { user } }: { req: PayloadRequest }) {
return hasRoles(["guest"], user);
}
export function isUser({ req: { user } }: { req: PayloadRequest }) {
return hasRoles(["user"], user);
}
export function isAdmin({ req: { user } }: { req: PayloadRequest }) {
return hasRoles(["admin"], user);
}

View file

@ -1,20 +1,20 @@
import { test, expect, Page } from '@playwright/test'
import { expect, Page, test } from "@playwright/test";
test.describe('Frontend', () => {
let page: Page
test.describe("Frontend", () => {
let page: Page;
test.beforeAll(async ({ browser }, testInfo) => {
const context = await browser.newContext()
page = await context.newPage()
})
const context = await browser.newContext();
page = await context.newPage();
});
test('can go on homepage', async ({ page }) => {
await page.goto('http://localhost:3000')
test("can go on homepage", async ({ page }) => {
await page.goto("http://localhost:3000");
await expect(page).toHaveTitle(/Payload Blank Template/)
await expect(page).toHaveTitle(/Payload Blank Template/);
const heading = page.locator('h1').first()
const heading = page.locator("h1").first();
await expect(heading).toHaveText('Welcome to your new project.')
})
})
await expect(heading).toHaveText("Welcome to your new project.");
});
});

View file

@ -1,20 +1,20 @@
import { getPayload, Payload } from 'payload'
import config from '@/payload.config'
import { getPayload, Payload } from "payload";
import config from "@/payload.config";
import { describe, it, beforeAll, expect } from 'vitest'
import { beforeAll, describe, expect, it } from "vitest";
let payload: Payload
let payload: Payload;
describe('API', () => {
describe("API", () => {
beforeAll(async () => {
const payloadConfig = await config
payload = await getPayload({ config: payloadConfig })
})
const payloadConfig = await config;
payload = await getPayload({ config: payloadConfig });
});
it('fetches users', async () => {
it("fetches users", async () => {
const users = await payload.find({
collection: 'users',
})
expect(users).toBeDefined()
})
})
collection: "users",
});
expect(users).toBeDefined();
});
});

View file

@ -30,7 +30,7 @@
"./src/payload.config.ts"
]
},
"target": "ES2022",
"target": "ES2022"
},
"include": [
"next-env.d.ts",
@ -40,5 +40,5 @@
],
"exclude": [
"node_modules"
],
]
}

View file

@ -1,12 +1,12 @@
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths'
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [tsconfigPaths(), react()],
test: {
environment: 'jsdom',
setupFiles: ['./vitest.setup.ts'],
include: ['tests/int/**/*.int.spec.ts'],
environment: "jsdom",
setupFiles: ["./vitest.setup.ts"],
include: ["tests/int/**/*.int.spec.ts"],
},
})
});

View file

@ -1,4 +1,4 @@
// Any setup scripts you might need go here
// Load .env files
import 'dotenv/config'
import "dotenv/config";