chore: restructures developer-portfolio files between app & payload

This commit is contained in:
PatrikKozak
2023-09-19 13:49:42 -04:00
parent d4b778dcf5
commit 1c5aed7ea6
86 changed files with 3298 additions and 2217 deletions

View File

@@ -4,6 +4,4 @@ PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
PAYLOAD_PUBLIC_DRAFT_SECRET=EXAMPLE_DRAFT_SECRET
COOKIE_DOMAIN=localhost
REVALIDATION_KEY=EXAMPLE_REVALIDATION_KEY
PAYLOAD_SEED=false
PAYLOAD_DROP_DATABASE=false
ENABLE_PAYLOAD_CLOUD=false
ENABLE_PAYLOAD_CLOUD=false

View File

@@ -3,7 +3,4 @@ module.exports = {
extends: ['plugin:@next/next/recommended', '@payloadcms'],
ignorePatterns: ['**/payload-types.ts'],
plugins: ['prettier'],
rules: {
'prettier/prettier': 'error',
},
}

View File

@@ -5,5 +5,5 @@ package-lock.json
.env
.next
.vercel
src/media
.DS_Store
/media
.DS_Store

View File

@@ -1,18 +1,18 @@
# Payload Developer Portfolio Example
This example demonstrates a complete professional portfolio application using [Payload](https://github.com/payloadcms/payload) and NextJS in a single Express server.
This example demonstrates a complete professional portfolio application using [Payload](https://github.com/payloadcms/payload) and NextJS in a single Express server.
## Highlights
- 💪 **Batteries-Included**
- 💪 **Batteries-Included**
- Design beautiful pages, describe portfolio projects, and build forms dynamically without writing code
- 🔎 **SEO-Friendly**
- 🔎 **SEO-Friendly**
- Includes [SEO plugin](https://github.com/payloadcms/plugin-seo) integration to author and preview page metadata
- 🪭 **Customization-Friendly**
- 🪭 **Customization-Friendly**
- Light/dark mode, [@shadcn/ui](https://ui.shadcn.com/) integration, prebuilt animations, and modular CMS "blocks" encourage extension and re-use
- 🏎️ **Performance-Focused**
- 🏎️ **Performance-Focused**
- Uses React Server Components, App Router, and `next/image` to optimize Web Vitals metrics
- 🦯 **Accessibility-Minded**
- 🦯 **Accessibility-Minded**
- Navigation, contrast, dialogs, and forms built with [WCAG 2](https://www.w3.org/WAI/standards-guidelines/wcag/) in mind
## Quick Start
@@ -22,21 +22,24 @@ This example demonstrates a complete professional portfolio application using [P
- [Node](https://nodejs.org/en) 18.x or newer
- [MongoDB](https://www.mongodb.com/try/download/community)
### Setup
To spin up this example locally, follow these steps:
1. First, clone the repo
1. Then `cd YOUR_PROJECT_REPO && cp .env.example .env`
1. Next `yarn && yarn seed` to start the app and seed it with example data
1. Now `open http://localhost:3000` to view the site
### Clone
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details.
If you have not done so already, you need to have standalone copy of this repo on your machine. If you've already cloned this repo, skip to [Development](#development).
### Editing Content
#### Method 1 (recommended)
To access the Admin interface, where you can edit content:
Go to Payload Cloud and [clone this template](https://payloadcms.com/new/clone/developer-portfolio). This will create a new repository on your GitHub account with this template's code which you can then clone to your own machine.
1. Go to `http://localhost:3000/admin`
1. Login with `dev@payloadcms.com` / `test`
### Development
1. First [clone the repo](#clone) if you have not done so already
1. `cd my-project && cp .env.example .env` to copy the example environment variables
1. `yarn && yarn dev` to install dependencies and start the dev server
1. `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.
## How it works
@@ -44,15 +47,118 @@ When you use Payload, you plug it into _**your**_ Express server. That's a funda
One of the strengths of this pattern is that it lets you do powerful things like integrate your Payload instance directly with your front-end. This will allow you to host Payload alongside a fully dynamic, CMS-integrated website or app on a single, combined server—while still getting all of the benefits of a headless CMS.
## Development
### Collections
To spin up this example locally, follow the [Quick Start](#quick-start).
See the [Collections](https://payloadcms.com/docs/configuration/collections) docs for details on how to extend this functionality.
- #### Users (Authentication)
Users in the platform can authenticate to interact with features such as adding projects comments and updating pages. Everyone has the same level of access on the platform. See [Access Control](#access-control) for more details.
For additional help, see the official [Auth Example](https://github.com/payloadcms/payload/tree/master/examples/auth) or the [Authentication](https://payloadcms.com/docs/authentication/overview#authentication-overview) docs.
- #### Projects
Projects are used to showcase your work. All projects are layout builder enabled so you can generate unique layouts for each project using layout-building blocks, see [Layout Builder](#layout-builder) for more details. Projects are also draft-enabled so you can preview them before publishing them to your website, see [Draft Preview](#draft-preview) for more details.
- #### Pages
All pages are layout builder enabled so you can generate unique layouts for each page using layout-building blocks, see [Layout Builder](#layout-builder) for more details. Pages are also draft-enabled so you can preview them before publishing them to your website, see [Draft Preview](#draft-preview) for more details.
- #### Media
This is the uploads enabled collection used by pages, posts, and projects to contain media like images, videos, downloads, and other assets.
- #### Technologies
A taxonomy used on projects to display what technologies were utilized on each project.
### Globals
See the [Globals](https://payloadcms.com/docs/configuration/globals) docs for details on how to extend this functionality.
- `Header`
The data required by the header on your front-end like nav links.
- `Profile`
The data required for the Developer in question of the portfolio.
## Layout Builder
Create unique page and project layouts for any type of content using a powerful layout builder. This template comes pre-configured with the following layout building blocks:
- ### Pages
- Content
- Form Block
- Media Block
- Media Content
- Profile Call to Action
- Project Grid
- ### Projects
- Content
- Form Block
- Media Block
- Media Content
Each block is fully designed and built into the front-end website that comes with this template. See [Developer Portfolio](#developer-portfolio) for more details.
## Draft Preview
All pages and projects are draft-enabled so you can preview them before publishing them to your website. To do this, these collections use [Versions](https://payloadcms.com/docs/configuration/collections#versions) with `drafts` set to `true`. This means that when you create a new page or project, it will be saved as a draft and will not be visible on your portfolio until you publish it. This also means that you can preview your draft before publishing it to your portfolio. To do this, we automatically format a custom URL which redirects to your front-end to securely fetch the draft version of your content.
Since the front-end of this template is statically generated, this also means that pages and projects will need to be regenerated as changes are made to published documents. To do this, we use an `afterChange` hook to regenerate the front-end when a document has changed and its `_status` is `published`.
For more details on how to extend this functionality, see the official [Draft Preview Example](https://github.com/payloadcms/payload/tree/master/examples/draft-preview).
## SEO
This template comes pre-configured with the official [Payload SEO Plugin](https://github.com/payloadcms/plugin-seo) for complete SEO control from the admin panel. All SEO data is fully integrated into the front-end website that comes with this template. See [Developer Portfolio](#developer-potfolio) for more details.
## Developer Portfolio
This template includes a beautifully designed, production-ready front-end built with the [Next.js App Router](https://nextjs.org), served right alongside your Payload app in a single Express server. This makes is so that you can deploy both apps simultaneously and host them together. If you prefer a different front-end framework, this pattern works for any framework that supports a custom server. If you prefer to host your website separately from Payload, you can easily [Eject](#eject) the front-end out from this template to swap in your own, or to use it as a standalone CMS. For more details, see the official [Custom Server Example](https://github.com/payloadcms/payload/tree/master/examples/custom-server).
Core features:
- [Next.js App Router](https://nextjs.org)
- [TypeScript](https://www.typescriptlang.org)
- [React Hook Form](https://react-hook-form.com)
- Authentication
- Fully featured projects
- Publication workflow
- User accounts
- Dark mode
- Pre-made layout building blocks
- SEO
### Cache
Although Next.js includes a robust set of caching strategies out of the box, Payload Cloud proxies and caches all files through Cloudflare using the [Official Cloud Plugin](https://github.com/payloadcms/plugin-cloud). This means that Next.js caching is not needed and is disabled by default. For more details, see the official [Next.js Caching Docs](https://nextjs.org/docs/app/building-your-application/caching).
### Eject
If you prefer another front-end framework or would like to use Payload as a standalone CMS, you can easily eject the front-end from this template. To eject, simply run `yarn eject`. This will uninstall all Next.js related dependencies and delete all files and folders related to the Next.js front-end. It also removes all custom routing from your `server.ts` file and updates your `eslintrc.js`.
> Note: Your eject script may not work as expected if you've made significant modifications to your project. If you run into any issues, compare your project's dependencies and file structure with this template. See [./src/eject](./src/eject) for full details.
For more details on how setup a custom server, see the official [Custom Server Example](https://github.com/payloadcms/payload/tree/master/examples/custom-server).
## Development
To spin up this example locally, follow the [Quick Start](#quick-start). Then [Seed](#seed) the database with a few pages, posts, and projects.
### Seed
On boot, a seed script is included to scaffold a basic database for you to use as an example. This is done by setting the `PAYLOAD_DROP_DATABASE` and `PAYLOAD_SEED` environment variables which are included in the `.env.example` by default. You can remove these from your `.env` to prevent this behavior. You can also freshly seed your project at any time by running `yarn seed`. This seed creates:
To seed the database, you can run `yarn seed`. This template also comes with a `GET /api/seed` endpoint you can use to seed the database from the admin panel.
- An admin user with email `dev@payloadcms.com`, password `test`,
This seed creates:
- An admin user with email `dev@payloadcms.com`, password `test`,
- A `home` page with Profile CTA, project grid, and contact form
- Example header and profile data
- Example media assets

View File

@@ -11,6 +11,6 @@
},
"aliases": {
"components": "src/app/_components",
"utils": "src/utilities"
"utils": "src/payload/utilities"
}
}

View File

@@ -1,5 +1,7 @@
{
"watch": ["server.ts"],
"exec": "ts-node --project tsconfig.server.json src/server.ts",
"ext": "js ts"
}
"ext": "ts",
"exec": "ts-node src/server.ts",
"ignore": [
"src/app"
]
}

View File

@@ -1,26 +1,27 @@
{
"name": "payload-developer-portfolio-template",
"description": "Payload developer portfolio template.",
"description": "Developer portfolio template for Payload",
"version": "1.0.0",
"main": "dist/server.js",
"license": "MIT",
"scripts": {
"dev": "cross-env PAYLOAD_SEED=false PAYLOAD_DROP_DATABASE=false PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon -V",
"seed": "rm -rf src/media .next && cross-env PAYLOAD_SEED=true PAYLOAD_DROP_DATABASE=true PAYLOAD_CONFIG_PATH=src/payload.config.ts ts-node src/server.ts",
"build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload/payload.config.ts nodemon",
"seed": "rm -rf src/media .next && cross-env PAYLOAD_SEED=true PAYLOAD_DROP_DATABASE=true PAYLOAD_CONFIG_PATH=src/payload/payload.config.ts ts-node src/server.ts",
"build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload/payload.config.ts payload build",
"build:server": "tsc --project tsconfig.server.json",
"build:next": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NEXT_BUILD=true node dist/server.js",
"build:next": "cross-env PAYLOAD_CONFIG_PATH=dist/payload/payload.config.js NEXT_BUILD=true node dist/server.js",
"build": "cross-env NODE_ENV=production yarn build:payload && yarn build:server && yarn copyfiles && yarn build:next",
"serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js",
"serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload/payload.config.js NODE_ENV=production node dist/server.js",
"eject": "yarn remove next react react-dom @next/eslint-plugin-next && ts-node eject.ts",
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/",
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types",
"generate:graphQLSchema": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema",
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload/payload.config.ts payload generate:types",
"generate:graphQLSchema": "PAYLOAD_CONFIG_PATH=src/payload/payload.config.ts payload generate:graphQLSchema",
"lint": "eslint src",
"lint:fix": "eslint --fix --ext .ts,.tsx src"
},
"dependencies": {
"@hookform/resolvers": "^3.2.0",
"@payloadcms/plugin-cloud": "^2.0.0",
"@payloadcms/plugin-form-builder": "^1.0.15",
"@payloadcms/plugin-seo": "^1.0.14-canary.0",
"@radix-ui/react-dialog": "^1.0.4",
@@ -34,7 +35,7 @@
"escape-html": "^1.0.3",
"express": "^4.17.1",
"lucide-react": "^0.263.1",
"next": "^13.4.19",
"next": "13.4.19",
"next-themes": "^0.2.1",
"nodemailer": "^6.9.4",
"payload": "1.15.2",

View File

@@ -1,7 +1,7 @@
import { Metadata, ResolvingMetadata } from 'next'
import { notFound, redirect } from 'next/navigation'
import { notFound } from 'next/navigation'
import { Media } from '../../payload-types'
import { Media } from '../../payload/payload-types'
import { ContentLayout } from '../_components/content/contentLayout'
import { fetchPage } from '../_utils/api'
import { parsePreviewOptions } from '../_utils/preview'

View File

@@ -1,4 +1,4 @@
import { cn } from '../../utilities'
import { cn } from '../utilities'
export const PayloadLogo = ({ className = '' }: { className?: string }) => (
<svg

View File

@@ -1,7 +1,7 @@
import { FC } from 'react'
import { Form, Page, Profile, Project } from '../../../payload-types'
import { cn } from '../../../utilities'
import { Form, Page, Profile, Project } from '../../../payload/payload-types'
import { cn } from '../../utilities'
import { ContentBlock } from './contentBlock'
import { FormBlock } from './formBlock'
import { MediaBlock } from './mediaBlock'

View File

@@ -3,7 +3,7 @@ import React, { FC, Fragment, useState } from 'react'
import { useForm } from 'react-hook-form'
import { Data } from 'payload/dist/admin/components/forms/Form/types'
import { Form as FormTypes } from '../../../payload-types'
import { Form as FormTypes } from '../../../payload/payload-types'
import { serverUrl } from '../../_utils/api'
import { Block } from '../ui/block'
import { Button } from '../ui/button'

View File

@@ -1,7 +1,7 @@
import { AnchorHTMLAttributes, ReactNode } from 'react'
import Link, { LinkProps } from 'next/link'
import { Header } from '../../../payload-types'
import { Header } from '../../../payload/payload-types'
export type PayloadLinkType = Header['navItems'][0]['link']

View File

@@ -2,8 +2,8 @@ import { FC, Fragment } from 'react'
import { cva } from 'class-variance-authority'
import Image from 'next/image'
import { Media } from '../../../payload-types'
import { cn } from '../../../utilities'
import { Media } from '../../../payload/payload-types'
import { cn } from '../../utilities'
import { Block, BlockProps } from '../ui/block'
import { MediaDialog } from './mediaDialog'

View File

@@ -1,6 +1,6 @@
import { FC, Fragment } from 'react'
import { Media } from '../../../payload-types'
import { Media } from '../../../payload/payload-types'
import { ContentBlock } from './contentBlock'
import { PayloadLink, PayloadLinkType } from './link'
import { MediaBlock } from './mediaBlock'

View File

@@ -3,7 +3,7 @@
import { FC, useState } from 'react'
import Image from 'next/image'
import { cn } from '../../../utilities'
import { cn } from '../../utilities'
import { Dialog, DialogContent, DialogTrigger } from '../ui/dialog'
interface MediaDialogProps {

View File

@@ -2,7 +2,7 @@ import { cva } from 'class-variance-authority'
import Image from 'next/image'
import Link from 'next/link'
import { Media, Profile } from '../../../payload-types'
import { Media, Profile } from '../../../payload/payload-types'
import { Block } from '../ui/block'
import { RichText } from './richText'
import { SocialIcons } from './socialIcons'

View File

@@ -1,6 +1,6 @@
import { FC } from 'react'
import { Profile, Project } from '../../../../payload-types'
import { Profile, Project } from '../../../../payload/payload-types'
import { FadeInContent } from '../../ui/fadeInContent'
import { ContentLayout } from '../contentLayout'
import { ProfileCTABlock } from '../profileCTABlock'

View File

@@ -1,7 +1,7 @@
'use client'
import { FC } from 'react'
import { Project } from '../../../../payload-types'
import { Project } from '../../../../payload/payload-types'
import { formatMonth } from '../../../_utils/format'
import { ProjectRoles } from './projectRole'

View File

@@ -1,7 +1,7 @@
'use client'
import { FC } from 'react'
import { Project } from '../../../../payload-types'
import { Project } from '../../../../payload/payload-types'
import { MediaBlock } from '../../../_components/content/mediaBlock'
import { TechnologiesUsed } from './technologiesUsed'

View File

@@ -1,6 +1,6 @@
import { FC } from 'react'
import { Project } from '../../../../payload-types'
import { Project } from '../../../../payload/payload-types'
interface ProjectRolesProps {
roles: Project['role']

View File

@@ -1,6 +1,6 @@
import { FC } from 'react'
import { Project, Technology } from '../../../../payload-types'
import { Project, Technology } from '../../../../payload/payload-types'
export interface TechnologiesUsedProps {
technologies: Project['technologiesUsed']

View File

@@ -1,7 +1,7 @@
import { FC } from 'react'
import Link from 'next/link'
import { Media, Project } from '../../../payload-types'
import { Media, Project } from '../../../payload/payload-types'
import { formatYear } from '../../_utils/format'
import { Block } from '../ui/block'
import { MediaBlock } from './mediaBlock'

View File

@@ -2,9 +2,9 @@ import { FC, Suspense } from 'react'
import { EnvelopeOpenIcon } from '@radix-ui/react-icons'
import { GithubIcon, LinkedinIcon, TwitterIcon } from 'lucide-react'
import { Profile } from '../../../payload-types'
import { cn } from '../../../utilities'
import { Profile } from '../../../payload/payload-types'
import { fetchProfile } from '../../_utils/api'
import { cn } from '../../utilities'
import { SocialLink } from '../ui/socialLink'
interface SocialIconsContentProps {

View File

@@ -2,7 +2,7 @@ import { FC } from 'react'
import Image from 'next/image'
import Link from 'next/link'
import { Profile } from '../../../payload-types'
import { Profile } from '../../../payload/payload-types'
import { PayloadLogo } from '../../_assets/payloadLogo'
import { SocialIcons } from '../content/socialIcons'
import { ThemeToggle } from './themeToggle'

View File

@@ -1,7 +1,7 @@
import Image from 'next/image'
import Link from 'next/link'
import { Header, Media, Profile } from '../../../payload-types'
import { Header, Media, Profile } from '../../../payload/payload-types'
import { PayloadLink } from '../content/link'
import { SkipToMainContentLink } from './skipToMainContent'

View File

@@ -4,7 +4,7 @@ import * as React from 'react'
import { Moon, Sun } from 'lucide-react'
import { useTheme } from 'next-themes'
import { cn } from '../../../utilities'
import { cn } from '../../utilities'
import { Button } from '../ui/button'
import {
DropdownMenu,

View File

@@ -2,7 +2,7 @@ import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, VariantProps } from 'class-variance-authority'
import { cn } from '../../../utilities'
import { cn } from '../../utilities'
import { FadeInContent } from './fadeInContent'
const blockVariants = cva('flex col-span-6 justify-center lg:justify-start', {

View File

@@ -2,7 +2,7 @@ import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '../../../utilities'
import { cn } from '../../utilities'
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',

View File

@@ -4,7 +4,7 @@ import * as React from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import { cva } from 'class-variance-authority'
import { cn } from '../../../utilities'
import { cn } from '../../utilities'
const dialogContentVariants = cva(
'p-4 lg:p-12 fixed left-[50%] top-[50%] z-50 translate-x-[-50%] translate-y-[-50%] duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg flex flex-col justify-center items-center',

View File

@@ -2,7 +2,7 @@ import * as React from 'react'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import { Check, ChevronRight, Circle } from 'lucide-react'
import { cn } from '../../../utilities'
import { cn } from '../../utilities'
const DropdownMenu = DropdownMenuPrimitive.Root

View File

@@ -10,7 +10,7 @@ import {
import * as LabelPrimitive from '@radix-ui/react-label'
import { Slot } from '@radix-ui/react-slot'
import { cn } from '../../../utilities'
import { cn } from '../../utilities'
import { Label } from './label'
const Form = FormProvider

View File

@@ -1,6 +1,6 @@
import * as React from 'react'
import { cn } from '../../../utilities'
import { cn } from '../../utilities'
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}

View File

@@ -2,7 +2,7 @@ import * as React from 'react'
import * as LabelPrimitive from '@radix-ui/react-label'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '../../../utilities'
import { cn } from '../../utilities'
const labelVariants = cva(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',

View File

@@ -1,6 +1,6 @@
import * as React from 'react'
import { cn } from '../../../utilities'
import { cn } from '../../utilities'
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}

View File

@@ -1,4 +1,4 @@
import type { Header, Page, Profile, Project } from '../../payload-types'
import type { Header, Page, Profile, Project } from '../../payload/payload-types'
import type { DraftOptions } from './preview'
export const serverUrl = process.env.PAYLOAD_PUBLIC_SERVER_URL ?? 'http://localhost:3000'

View File

@@ -1,7 +1,7 @@
import React from 'react'
import { Metadata, ResolvingMetadata } from 'next'
import { Media } from '../payload-types'
import { Media } from '../payload/payload-types'
import { ContentLayout } from './_components/content/contentLayout'
import { fetchPage, fetchProfile } from './_utils/api'
import { parsePreviewOptions } from './_utils/preview'

View File

@@ -1,7 +1,7 @@
import { Metadata, ResolvingMetadata } from 'next'
import { notFound, redirect } from 'next/navigation'
import { notFound } from 'next/navigation'
import { Media } from '../../../payload-types'
import { Media } from '../../../payload/payload-types'
import { ProjectDetails } from '../../_components/content/projectDetails/projectDetails'
import { fetchProfile, fetchProject } from '../../_utils/api'
import { parsePreviewOptions } from '../../_utils/preview'

View File

@@ -2,7 +2,6 @@ import type { CollectionConfig } from 'payload/types'
import { loggedIn } from '../../access/loggedIn'
import { publishedOrLoggedIn } from '../../access/publishedOrLoggedIn'
import { serverUrl } from '../../app/_utils/api'
import { Content } from '../../blocks/Content'
import { Form } from '../../blocks/Form'
import { MediaBlock } from '../../blocks/Media'
@@ -14,7 +13,7 @@ import { tagRevalidator } from '../../utilities/tagRevalidator'
const formatAppURL = ({ doc }): string => {
const pathToUse = doc.slug === 'home' ? '' : doc.slug
const { pathname } = new URL(`${serverUrl}/${pathToUse}`)
const { pathname } = new URL(`${process.env.PAYLOAD_PUBLIC_SERVER_URL}/${pathToUse}`)
return pathname
}
@@ -23,7 +22,7 @@ export const Pages: CollectionConfig = {
admin: {
useAsTitle: 'title',
preview: doc => {
return `${serverUrl}${formatAppURL({ doc })}?preview=true`
return `${process.env.PAYLOAD_PUBLIC_SERVER_URL}${formatAppURL({ doc })}?preview=true`
},
},
versions: {

View File

@@ -2,7 +2,6 @@ import type { CollectionConfig } from 'payload/types'
import { loggedIn } from '../../access/loggedIn'
import { publishedOrLoggedIn } from '../../access/publishedOrLoggedIn'
import { serverUrl } from '../../app/_utils/api'
import { Content } from '../../blocks/Content'
import { Form } from '../../blocks/Form'
import { MediaBlock } from '../../blocks/Media'
@@ -15,7 +14,7 @@ export const Projects: CollectionConfig = {
admin: {
useAsTitle: 'title',
preview: doc => {
return `${serverUrl}/projects/${doc.slug}?preview=true`
return `${process.env.PAYLOAD_PUBLIC_SERVER_URL}/projects/${doc.slug}?preview=true`
},
},
versions: {

View File

@@ -0,0 +1,40 @@
import React, { Fragment, useCallback, useState } from 'react'
export const SeedButton: React.FC = () => {
const [loading, setLoading] = useState(false)
const [seeded, setSeeded] = useState(false)
const [error, setError] = useState(null)
const handleClick = useCallback(
async e => {
e.preventDefault()
if (loading || seeded) return
setLoading(true)
setTimeout(async () => {
try {
await fetch('/api/seed')
setSeeded(true)
} catch (err) {
setError(err)
}
}, 1000)
},
[loading, seeded],
)
let message = ''
if (loading) message = ' (seeding...)'
if (seeded) message = ' (done!)'
if (error) message = ` (error: ${error})`
return (
<Fragment>
<a href="/api/seed" target="_blank" rel="noopener noreferrer" onClick={handleClick}>
Seed your database
</a>
{message}
</Fragment>
)
}

View File

@@ -0,0 +1,24 @@
@import '~payload/scss';
.dashboard .before-dashboard {
margin-bottom: base(1.5);
&__banner {
& h4 {
margin: 0;
}
}
&__instructions {
list-style: decimal;
margin-bottom: base(0.5);
& li {
width: 100%;
}
}
& a:hover {
opacity: 0.85;
}
}

View File

@@ -0,0 +1,75 @@
import React from 'react'
import { Banner } from 'payload/components'
import { SeedButton } from './SeedButton'
import './index.scss'
const baseClass = 'before-dashboard'
const BeforeDashboard: React.FC = () => {
return (
<div className={baseClass}>
<Banner className={`${baseClass}__banner`} type="success">
<h4>Welcome to your dashboard!</h4>
</Banner>
Here&apos;s what to do next:
<ul className={`${baseClass}__instructions`}>
<li>
<SeedButton />
{
' with a few pages, projects, and technologies to jump-start your new developer portfolio, then '
}
<a href="/">visit your portfolio</a>
{' to see the results.'}
</li>
<li>
If you created this repo using Payload Cloud, head over to GitHub and clone it to your
local machine. It will be under the <i>GitHub Scope</i> that you selected when creating
this project.
</li>
<li>
{'Modify your '}
<a
href="https://payloadcms.com/docs/configuration/collections"
target="_blank"
rel="noopener noreferrer"
>
collections
</a>
{' and add more '}
<a
href="https://payloadcms.com/docs/fields/overview"
target="_blank"
rel="noopener noreferrer"
>
fields
</a>
{' as needed. If you are new to Payload, we also recommend you check out the '}
<a
href="https://payloadcms.com/docs/getting-started/what-is-payload"
target="_blank"
rel="noopener noreferrer"
>
Getting Started
</a>
{' docs.'}
</li>
<li>
Commit and push your changes to the repository to trigger a redeployment of your project.
</li>
</ul>
{'Pro Tip: This block is a '}
<a
href={'https://payloadcms.com/docs/admin/components#base-component-overrides'}
target="_blank"
rel="noopener noreferrer"
>
custom component
</a>
, you can remove it at any time by updating your <strong>payload.config</strong>.
</div>
)
}
export default BeforeDashboard

View File

@@ -0,0 +1,21 @@
import type { PayloadHandler } from 'payload/config'
import { seed as seedScript } from '../seed'
export const seed: PayloadHandler = async (req, res): Promise<void> => {
const { user, payload } = req
if (!user) {
res.status(401).json({ error: 'Unauthorized' })
return
}
try {
await seedScript(payload)
res.json({ success: true })
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error'
payload.logger.error(message)
res.json({ error: message })
}
}

View File

@@ -1,23 +1,24 @@
import { payloadCloud } from '@payloadcms/plugin-cloud'
import formBuilder from '@payloadcms/plugin-form-builder'
import seo from '@payloadcms/plugin-seo'
import dotenv from 'dotenv'
import path from 'path'
dotenv.config({
path: path.resolve(__dirname, '../.env'),
})
import { buildConfig } from 'payload/config'
import { serverUrl } from './app/_utils/api'
import { Media } from './collections/Media'
import { Pages } from './collections/Pages'
import { Projects } from './collections/Projects'
import { Technologies } from './collections/Technologies'
import { Users } from './collections/Users'
import BeforeDashboard from './components/BeforeDashboard'
import { seed } from './endpoints/seed'
import { Header } from './globals/Header'
import { Profile } from './globals/Profile'
dotenv.config({
path: path.resolve(__dirname, '../../.env'),
})
const plugins = [
formBuilder({
fields: {
@@ -37,14 +38,31 @@ const plugins = [
collections: ['pages', 'projects'],
uploadsCollection: 'media',
}),
payloadCloud(),
]
export default buildConfig({
serverURL: serverUrl || '',
admin: {
components: {
// The `BeforeDashboard` component renders the 'welcome' block that you see after logging into your admin panel.
// Feel free to delete this at any time. Simply remove the line below and the import `BeforeDashboard` statement on line 15.
beforeDashboard: [BeforeDashboard],
},
},
serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL,
collections: [Media, Pages, Projects, Technologies, Users],
globals: [Header, Profile],
plugins,
typescript: {
outputFile: path.resolve(__dirname, 'payload-types.ts'),
},
endpoints: [
// The seed endpoint is used to populate the database with some example data
// You should delete this endpoint before deploying your site to production
{
path: '/seed',
method: 'get',
handler: seed,
},
],
})

View File

@@ -0,0 +1,99 @@
import fs from 'fs'
import path from 'path'
import type { Payload } from 'payload'
import type { Header, Profile } from '../payload-types'
import { seedForms } from './forms'
import { seedGlobals } from './globals'
import { seedMedia } from './media'
import { seedPages } from './pages'
import { seedProjects } from './projects'
import { seedTechnologies } from './technologies'
type GlobalType = 'profile' | 'header'
const collections = ['forms', 'media', 'pages', 'projects', 'technologies']
// const globals = ['profile', 'header']
const globals: GlobalType[] = ['profile', 'header']
const getDefaultData = (globalName: GlobalType): Header | Profile => {
switch (globalName) {
case 'profile':
return {
id: '',
name: 'Default',
}
case 'header':
return {
id: '',
}
default:
throw new Error('Invalid global name')
}
}
// Next.js revalidation errors are normal when seeding the database without a server running
// i.e. running `yarn seed` locally instead of using the admin UI within an active app
// The app is not running to revalidate the pages and so the API routes are not available
// These error messages can be ignored: `Error hitting revalidate route for...`
export const seed = async (payload: Payload): Promise<void> => {
payload.logger.info('Seeding database...')
// we need to clear the media directory before seeding
// as well as the collections and globals
// this is because while `yarn seed` drops the database
// the custom `/api/seed` endpoint does not
payload.logger.info(`— Clearing media...`)
const mediaDir = path.resolve(__dirname, '../../media')
if (fs.existsSync(mediaDir)) {
fs.rmdirSync(mediaDir, { recursive: true })
}
payload.logger.info(`— Clearing collections and globals...`)
// clear the database
await Promise.all([
...collections.map(async collection =>
payload.delete({
collection,
where: {},
}),
), // eslint-disable-line function-paren-newline
globals.map(async global => {
const defaultData = getDefaultData(global)
await payload.updateGlobal({
slug: global,
data: defaultData,
})
}),
])
payload.logger.info(`— Seeding form...`)
const forms = await seedForms()
payload.logger.info(`— Seeding media...`)
const media = await seedMedia()
payload.logger.info(`— Seeding globals...`)
await seedGlobals(media)
payload.logger.info(`— Seeding technologies...`)
const technologies = await seedTechnologies()
payload.logger.info(`— Seeding projects...`)
const projects = await seedProjects(media, technologies)
payload.logger.info(`— Seeding pages...`)
await seedPages(forms, projects)
payload.logger.info('Seeded database successfully!')
}

View File

@@ -7,7 +7,7 @@ export async function seedMedia() {
data: {
alt: 'Profile picture',
},
filePath: `${__dirname}/media/headshot.png`,
filePath: `${__dirname}/media/headshot.jpg`,
})
const designDesignFeaturedScreenshot = await payload.create({
@@ -15,7 +15,7 @@ export async function seedMedia() {
data: {
alt: 'Marketing Image for Pre-Launch',
},
filePath: `${__dirname}/media/design-design-featured.png`,
filePath: `${__dirname}/media/design-design-featured.jpg`,
})
const outsideAppFeaturedScreenshot = await payload.create({
@@ -23,7 +23,7 @@ export async function seedMedia() {
data: {
alt: 'Marketing Image for Pre-Launch',
},
filePath: `${__dirname}/media/outside-app-featured.png`,
filePath: `${__dirname}/media/outside-app-featured.jpg`,
})
const designAppFeaturedScreenshot = await payload.create({
@@ -31,7 +31,7 @@ export async function seedMedia() {
data: {
alt: 'Marketing Image for Pre-Launch',
},
filePath: `${__dirname}/media/design-app-featured.png`,
filePath: `${__dirname}/media/design-app-featured.jpg`,
})
const artAppFeaturedScreenshot = await payload.create({
@@ -39,7 +39,7 @@ export async function seedMedia() {
data: {
alt: 'Marketing Image for Pre-Launch',
},
filePath: `${__dirname}/media/art-app-featured.png`,
filePath: `${__dirname}/media/art-app-featured.jpg`,
})
const genericMarketingImageOne = await payload.create({
@@ -47,7 +47,7 @@ export async function seedMedia() {
data: {
alt: 'Marketing Image for Pre-Launch',
},
filePath: `${__dirname}/media/generic-1.png`,
filePath: `${__dirname}/media/generic-1.jpg`,
})
const genericMarketingImageTwo = await payload.create({
@@ -55,7 +55,7 @@ export async function seedMedia() {
data: {
alt: 'Marketing Image for Pre-Launch',
},
filePath: `${__dirname}/media/generic-2.png`,
filePath: `${__dirname}/media/generic-2.jpg`,
})
const genericMarketingImageThree = await payload.create({
@@ -63,7 +63,7 @@ export async function seedMedia() {
data: {
alt: 'UI/UX Examples',
},
filePath: `${__dirname}/media/generic-3.png`,
filePath: `${__dirname}/media/generic-3.jpg`,
})
return {

View File

Before

Width:  |  Height:  |  Size: 280 KiB

After

Width:  |  Height:  |  Size: 280 KiB

View File

Before

Width:  |  Height:  |  Size: 337 KiB

After

Width:  |  Height:  |  Size: 337 KiB

View File

Before

Width:  |  Height:  |  Size: 308 KiB

After

Width:  |  Height:  |  Size: 308 KiB

View File

Before

Width:  |  Height:  |  Size: 371 KiB

After

Width:  |  Height:  |  Size: 371 KiB

View File

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 121 KiB

View File

@@ -1,6 +1,15 @@
import payload from 'payload'
export const seedUsers = async (): Promise<void> => {
await payload.delete({
collection: 'users',
where: {
email: {
equals: 'dev@payloadcms.com',
},
},
})
// create admin
await payload.create({
collection: 'users',

View File

@@ -1,9 +1,7 @@
import type { AfterChangeHook, TypeWithID } from 'payload/dist/collections/config/types'
import { serverUrl } from '../app/_utils/api'
export const formatAppURL = ({ doc }): string => {
const { pathname } = new URL(`${serverUrl}/projects/${doc.slug}`)
const { pathname } = new URL(`${process.env.PAYLOAD_PUBLIC_SERVER_URL}/projects/${doc.slug}`)
return pathname
}
@@ -18,7 +16,7 @@ export const tagRevalidator =
const tag = getTag(doc)
try {
const res = await fetch(
`${serverUrl}/api/revalidate?secret=${process.env.REVALIDATION_KEY}&tag=${tag}`,
`${process.env.PAYLOAD_PUBLIC_SERVER_URL}/api/revalidate?secret=${process.env.REVALIDATION_KEY}&tag=${tag}`,
)
if (res.ok) {

View File

@@ -1,24 +0,0 @@
import type { Payload } from 'payload'
import { seedForms } from './forms'
import { seedGlobals } from './globals'
import { seedMedia } from './media'
import { seedPages } from './pages'
import { seedProjects } from './projects'
import { seedTechnologies } from './technologies'
import { seedUsers } from './users'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const seed = async (_payload: Payload): Promise<void> => {
await seedUsers()
const forms = await seedForms()
const media = await seedMedia()
await seedGlobals(media)
const technologies = await seedTechnologies()
const projects = await seedProjects(media, technologies)
await seedPages(forms, projects)
}

View File

@@ -12,8 +12,7 @@ dotenv.config({
import express from 'express'
import payload from 'payload'
import { serverUrl } from './app/_utils/api'
import { seed } from './seed'
import { seed } from './payload/seed'
const app = express()
const PORT = process.env.PORT || 3000
@@ -36,10 +35,11 @@ const start = async (): Promise<void> => {
if (process.env.PAYLOAD_SEED === 'true') {
payload.logger.info('---- SEEDING DATABASE ----')
await seed(payload)
process.exit()
}
app.listen(PORT, async () => {
payload.logger.info(`App URL: ${serverUrl}`)
payload.logger.info(`App URL: ${process.env.PAYLOAD_PUBLIC_SERVER_URL}`)
})
}

View File

@@ -10,8 +10,7 @@ dotenv.config({
import express from 'express'
import payload from 'payload'
import { serverUrl } from './app/_utils/api'
import { seed } from './seed'
import { seed } from './payload/seed'
const app = express()
const PORT = process.env.PORT || 3000
@@ -29,6 +28,7 @@ const start = async (): Promise<void> => {
if (process.env.PAYLOAD_SEED === 'true') {
payload.logger.info('---- SEEDING DATABASE ----')
await seed(payload)
process.exit()
}
if (process.env.NEXT_BUILD) {
@@ -54,7 +54,7 @@ const start = async (): Promise<void> => {
payload.logger.info('Next.js started')
app.listen(PORT, async () => {
payload.logger.info(`Next.js App URL: ${serverUrl}`)
payload.logger.info(`Next.js App URL: ${process.env.PAYLOAD_PUBLIC_SERVER_URL}`)
})
})
}

View File

@@ -8,6 +8,6 @@
},
"include": [
"src/server.ts",
"src/payload.config.ts",
"src/payload/payload.config.ts",
]
}

File diff suppressed because it is too large Load Diff