├── .env.example ├── .eslintignore ├── .eslintrc.json.bak ├── .gitignore ├── .prettierrc.json ├── README.md ├── components.json ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── arrow.svg ├── blur.png ├── crosshair.svg ├── gradient.webp ├── next.svg ├── pattern.png ├── payload.svg └── scanline-light.png ├── src ├── actions │ └── subscribeToNewsletter.ts ├── app │ ├── (app) │ │ ├── globals.css │ │ ├── globals.scss │ │ ├── icon.svg │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── posts │ │ │ └── [postId] │ │ │ └── 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 ├── collections │ ├── Media.ts │ ├── Posts.ts │ └── Users.ts ├── components │ ├── Background │ │ ├── index.tsx │ │ └── styles.module.scss │ ├── Links │ │ ├── index.tsx │ │ └── styles.module.scss │ ├── Logos │ │ ├── index.tsx │ │ └── styles.module.scss │ ├── RichText │ │ ├── index.module.scss │ │ ├── index.tsx │ │ └── serialize │ │ │ ├── index.tsx │ │ │ └── nodeFormat.ts │ ├── newsletter │ │ ├── subscribeBtn.tsx │ │ └── topPosts.tsx │ └── ui │ │ ├── button.tsx │ │ ├── card.tsx │ │ └── input.tsx ├── hooks │ └── afterPostCreate.ts ├── lib │ ├── payload.ts │ └── utils.ts ├── middleware.ts ├── payload-types.ts └── payload.config.ts ├── tailwind.config.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | POSTGRES_URL=postgres://127.0.0.1:5432/payload 2 | PAYLOAD_SECRET= 3 | BLOB_READ_WRITE_TOKEN=vercel_blob_rw_examplestoreid_somethingelse 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .tmp 2 | **/.git 3 | **/.hg 4 | **/.pnp.* 5 | **/.svn 6 | **/.yarn/** 7 | **/build 8 | **/dist/** 9 | **/node_modules 10 | **/temp 11 | -------------------------------------------------------------------------------- /.eslintrc.json.bak: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | .env 4 | 5 | # dependencies 6 | /node_modules 7 | /.pnp 8 | .pnp.js 9 | .yarn/install-state.gz 10 | 11 | # testing 12 | /coverage 13 | 14 | # next.js 15 | /.next/ 16 | /out/ 17 | 18 | # production 19 | /build 20 | 21 | # misc 22 | .DS_Store 23 | *.pem 24 | 25 | # debug 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | # local env files 31 | .env*.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 100, 5 | "semi": false 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fpayloadcms%2Fpayload%2Ftemplates%2Fvercel-postgres&project-name=payload-project&env=PAYLOAD_SECRET&build-command=pnpm%20run%20ci&stores=%5B%7B%22type%22%3A%22postgres%22%7D%2C%7B%22type%22%3A%22blob%22%7D%5D) 2 | 3 | > One-click deployment template of Payload on Vercel 4 | 5 | - [Payload](https://github.com/payloadcms/payload) already installed into Next.js 6 | - PostgreSQL adapter configured for Neon 7 | - Cloud Storage plugin configured for [Vercel Blob Storage](https://vercel.com/docs/storage/vercel-blob) 8 | -------------------------------------------------------------------------------- /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.ts", 8 | "css": "src/app/(app)/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | import { withPayload } from '@payloadcms/next/withPayload' 2 | /** @type {import('next').NextConfig} */ 3 | const nextConfig = {} 4 | 5 | export default withPayload(nextConfig) 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vercel-deploy-payload-postgres", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "ci": "payload migrate && pnpm build", 8 | "dev": "next dev", 9 | "generate:types": "payload generate:types", 10 | "lint": "next lint", 11 | "payload": "cross-env NODE_OPTIONS=--no-deprecation payload", 12 | "start": "next start" 13 | }, 14 | "dependencies": { 15 | "@clerk/nextjs": "^6.0.1", 16 | "@lexical/list": "^0.18.0", 17 | "@lexical/rich-text": "^0.18.0", 18 | "@payloadcms/db-postgres": "beta", 19 | "@payloadcms/next": "beta", 20 | "@payloadcms/richtext-lexical": "3.0.0-beta.115", 21 | "@payloadcms/storage-vercel-blob": "beta", 22 | "@radix-ui/react-slot": "^1.1.0", 23 | "@types/escape-html": "^1.0.4", 24 | "@vercel/blob": "^0.22.3", 25 | "class-variance-authority": "^0.7.0", 26 | "clsx": "^2.1.1", 27 | "cross-env": "^7.0.3", 28 | "escape-html": "^1.0.3", 29 | "lexical": "^0.18.0", 30 | "lucide-react": "^0.453.0", 31 | "next": "15.0.0-canary.173", 32 | "payload": "beta", 33 | "react": "19.0.0-rc-3edc000d-20240926", 34 | "react-dom": "19.0.0-rc-3edc000d-20240926", 35 | "resend": "^4.0.0", 36 | "sharp": "0.32.6", 37 | "tailwind-merge": "^2.5.4", 38 | "tailwindcss-animate": "^1.0.7" 39 | }, 40 | "devDependencies": { 41 | "@types/node": "^22.5.4", 42 | "@types/react": "npm:types-react@19.0.0-rc.1", 43 | "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1", 44 | "autoprefixer": "^10.0.1", 45 | "eslint": "^8", 46 | "eslint-config-next": "15.0.0-canary.173", 47 | "postcss": "^8", 48 | "tailwindcss": "^3.3.0", 49 | "typescript": "5.6.2" 50 | }, 51 | "engines": { 52 | "node": "^18.20.2 || >=20.9.0" 53 | }, 54 | "pnpm": { 55 | "overrides": { 56 | "@types/react": "npm:types-react@19.0.0-rc.1", 57 | "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1" 58 | } 59 | }, 60 | "overrides": { 61 | "@types/react": "npm:types-react@19.0.0-rc.1", 62 | "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/blur.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdecoded-tutorials/newsletter-app-tutorial/a9046243e9562eaa8d7b5f14a18483be5a78cc91/public/blur.png -------------------------------------------------------------------------------- /public/crosshair.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/gradient.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdecoded-tutorials/newsletter-app-tutorial/a9046243e9562eaa8d7b5f14a18483be5a78cc91/public/gradient.webp -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdecoded-tutorials/newsletter-app-tutorial/a9046243e9562eaa8d7b5f14a18483be5a78cc91/public/pattern.png -------------------------------------------------------------------------------- /public/payload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/scanline-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdecoded-tutorials/newsletter-app-tutorial/a9046243e9562eaa8d7b5f14a18483be5a78cc91/public/scanline-light.png -------------------------------------------------------------------------------- /src/actions/subscribeToNewsletter.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | import { Resend } from 'resend' 3 | 4 | async function subscribeToNewsletter(state: string, formData: FormData): Promise { 5 | const apiKey = process.env.RESEND_API_KEY 6 | const audienceId = process.env.RESEND_AUDIENCE_ID 7 | 8 | if (!apiKey || !audienceId) { 9 | return 'Missing API Key or Audience ID' 10 | } 11 | 12 | const resend = new Resend(apiKey) 13 | 14 | const email = formData.get('email') as string 15 | try { 16 | const response = await resend.contacts.create({ 17 | email: email, 18 | unsubscribed: false, 19 | audienceId: audienceId, 20 | }) 21 | console.log(response) 22 | return 'Subscribed to newsletter' 23 | } catch (error) { 24 | console.error(error) 25 | return 'Error subscribing to newsletter' 26 | } 27 | } 28 | 29 | export default subscribeToNewsletter 30 | -------------------------------------------------------------------------------- /src/app/(app)/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 0 0% 3.9%; 9 | 10 | --muted: 0 0% 96.1%; 11 | --muted-foreground: 0 0% 45.1%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 0 0% 3.9%; 15 | 16 | --border: 0 0% 89.8%; 17 | --input: 0 0% 89.8%; 18 | 19 | --card: 0 0% 100%; 20 | --card-foreground: 0 0% 3.9%; 21 | 22 | --primary: 0 0% 9%; 23 | --primary-foreground: 0 0% 98%; 24 | 25 | --secondary: 0 0% 96.1%; 26 | --secondary-foreground: 0 0% 9%; 27 | 28 | --accent: 0 0% 96.1%; 29 | --accent-foreground: 0 0% 9%; 30 | 31 | --destructive: 0 84.2% 60.2%; 32 | --destructive-foreground: 0 0% 98%; 33 | 34 | --ring: 0 0% 3.9%; 35 | 36 | --radius: 0.5rem; 37 | 38 | --chart-1: 12 76% 61%; 39 | 40 | --chart-2: 173 58% 39%; 41 | 42 | --chart-3: 197 37% 24%; 43 | 44 | --chart-4: 43 74% 66%; 45 | 46 | --chart-5: 27 87% 67%; 47 | } 48 | 49 | .dark { 50 | --background: 0 0% 3.9%; 51 | --foreground: 0 0% 98%; 52 | 53 | --muted: 0 0% 14.9%; 54 | --muted-foreground: 0 0% 63.9%; 55 | 56 | --accent: 0 0% 14.9%; 57 | --accent-foreground: 0 0% 98%; 58 | 59 | --popover: 0 0% 3.9%; 60 | --popover-foreground: 0 0% 98%; 61 | 62 | --border: 0 0% 14.9%; 63 | --input: 0 0% 14.9%; 64 | 65 | --card: 0 0% 3.9%; 66 | --card-foreground: 0 0% 98%; 67 | 68 | --primary: 0 0% 98%; 69 | --primary-foreground: 0 0% 9%; 70 | 71 | --secondary: 0 0% 14.9%; 72 | --secondary-foreground: 0 0% 98%; 73 | 74 | --destructive: 0 62.8% 30.6%; 75 | --destructive-foreground: 0 0% 98%; 76 | 77 | --ring: 0 0% 83.1%; 78 | 79 | --radius: 0.5rem; 80 | 81 | --chart-1: 220 70% 50%; 82 | 83 | --chart-2: 160 60% 45%; 84 | 85 | --chart-3: 30 80% 55%; 86 | 87 | --chart-4: 280 65% 60%; 88 | 89 | --chart-5: 340 75% 55%; 90 | } 91 | } 92 | 93 | @layer base { 94 | * { 95 | @apply border-border; 96 | } 97 | body { 98 | @apply bg-background text-foreground; 99 | font-feature-settings: "rlig" 1, "calt" 1; 100 | } 101 | } -------------------------------------------------------------------------------- /src/app/(app)/globals.scss: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | :root { 6 | --gutter: calc(50vw - 720px); 7 | --gridline: linear-gradient( 8 | rgba(255, 255, 255, 0.1), 9 | rgba(255, 255, 255, 0.025), 10 | rgba(255, 255, 255, 0.025), 11 | rgba(255, 255, 255, 0.1), 12 | rgba(255, 255, 255, 0.1) 13 | ); 14 | 15 | @media screen and (max-width: 1600px) { 16 | --gutter: 80px; 17 | } 18 | 19 | @media screen and (max-width: 1200px) { 20 | --gutter: 40px; 21 | --gridline: rgba(255, 255, 255, 0.1); 22 | } 23 | 24 | @media screen and (max-width: 600px) { 25 | --gutter: 20px; 26 | } 27 | } 28 | 29 | html { 30 | width: 100%; 31 | height: 100%; 32 | background-color: #000000; 33 | } 34 | 35 | body { 36 | color: #ffffff; 37 | margin: 0; 38 | width: 100%; 39 | padding: 0; 40 | overflow-x: hidden; 41 | } 42 | 43 | main { 44 | display: flex; 45 | flex-direction: column; 46 | align-items: center; 47 | justify-content: space-between; 48 | width: 100%; 49 | height: 100vh; 50 | pointer-events: none; 51 | padding: 80px var(--gutter); 52 | 53 | @media screen and (max-width: 1200px) { 54 | height: auto; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/app/(app)/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/app/(app)/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | 3 | import { Inter } from 'next/font/google' 4 | import React from 'react' 5 | 6 | import './globals.css' 7 | 8 | import { 9 | ClerkProvider, 10 | SignInButton, 11 | SignedIn, 12 | SignedOut, 13 | UserButton 14 | } from '@clerk/nextjs' 15 | 16 | import Link from 'next/link' 17 | 18 | const inter = Inter({ subsets: ['latin'] }) 19 | 20 | export const metadata: Metadata = { 21 | description: 'A Payload starter project with Next.js, Vercel Postgres, and Vercel Blob Storage.', 22 | title: 'Payload Vercel Starter', 23 | } 24 | 25 | export default function RootLayout({ 26 | children, 27 | }: Readonly<{ 28 | children: React.ReactNode 29 | }>) { 30 | return ( 31 | 32 | 33 | 34 |
35 | 36 | Web Weekly 37 | 38 | 50 |
51 | {children} 52 |
53 | 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /src/app/(app)/page.tsx: -------------------------------------------------------------------------------- 1 | import { Background } from '@/components/Background' 2 | import { Links } from '@/components/Links' 3 | import { Logos } from '@/components/Logos' 4 | import Link from 'next/link' 5 | import React from 'react' 6 | import SubscribeBtn from '@/components/newsletter/subscribeBtn' 7 | import TopPosts from '@/components/newsletter/topPosts' 8 | 9 | 10 | export default async function Home() { 11 | return ( 12 |
13 |
14 |
15 |
16 |
17 |
18 |

Stay Informed with Our Newsletter

19 |

Get the latest updates, news, and exclusive content delivered straight to your inbox.

20 |
21 |
22 | 23 |
24 |
25 |
26 |
27 |
28 |
29 |

Features

30 |
31 |
32 |

Weekly Updates

33 |

Receive curated content every week, keeping you informed and inspired. 34 |

35 |
36 |
37 |

Exclusive Insights

38 |

Gain access to expert analysis and insider knowledge in your field. 39 |

40 |
41 |
42 |

Community Access

43 |

Join a network of like-minded professionals and enthusiasts. 44 |

45 |
46 |
47 |
48 |
49 |
50 | 51 |
52 |
53 |
Web Weekly
54 |
55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /src/app/(app)/posts/[postId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { getPayload } from "@/lib/payload"; 2 | import RichText from "@/components/RichText"; 3 | 4 | const page = async ({ params }: any) => { 5 | const { postId } = await params; 6 | const payload = await getPayload(); 7 | let post = await payload.find({ 8 | collection: 'posts', 9 | where: { 10 | id: { 11 | equals: postId 12 | } 13 | } 14 | }) 15 | 16 | if (!post || !post.docs || post.docs.length === 0 || !postId) { 17 | return (
18 |

Post not found

19 |
) 20 | } 21 | return ( 22 |
23 |

{post.docs[0].title}

24 | 25 |
26 | ) 27 | } 28 | 29 | export default page; -------------------------------------------------------------------------------- /src/app/(payload)/admin/[[...segments]]/not-found.tsx: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import type { Metadata } from 'next' 4 | 5 | import config from '@payload-config' 6 | import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views' 7 | import { importMap } from "../importMap.js" 8 | 9 | type Args = { 10 | params: Promise<{ 11 | segments: string[] 12 | }> 13 | searchParams: Promise<{ 14 | [key: string]: string | string[] 15 | }> 16 | } 17 | 18 | export const generateMetadata = ({ params, searchParams }: Args): Promise => 19 | generatePageMetadata({ config, params, searchParams }) 20 | 21 | const NotFound = ({ params, searchParams }: Args) => NotFoundPage({ config, importMap, params, searchParams }) 22 | 23 | export default NotFound 24 | -------------------------------------------------------------------------------- /src/app/(payload)/admin/[[...segments]]/page.tsx: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import type { Metadata } from 'next' 4 | import { importMap } from "../importMap.js" 5 | 6 | import config from '@payload-config' 7 | import { RootPage, generatePageMetadata } from '@payloadcms/next/views' 8 | 9 | type Args = { 10 | params: Promise<{ 11 | segments: string[] 12 | }> 13 | searchParams: Promise<{ 14 | [key: string]: string | string[] 15 | }> 16 | } 17 | 18 | export const generateMetadata = ({ params, searchParams }: Args): Promise => 19 | generatePageMetadata({ config, params, searchParams }) 20 | 21 | const Page = ({ params, searchParams }: Args) => RootPage({ config, importMap, params, searchParams }) 22 | 23 | export default Page 24 | -------------------------------------------------------------------------------- /src/app/(payload)/admin/importMap.js: -------------------------------------------------------------------------------- 1 | import { RichTextCell as RichTextCell_0 } from '@payloadcms/richtext-lexical/client' 2 | import { RichTextField as RichTextField_1 } from '@payloadcms/richtext-lexical/client' 3 | import { getGenerateComponentMap as getGenerateComponentMap_2 } from '@payloadcms/richtext-lexical/generateComponentMap' 4 | import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_3 } from '@payloadcms/richtext-lexical/client' 5 | import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_4 } from '@payloadcms/richtext-lexical/client' 6 | import { UploadFeatureClient as UploadFeatureClient_5 } from '@payloadcms/richtext-lexical/client' 7 | import { BlockquoteFeatureClient as BlockquoteFeatureClient_6 } from '@payloadcms/richtext-lexical/client' 8 | import { RelationshipFeatureClient as RelationshipFeatureClient_7 } from '@payloadcms/richtext-lexical/client' 9 | import { LinkFeatureClient as LinkFeatureClient_8 } from '@payloadcms/richtext-lexical/client' 10 | import { ChecklistFeatureClient as ChecklistFeatureClient_9 } from '@payloadcms/richtext-lexical/client' 11 | import { OrderedListFeatureClient as OrderedListFeatureClient_10 } from '@payloadcms/richtext-lexical/client' 12 | import { UnorderedListFeatureClient as UnorderedListFeatureClient_11 } from '@payloadcms/richtext-lexical/client' 13 | import { IndentFeatureClient as IndentFeatureClient_12 } from '@payloadcms/richtext-lexical/client' 14 | import { AlignFeatureClient as AlignFeatureClient_13 } from '@payloadcms/richtext-lexical/client' 15 | import { HeadingFeatureClient as HeadingFeatureClient_14 } from '@payloadcms/richtext-lexical/client' 16 | import { ParagraphFeatureClient as ParagraphFeatureClient_15 } from '@payloadcms/richtext-lexical/client' 17 | import { InlineCodeFeatureClient as InlineCodeFeatureClient_16 } from '@payloadcms/richtext-lexical/client' 18 | import { SuperscriptFeatureClient as SuperscriptFeatureClient_17 } from '@payloadcms/richtext-lexical/client' 19 | import { SubscriptFeatureClient as SubscriptFeatureClient_18 } from '@payloadcms/richtext-lexical/client' 20 | import { StrikethroughFeatureClient as StrikethroughFeatureClient_19 } from '@payloadcms/richtext-lexical/client' 21 | import { UnderlineFeatureClient as UnderlineFeatureClient_20 } from '@payloadcms/richtext-lexical/client' 22 | import { BoldFeatureClient as BoldFeatureClient_21 } from '@payloadcms/richtext-lexical/client' 23 | import { ItalicFeatureClient as ItalicFeatureClient_22 } from '@payloadcms/richtext-lexical/client' 24 | 25 | export const importMap = { 26 | "@payloadcms/richtext-lexical/client#RichTextCell": RichTextCell_0, 27 | "@payloadcms/richtext-lexical/client#RichTextField": RichTextField_1, 28 | "@payloadcms/richtext-lexical/generateComponentMap#getGenerateComponentMap": getGenerateComponentMap_2, 29 | "@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_3, 30 | "@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_4, 31 | "@payloadcms/richtext-lexical/client#UploadFeatureClient": UploadFeatureClient_5, 32 | "@payloadcms/richtext-lexical/client#BlockquoteFeatureClient": BlockquoteFeatureClient_6, 33 | "@payloadcms/richtext-lexical/client#RelationshipFeatureClient": RelationshipFeatureClient_7, 34 | "@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_8, 35 | "@payloadcms/richtext-lexical/client#ChecklistFeatureClient": ChecklistFeatureClient_9, 36 | "@payloadcms/richtext-lexical/client#OrderedListFeatureClient": OrderedListFeatureClient_10, 37 | "@payloadcms/richtext-lexical/client#UnorderedListFeatureClient": UnorderedListFeatureClient_11, 38 | "@payloadcms/richtext-lexical/client#IndentFeatureClient": IndentFeatureClient_12, 39 | "@payloadcms/richtext-lexical/client#AlignFeatureClient": AlignFeatureClient_13, 40 | "@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_14, 41 | "@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_15, 42 | "@payloadcms/richtext-lexical/client#InlineCodeFeatureClient": InlineCodeFeatureClient_16, 43 | "@payloadcms/richtext-lexical/client#SuperscriptFeatureClient": SuperscriptFeatureClient_17, 44 | "@payloadcms/richtext-lexical/client#SubscriptFeatureClient": SubscriptFeatureClient_18, 45 | "@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_19, 46 | "@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_20, 47 | "@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_21, 48 | "@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_22 49 | } 50 | -------------------------------------------------------------------------------- /src/app/(payload)/api/[...slug]/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes' 5 | 6 | export const GET = REST_GET(config) 7 | export const POST = REST_POST(config) 8 | export const DELETE = REST_DELETE(config) 9 | export const PATCH = REST_PATCH(config) 10 | export const OPTIONS = REST_OPTIONS(config) 11 | -------------------------------------------------------------------------------- /src/app/(payload)/api/graphql-playground/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes' 5 | 6 | export const GET = GRAPHQL_PLAYGROUND_GET(config) 7 | -------------------------------------------------------------------------------- /src/app/(payload)/api/graphql/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import { GRAPHQL_POST } from '@payloadcms/next/routes' 5 | 6 | export const POST = GRAPHQL_POST(config) 7 | -------------------------------------------------------------------------------- /src/app/(payload)/custom.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdecoded-tutorials/newsletter-app-tutorial/a9046243e9562eaa8d7b5f14a18483be5a78cc91/src/app/(payload)/custom.scss -------------------------------------------------------------------------------- /src/app/(payload)/layout.tsx: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import configPromise from '@payload-config' 4 | import '@payloadcms/next/css' 5 | import { RootLayout } from '@payloadcms/next/layouts' 6 | import React from 'react' 7 | import { importMap } from "./admin/importMap.js" 8 | 9 | import './custom.scss' 10 | 11 | type Args = { 12 | children: React.ReactNode 13 | } 14 | 15 | const Layout = ({ children }: Args) => {children} 16 | 17 | export default Layout 18 | -------------------------------------------------------------------------------- /src/collections/Media.ts: -------------------------------------------------------------------------------- 1 | import type { CollectionConfig } from 'payload' 2 | 3 | export const Media: CollectionConfig = { 4 | slug: 'media', 5 | access: { 6 | read: () => true, 7 | }, 8 | fields: [ 9 | { 10 | name: 'alt', 11 | type: 'text', 12 | required: true, 13 | }, 14 | ], 15 | upload: true, 16 | } 17 | -------------------------------------------------------------------------------- /src/collections/Posts.ts: -------------------------------------------------------------------------------- 1 | import type { CollectionConfig } from 'payload' 2 | import afterPostCreate from '../hooks/afterPostCreate' 3 | 4 | export const Posts: CollectionConfig = { 5 | slug: 'posts', 6 | access: { 7 | read: () => true, 8 | }, 9 | fields: [ 10 | { 11 | name: 'title', 12 | type: 'text', 13 | required: true, 14 | }, 15 | { 16 | name: 'postBody', 17 | type: 'richText', 18 | required: true, 19 | }, 20 | ], 21 | hooks: { 22 | afterOperation: [afterPostCreate], 23 | }, 24 | // upload: true, 25 | } 26 | -------------------------------------------------------------------------------- /src/collections/Users.ts: -------------------------------------------------------------------------------- 1 | import type { CollectionConfig } from 'payload' 2 | 3 | export const Users: CollectionConfig = { 4 | slug: 'users', 5 | admin: { 6 | useAsTitle: 'email', 7 | }, 8 | auth: true, 9 | fields: [ 10 | // Email added by default 11 | // Add more fields as needed 12 | ], 13 | } 14 | -------------------------------------------------------------------------------- /src/components/Background/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import styles from './styles.module.scss' 4 | 5 | export const Background = () => { 6 | return ( 7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/components/Background/styles.module.scss: -------------------------------------------------------------------------------- 1 | .background { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | z-index: -2; 8 | } 9 | 10 | .blur { 11 | display: block; 12 | position: absolute; 13 | width: 100%; 14 | height: 100%; 15 | background: url('/blur.png'); 16 | background-repeat: repeat; 17 | background-size: 400px 400px; 18 | background-blend-mode: soft-light, normal; 19 | backdrop-filter: blur(60px); 20 | z-index: -1; 21 | } 22 | 23 | .gradient { 24 | display: block; 25 | position: absolute; 26 | width: 100%; 27 | height: 100%; 28 | background: url('/gradient.webp'); 29 | background-size: cover; 30 | background-position: center; 31 | z-index: -3; 32 | } 33 | 34 | .gridlineContainer { 35 | position: fixed; 36 | top: 0; 37 | left: 0; 38 | padding: 0 var(--gutter); 39 | width: 100vw; 40 | height: 100vh; 41 | display: flex; 42 | align-items: center; 43 | z-index: 0; 44 | & div { 45 | position: relative; 46 | display: block; 47 | width: 100%; 48 | height: 100%; 49 | 50 | &::before { 51 | content: ''; 52 | display: block; 53 | position: absolute; 54 | width: 1px; 55 | height: 100%; 56 | left: 0; 57 | top: 0; 58 | background: var(--gridline); 59 | } 60 | 61 | &:last-of-type::after { 62 | content: ''; 63 | display: block; 64 | position: absolute; 65 | width: 1px; 66 | height: 100%; 67 | right: 0; 68 | top: 0; 69 | background: var(--gridline); 70 | } 71 | 72 | &.hideMed { 73 | @media screen and (max-width: 1200px) { 74 | display: none; 75 | } 76 | } 77 | 78 | &.hideSmall { 79 | @media screen and (max-width: 600px) { 80 | display: none; 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/components/Links/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import React from 'react' 3 | 4 | import styles from './styles.module.scss' 5 | 6 | export const Links = () => { 7 | return ( 8 |
9 | 10 |
Admin Panel
11 | Manage your site's content from the admin panel. 12 |
13 | 14 | 15 |
Payload Docs
16 | Learn about how to build your backend with Payload. 17 |
18 | 19 | 20 |
Next.js Docs
21 | Find in-depth information about Next.js features and API. 22 |
23 | 24 | 25 |
Need help?
26 | Join our Discord to ask questions and get help from the community. 27 |
28 | 29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/components/Links/styles.module.scss: -------------------------------------------------------------------------------- 1 | .links { 2 | display: grid; 3 | grid-template-columns: repeat(4, 1fr); 4 | width: 100%; 5 | margin: 0 var(--gutter); 6 | border-block: 1px solid rgba(255, 255, 255, 0.1); 7 | gap: 1px; 8 | pointer-events: all; 9 | 10 | @media screen and (max-width: 1200px) { 11 | grid-template-columns: repeat(2, 1fr); 12 | border-bottom: none; 13 | } 14 | 15 | @media screen and (max-width: 600px) { 16 | grid-template-columns: 1fr; 17 | } 18 | 19 | a { 20 | position: relative; 21 | display: flex; 22 | width: 100%; 23 | padding: 24px; 24 | padding-right: 48px; 25 | flex-direction: column; 26 | align-items: flex-start; 27 | gap: 12px; 28 | text-decoration: none; 29 | color: rgba(255, 255, 255, 0.75); 30 | 31 | @media screen and (max-width: 1200px) { 32 | border-bottom: 1px solid rgba(255, 255, 255, 0.1); 33 | } 34 | 35 | h6 { 36 | margin: 0; 37 | color: #fff; 38 | font-size: 20px; 39 | font-style: normal; 40 | font-weight: 600; 41 | line-height: normal; 42 | letter-spacing: -0.02em; 43 | 44 | @media screen and (max-width: 1200px) { 45 | font-size: 16px; 46 | } 47 | } 48 | 49 | span { 50 | line-height: 1.5; 51 | } 52 | 53 | &::before { 54 | display: block; 55 | position: absolute; 56 | content: url('/arrow.svg'); 57 | width: 12px; 58 | height: 12px; 59 | top: 24px; 60 | right: 24px; 61 | opacity: 0.25; 62 | transition-property: top, right, opacity; 63 | transition-duration: 0.3s; 64 | } 65 | 66 | &::after { 67 | position: absolute; 68 | left: 0; 69 | bottom: 0; 70 | content: (''); 71 | display: block; 72 | width: 0; 73 | height: 2px; 74 | background-color: #fff; 75 | transition: width 0.3s; 76 | } 77 | 78 | &:hover { 79 | &::before { 80 | top: 20px; 81 | right: 20px; 82 | opacity: 1; 83 | } 84 | 85 | &::after { 86 | width: 100%; 87 | } 88 | 89 | .scanlines { 90 | opacity: 0.1; 91 | } 92 | } 93 | } 94 | } 95 | 96 | .scanlines { 97 | position: absolute; 98 | top: 0px; 99 | left: 0px; 100 | right: 0px; 101 | bottom: 0px; 102 | background: url('/scanline-light.png'); 103 | opacity: 0; 104 | transition: opacity 0.3s; 105 | } 106 | -------------------------------------------------------------------------------- /src/components/Logos/index.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | import React from 'react' 3 | 4 | import styles from './styles.module.scss' 5 | 6 | export const Logos = () => { 7 | return ( 8 |
9 | Payload Logo 17 | 18 | Next.js Logo 26 |
27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/components/Logos/styles.module.scss: -------------------------------------------------------------------------------- 1 | .logos { 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: center; 5 | align-items: center; 6 | height: 100%; 7 | min-height: 50vh; 8 | width: 100%; 9 | gap: 80px; 10 | 11 | @media screen and (max-width: 600px) { 12 | gap: 40px; 13 | flex-direction: column; 14 | } 15 | } 16 | 17 | .payloadLogo { 18 | width: 100%; 19 | height: auto; 20 | max-width: 320px; 21 | display: flex; 22 | justify-content: flex-end; 23 | align-items: center; 24 | } 25 | 26 | .nextLogo { 27 | width: 100%; 28 | height: auto; 29 | max-width: 320px; 30 | display: flex; 31 | justify-content: flex-start; 32 | align-items: center; 33 | color: #fff; 34 | } 35 | 36 | .payloadLogo, 37 | .nextLogo { 38 | @media screen and (max-width: 1200px) { 39 | width: 25vw; 40 | height: auto; 41 | } 42 | 43 | @media screen and (max-width: 600px) { 44 | width: 50vw; 45 | justify-content: center; 46 | align-items: center; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/components/RichText/index.module.scss: -------------------------------------------------------------------------------- 1 | .richText { 2 | :first-child { 3 | margin-top: 0; 4 | } 5 | :last-child { 6 | margin-bottom: 0; 7 | } 8 | } -------------------------------------------------------------------------------- /src/components/RichText/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import classes from './index.module.scss' 4 | import { serializeLexical } from './serialize/index' 5 | 6 | const RichText: React.FC<{ className?: string; content: any }> = ({ className, content }) => { 7 | if (!content) { 8 | return null 9 | } 10 | 11 | return ( 12 |
13 | {content && 14 | !Array.isArray(content) && 15 | typeof content === 'object' && 16 | 'root' in content && 17 | serializeLexical({ nodes: content?.root?.children })} 18 |
19 | ) 20 | } 21 | 22 | export default RichText -------------------------------------------------------------------------------- /src/components/RichText/serialize/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/strict-boolean-expressions */ 2 | import type { SerializedListItemNode, SerializedListNode } from '@lexical/list' 3 | import type { SerializedHeadingNode, SerializedQuoteNode } from '@lexical/rich-text' 4 | import type { LinkFields, SerializedLinkNode } from '@payloadcms/richtext-lexical' 5 | import type { SerializedElementNode, SerializedLexicalNode, SerializedTextNode } from 'lexical' 6 | 7 | import escapeHTML from 'escape-html' 8 | import Link from 'next/link' 9 | import React, { Fragment } from 'react' 10 | 11 | import { 12 | IS_BOLD, 13 | IS_CODE, 14 | IS_ITALIC, 15 | IS_STRIKETHROUGH, 16 | IS_SUBSCRIPT, 17 | IS_SUPERSCRIPT, 18 | IS_UNDERLINE, 19 | } from './nodeFormat' 20 | 21 | interface Props { 22 | nodes: SerializedLexicalNode[] 23 | } 24 | 25 | export function serializeLexical({ nodes }: Props) { 26 | return ( 27 | 28 | {nodes?.map((_node, index) => { 29 | if (_node.type === 'text') { 30 | const node = _node as SerializedTextNode 31 | let text = ( 32 | 33 | ) 34 | if (node.format & IS_BOLD) { 35 | text = {text} 36 | } 37 | if (node.format & IS_ITALIC) { 38 | text = {text} 39 | } 40 | if (node.format & IS_STRIKETHROUGH) { 41 | text = ( 42 | 43 | {text} 44 | 45 | ) 46 | } 47 | if (node.format & IS_UNDERLINE) { 48 | text = ( 49 | 50 | {text} 51 | 52 | ) 53 | } 54 | if (node.format & IS_CODE) { 55 | text = {text} 56 | } 57 | if (node.format & IS_SUBSCRIPT) { 58 | text = {text} 59 | } 60 | if (node.format & IS_SUPERSCRIPT) { 61 | text = {text} 62 | } 63 | 64 | return text 65 | } 66 | 67 | if (_node == null) { 68 | return null 69 | } 70 | 71 | // NOTE: Hacky fix for 72 | // https://github.com/facebook/lexical/blob/d10c4e6e55261b2fdd7d1845aed46151d0f06a8c/packages/lexical-list/src/LexicalListItemNode.ts#L133 73 | // which does not return checked: false (only true - i.e. there is no prop for false) 74 | const serializedChildrenFn = (node: SerializedElementNode) => { 75 | if (node.children == null) { 76 | return null 77 | } else { 78 | if (node?.type === 'list' && (node as SerializedListNode)?.listType === 'check') { 79 | for (const item of node.children) { 80 | if ('checked' in item) { 81 | if (!item?.checked) { 82 | item.checked = false 83 | } 84 | } 85 | } 86 | return serializeLexical({ nodes: node.children }) 87 | } else { 88 | return serializeLexical({ nodes: node.children }) 89 | } 90 | } 91 | } 92 | 93 | const serializedChildren = 94 | 'children' in _node ? serializedChildrenFn(_node as SerializedElementNode) : '' 95 | 96 | switch (_node.type) { 97 | case 'linebreak': { 98 | return
99 | } 100 | case 'paragraph': { 101 | return

{serializedChildren}

102 | } 103 | case 'heading': { 104 | const node = _node as SerializedHeadingNode 105 | 106 | // type Heading = Extract 107 | const Tag = node?.tag as any 108 | return {serializedChildren} 109 | } 110 | case 'label': 111 | return

{serializedChildren}

112 | 113 | case 'largeBody': { 114 | return

{serializedChildren}

115 | } 116 | case 'list': { 117 | const node = _node as SerializedListNode 118 | 119 | // type List = Extract 120 | const Tag = node?.tag as any 121 | return ( 122 | 123 | {serializedChildren} 124 | 125 | ) 126 | } 127 | case 'listitem': { 128 | const node = _node as SerializedListItemNode 129 | 130 | if (node?.checked != null) { 131 | return ( 132 |
  • 144 | {serializedChildren} 145 |
  • 146 | ) 147 | } else { 148 | return ( 149 |
  • 150 | {serializedChildren} 151 |
  • 152 | ) 153 | } 154 | } 155 | case 'quote': { 156 | const node = _node as SerializedQuoteNode 157 | 158 | return
    {serializedChildren}
    159 | } 160 | case 'link': { 161 | const node = _node as SerializedLinkNode 162 | 163 | const fields: LinkFields = node.fields 164 | 165 | if (fields.linkType === 'custom') { 166 | const rel = fields.newTab ? 'noopener noreferrer' : undefined 167 | 168 | return ( 169 | 179 | {serializedChildren} 180 | 181 | ) 182 | } else { 183 | return Internal link coming soon 184 | } 185 | } 186 | 187 | default: 188 | return null 189 | } 190 | })} 191 |
    192 | ) 193 | } -------------------------------------------------------------------------------- /src/components/RichText/serialize/nodeFormat.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-redundant-type-constituents */ 2 | /* eslint-disable regexp/no-obscure-range */ 3 | //This copy-and-pasted from lexical here here: https://github.com/facebook/lexical/blob/c2ceee223f46543d12c574e62155e619f9a18a5d/packages/lexical/src/LexicalConstants.ts 4 | 5 | import type { ElementFormatType, TextFormatType } from 'lexical' 6 | 7 | /** 8 | * Copyright (c) Meta Platforms, Inc. and affiliates. 9 | * 10 | * This source code is licensed under the MIT license found in the 11 | * LICENSE file in the root directory of this source tree. 12 | * 13 | */ 14 | 15 | // DOM 16 | export const DOM_ELEMENT_TYPE = 1 17 | export const DOM_TEXT_TYPE = 3 18 | 19 | // Reconciling 20 | export const NO_DIRTY_NODES = 0 21 | export const HAS_DIRTY_NODES = 1 22 | export const FULL_RECONCILE = 2 23 | 24 | // Text node modes 25 | export const IS_NORMAL = 0 26 | export const IS_TOKEN = 1 27 | export const IS_SEGMENTED = 2 28 | // IS_INERT = 3 29 | 30 | // Text node formatting 31 | export const IS_BOLD = 1 32 | export const IS_ITALIC = 1 << 1 33 | export const IS_STRIKETHROUGH = 1 << 2 34 | export const IS_UNDERLINE = 1 << 3 35 | export const IS_CODE = 1 << 4 36 | export const IS_SUBSCRIPT = 1 << 5 37 | export const IS_SUPERSCRIPT = 1 << 6 38 | export const IS_HIGHLIGHT = 1 << 7 39 | 40 | export const IS_ALL_FORMATTING = 41 | IS_BOLD | 42 | IS_ITALIC | 43 | IS_STRIKETHROUGH | 44 | IS_UNDERLINE | 45 | IS_CODE | 46 | IS_SUBSCRIPT | 47 | IS_SUPERSCRIPT | 48 | IS_HIGHLIGHT 49 | 50 | // Text node details 51 | export const IS_DIRECTIONLESS = 1 52 | export const IS_UNMERGEABLE = 1 << 1 53 | 54 | // Element node formatting 55 | export const IS_ALIGN_LEFT = 1 56 | export const IS_ALIGN_CENTER = 2 57 | export const IS_ALIGN_RIGHT = 3 58 | export const IS_ALIGN_JUSTIFY = 4 59 | export const IS_ALIGN_START = 5 60 | export const IS_ALIGN_END = 6 61 | 62 | // Reconciliation 63 | export const NON_BREAKING_SPACE = '\u00A0' 64 | const ZERO_WIDTH_SPACE = '\u200b' 65 | 66 | export const DOUBLE_LINE_BREAK = '\n\n' 67 | 68 | // For FF, we need to use a non-breaking space, or it gets composition 69 | // in a stuck state. 70 | 71 | const RTL = '\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC' 72 | const LTR = 73 | 'A-Za-z\u00C0-\u00D6\u00D8-\u00F6' + 74 | '\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF\u200E\u2C00-\uFB1C' + 75 | '\uFE00-\uFE6F\uFEFD-\uFFFF' 76 | 77 | // eslint-disable-next-line no-misleading-character-class 78 | export const RTL_REGEX = new RegExp('^[^' + LTR + ']*[' + RTL + ']') 79 | // eslint-disable-next-line no-misleading-character-class 80 | export const LTR_REGEX = new RegExp('^[^' + RTL + ']*[' + LTR + ']') 81 | 82 | export const TEXT_TYPE_TO_FORMAT: Record = { 83 | bold: IS_BOLD, 84 | code: IS_CODE, 85 | highlight: IS_HIGHLIGHT, 86 | italic: IS_ITALIC, 87 | strikethrough: IS_STRIKETHROUGH, 88 | subscript: IS_SUBSCRIPT, 89 | superscript: IS_SUPERSCRIPT, 90 | underline: IS_UNDERLINE, 91 | } 92 | 93 | export const DETAIL_TYPE_TO_DETAIL: Record = { 94 | directionless: IS_DIRECTIONLESS, 95 | unmergeable: IS_UNMERGEABLE, 96 | } 97 | 98 | export const ELEMENT_TYPE_TO_FORMAT: Record, number> = { 99 | center: IS_ALIGN_CENTER, 100 | end: IS_ALIGN_END, 101 | justify: IS_ALIGN_JUSTIFY, 102 | left: IS_ALIGN_LEFT, 103 | right: IS_ALIGN_RIGHT, 104 | start: IS_ALIGN_START, 105 | } 106 | 107 | export const ELEMENT_FORMAT_TO_TYPE: Record = { 108 | [IS_ALIGN_CENTER]: 'center', 109 | [IS_ALIGN_END]: 'end', 110 | [IS_ALIGN_JUSTIFY]: 'justify', 111 | [IS_ALIGN_LEFT]: 'left', 112 | [IS_ALIGN_RIGHT]: 'right', 113 | [IS_ALIGN_START]: 'start', 114 | } 115 | 116 | export const TEXT_MODE_TO_TYPE: Record = { 117 | normal: IS_NORMAL, 118 | segmented: IS_SEGMENTED, 119 | token: IS_TOKEN, 120 | } 121 | 122 | export const TEXT_TYPE_TO_MODE: Record = { 123 | [IS_NORMAL]: 'normal', 124 | [IS_SEGMENTED]: 'segmented', 125 | [IS_TOKEN]: 'token', 126 | } 127 | -------------------------------------------------------------------------------- /src/components/newsletter/subscribeBtn.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Button } from "@/components/ui/button"; 3 | import { Input } from "@/components/ui/input"; 4 | import subscribeToNewsletter from "@/actions/subscribeToNewsletter"; 5 | import { useActionState } from "react"; 6 | 7 | const SubscribeBtn = () => { 8 | const [message, formAction] = useActionState(subscribeToNewsletter, ""); 9 | console.log(message); 10 | 11 | return ( 12 |
    13 |
    14 |
    15 |
    {message &&

    {message}

    }
    16 |
    17 | ); 18 | } 19 | 20 | export default SubscribeBtn; -------------------------------------------------------------------------------- /src/components/newsletter/topPosts.tsx: -------------------------------------------------------------------------------- 1 | import { getPayload } from "@/lib/payload"; 2 | import { 3 | Card, 4 | CardContent, 5 | CardTitle, 6 | CardHeader, 7 | } from "@/components/ui/card" 8 | import { Post } from "../../payload-types"; 9 | import Link from "next/link"; 10 | 11 | const TopPosts = async () => { 12 | const posts = await (await getPayload()).find({ 13 | collection: 'posts', 14 | limit: 3, 15 | }) 16 | 17 | return ( 18 |
    19 |

    Top Posts

    20 |
    21 | {posts.docs.map((post: Post) => { 22 | const root = post.postBody.root; 23 | const firstParagraph = root.children.find(child => child.type === 'paragraph' && Array.isArray(child.children) && child?.children.length > 0); 24 | const paragraphText = firstParagraph && Array.isArray(firstParagraph.children) ? firstParagraph.children.map(child => child.text).join('') : ''; 25 | 26 | return ( 27 | 28 | 29 | 30 | 31 | {post.title} 32 | 33 | {paragraphText} 34 | 35 | 36 | 37 | ) 38 | })} 39 |
    40 |
    41 | ) 42 | } 43 | 44 | export default TopPosts -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap 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 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
    17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
    29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

    44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

    56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

    64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
    76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /src/hooks/afterPostCreate.ts: -------------------------------------------------------------------------------- 1 | import type { CollectionAfterOperationHook } from 'payload' 2 | import { Resend } from 'resend' 3 | import { 4 | HTMLConverterFeature, 5 | consolidateHTMLConverters, 6 | convertLexicalToHTML, 7 | defaultEditorConfig, 8 | defaultEditorFeatures, 9 | sanitizeServerEditorConfig, 10 | } from '@payloadcms/richtext-lexical' 11 | 12 | const afterPostCreate: CollectionAfterOperationHook = async ({ 13 | args, // arguments passed into the operation 14 | operation, // name of the operation 15 | req, // full express request 16 | result, // the result of the operation, before modifications 17 | }: any) => { 18 | if (operation !== 'create') { 19 | return result 20 | } 21 | 22 | const apiKey = process.env.CLOUDFLARE_API_KEY 23 | 24 | const newPost = result.docs ? result.docs[0] : result 25 | const title = newPost.title 26 | const body = newPost.postBody 27 | 28 | const editorConfig = defaultEditorConfig 29 | editorConfig.features = [...defaultEditorFeatures, HTMLConverterFeature({})] 30 | const sanitizedEditorConfig = await sanitizeServerEditorConfig(editorConfig, req.payload.config) 31 | 32 | const html = await convertLexicalToHTML({ 33 | converters: consolidateHTMLConverters({ 34 | editorConfig: sanitizedEditorConfig, 35 | }), 36 | data: body, 37 | req, 38 | }) 39 | 40 | if (!apiKey) { 41 | return result 42 | } 43 | 44 | const url = `https://newsletter-tutorial-workers.webdecodedtutorials.workers.dev` 45 | const headers = { 46 | Authorization: `Basic ${apiKey}`, 47 | } 48 | 49 | const reqBody = JSON.stringify({ 50 | subject: title, 51 | emailContent: html, 52 | }) 53 | 54 | fetch(url, { 55 | method: 'POST', 56 | headers: headers, 57 | body: reqBody, 58 | }) 59 | .then((response) => { 60 | console.log(response) 61 | }) 62 | .catch((error) => { 63 | console.error(error) 64 | }) 65 | 66 | return result 67 | } 68 | 69 | export default afterPostCreate 70 | -------------------------------------------------------------------------------- /src/lib/payload.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { getPayloadHMR } from '@payloadcms/next/utilities' 4 | import config from '@payload-config' 5 | 6 | export async function getPayload() { 7 | return getPayloadHMR({ config }) 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server' 2 | 3 | const isProtectedRoute = createRouteMatcher(['/posts(.*)']) 4 | 5 | export default clerkMiddleware(async (auth, req) => { 6 | if (isProtectedRoute(req)) await auth.protect() 7 | }) 8 | 9 | export const config = { 10 | matcher: [ 11 | // Skip Next.js internals and all static files, unless found in search params 12 | '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)', 13 | // Always run for API routes 14 | // '/(api|trpc)(.*)', 15 | ], 16 | } 17 | -------------------------------------------------------------------------------- /src/payload-types.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * This file was automatically generated by Payload. 5 | * DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config, 6 | * and re-run `payload generate:types` to regenerate this file. 7 | */ 8 | 9 | export interface Config { 10 | auth: { 11 | users: UserAuthOperations; 12 | }; 13 | collections: { 14 | users: User; 15 | media: Media; 16 | posts: Post; 17 | 'payload-locked-documents': PayloadLockedDocument; 18 | 'payload-preferences': PayloadPreference; 19 | 'payload-migrations': PayloadMigration; 20 | }; 21 | db: { 22 | defaultIDType: number; 23 | }; 24 | globals: {}; 25 | locale: null; 26 | user: User & { 27 | collection: 'users'; 28 | }; 29 | } 30 | export interface UserAuthOperations { 31 | forgotPassword: { 32 | email: string; 33 | password: string; 34 | }; 35 | login: { 36 | email: string; 37 | password: string; 38 | }; 39 | registerFirstUser: { 40 | email: string; 41 | password: string; 42 | }; 43 | unlock: { 44 | email: string; 45 | password: string; 46 | }; 47 | } 48 | /** 49 | * This interface was referenced by `Config`'s JSON-Schema 50 | * via the `definition` "users". 51 | */ 52 | export interface User { 53 | id: number; 54 | updatedAt: string; 55 | createdAt: string; 56 | email: string; 57 | resetPasswordToken?: string | null; 58 | resetPasswordExpiration?: string | null; 59 | salt?: string | null; 60 | hash?: string | null; 61 | loginAttempts?: number | null; 62 | lockUntil?: string | null; 63 | password?: string | null; 64 | } 65 | /** 66 | * This interface was referenced by `Config`'s JSON-Schema 67 | * via the `definition` "media". 68 | */ 69 | export interface Media { 70 | id: number; 71 | alt: string; 72 | updatedAt: string; 73 | createdAt: string; 74 | url?: string | null; 75 | thumbnailURL?: string | null; 76 | filename?: string | null; 77 | mimeType?: string | null; 78 | filesize?: number | null; 79 | width?: number | null; 80 | height?: number | null; 81 | focalX?: number | null; 82 | focalY?: number | null; 83 | } 84 | /** 85 | * This interface was referenced by `Config`'s JSON-Schema 86 | * via the `definition` "posts". 87 | */ 88 | export interface Post { 89 | id: number; 90 | title: string; 91 | postBody: { 92 | root: { 93 | type: string; 94 | children: { 95 | type: string; 96 | version: number; 97 | [k: string]: unknown; 98 | }[]; 99 | direction: ('ltr' | 'rtl') | null; 100 | format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | ''; 101 | indent: number; 102 | version: number; 103 | }; 104 | [k: string]: unknown; 105 | }; 106 | updatedAt: string; 107 | createdAt: string; 108 | } 109 | /** 110 | * This interface was referenced by `Config`'s JSON-Schema 111 | * via the `definition` "payload-locked-documents". 112 | */ 113 | export interface PayloadLockedDocument { 114 | id: number; 115 | document?: 116 | | ({ 117 | relationTo: 'users'; 118 | value: number | User; 119 | } | null) 120 | | ({ 121 | relationTo: 'media'; 122 | value: number | Media; 123 | } | null) 124 | | ({ 125 | relationTo: 'posts'; 126 | value: number | Post; 127 | } | null); 128 | globalSlug?: string | null; 129 | user: { 130 | relationTo: 'users'; 131 | value: number | User; 132 | }; 133 | updatedAt: string; 134 | createdAt: string; 135 | } 136 | /** 137 | * This interface was referenced by `Config`'s JSON-Schema 138 | * via the `definition` "payload-preferences". 139 | */ 140 | export interface PayloadPreference { 141 | id: number; 142 | user: { 143 | relationTo: 'users'; 144 | value: number | User; 145 | }; 146 | key?: string | null; 147 | value?: 148 | | { 149 | [k: string]: unknown; 150 | } 151 | | unknown[] 152 | | string 153 | | number 154 | | boolean 155 | | null; 156 | updatedAt: string; 157 | createdAt: string; 158 | } 159 | /** 160 | * This interface was referenced by `Config`'s JSON-Schema 161 | * via the `definition` "payload-migrations". 162 | */ 163 | export interface PayloadMigration { 164 | id: number; 165 | name?: string | null; 166 | batch?: number | null; 167 | updatedAt: string; 168 | createdAt: string; 169 | } 170 | /** 171 | * This interface was referenced by `Config`'s JSON-Schema 172 | * via the `definition` "auth". 173 | */ 174 | export interface Auth { 175 | [k: string]: unknown; 176 | } 177 | 178 | 179 | declare module 'payload' { 180 | export interface GeneratedTypes extends Config {} 181 | } -------------------------------------------------------------------------------- /src/payload.config.ts: -------------------------------------------------------------------------------- 1 | import { postgresAdapter } from '@payloadcms/db-postgres' 2 | import { lexicalEditor } from '@payloadcms/richtext-lexical' 3 | import { vercelBlobStorage } from '@payloadcms/storage-vercel-blob' 4 | import path from 'path' 5 | import { buildConfig } from 'payload' 6 | import sharp from 'sharp' 7 | import { fileURLToPath } from 'url' 8 | 9 | import { Media } from './collections/Media' 10 | import { Users } from './collections/Users' 11 | import { Posts } from './collections/Posts' 12 | 13 | const filename = fileURLToPath(import.meta.url) 14 | const dirname = path.dirname(filename) 15 | 16 | export default buildConfig({ 17 | admin: { 18 | user: Users.slug, 19 | }, 20 | collections: [Users, Media, Posts], 21 | db: postgresAdapter({ 22 | pool: { 23 | connectionString: process.env.POSTGRES_URL, 24 | }, 25 | }), 26 | editor: lexicalEditor({}), 27 | plugins: [ 28 | vercelBlobStorage({ 29 | collections: { 30 | [Media.slug]: true, 31 | }, 32 | token: process.env.BLOB_READ_WRITE_TOKEN || '', 33 | }), 34 | ], 35 | secret: process.env.PAYLOAD_SECRET || '', 36 | 37 | sharp, 38 | 39 | typescript: { 40 | outputFile: path.resolve(dirname, 'payload-types.ts'), 41 | }, 42 | }) 43 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | const { fontFamily } = require('tailwindcss/defaultTheme') 3 | 4 | const config: Config = { 5 | darkMode: ['class'], 6 | content: [ 7 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}', 8 | './src/components/**/*.{js,ts,jsx,tsx,mdx}', 9 | './src/app/**/*.{js,ts,jsx,tsx,mdx}', 10 | ], 11 | theme: { 12 | container: { 13 | center: true, 14 | padding: '2rem', 15 | screens: { 16 | '2xl': '1400px', 17 | }, 18 | }, 19 | extend: { 20 | colors: { 21 | border: 'hsl(var(--border))', 22 | input: 'hsl(var(--input))', 23 | ring: 'hsl(var(--ring))', 24 | background: 'hsl(var(--background))', 25 | foreground: 'hsl(var(--foreground))', 26 | primary: { 27 | DEFAULT: 'hsl(var(--primary))', 28 | foreground: 'hsl(var(--primary-foreground))', 29 | }, 30 | secondary: { 31 | DEFAULT: 'hsl(var(--secondary))', 32 | foreground: 'hsl(var(--secondary-foreground))', 33 | }, 34 | destructive: { 35 | DEFAULT: 'hsl(var(--destructive))', 36 | foreground: 'hsl(var(--destructive-foreground))', 37 | }, 38 | muted: { 39 | DEFAULT: 'hsl(var(--muted))', 40 | foreground: 'hsl(var(--muted-foreground))', 41 | }, 42 | accent: { 43 | DEFAULT: 'hsl(var(--accent))', 44 | foreground: 'hsl(var(--accent-foreground))', 45 | }, 46 | popover: { 47 | DEFAULT: 'hsl(var(--popover))', 48 | foreground: 'hsl(var(--popover-foreground))', 49 | }, 50 | card: { 51 | DEFAULT: 'hsl(var(--card))', 52 | foreground: 'hsl(var(--card-foreground))', 53 | }, 54 | chart: { 55 | '1': 'hsl(var(--chart-1))', 56 | '2': 'hsl(var(--chart-2))', 57 | '3': 'hsl(var(--chart-3))', 58 | '4': 'hsl(var(--chart-4))', 59 | '5': 'hsl(var(--chart-5))', 60 | }, 61 | }, 62 | borderRadius: { 63 | lg: 'var(--radius)', 64 | md: 'calc(var(--radius) - 2px)', 65 | sm: 'calc(var(--radius) - 4px)', 66 | }, 67 | fontFamily: { 68 | sans: ['var(--font-sans)', ...fontFamily.sans], 69 | }, 70 | keyframes: { 71 | 'accordion-down': { 72 | from: { 73 | height: '0', 74 | }, 75 | to: { 76 | height: 'var(--radix-accordion-content-height)', 77 | }, 78 | }, 79 | 'accordion-up': { 80 | from: { 81 | height: 'var(--radix-accordion-content-height)', 82 | }, 83 | to: { 84 | height: '0', 85 | }, 86 | }, 87 | }, 88 | animation: { 89 | 'accordion-down': 'accordion-down 0.2s ease-out', 90 | 'accordion-up': 'accordion-up 0.2s ease-out', 91 | }, 92 | }, 93 | }, 94 | plugins: [require('tailwindcss-animate')], 95 | } 96 | export default config 97 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "noEmit": true, 13 | "esModuleInterop": true, 14 | "module": "esnext", 15 | "moduleResolution": "bundler", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve", 19 | "incremental": true, 20 | "plugins": [ 21 | { 22 | "name": "next" 23 | } 24 | ], 25 | "paths": { 26 | "@/*": [ 27 | "./src/*" 28 | ], 29 | "@payload-config": [ 30 | "./src/payload.config.ts" 31 | ] 32 | }, 33 | "target": "ES2017" 34 | }, 35 | "include": [ 36 | "next-env.d.ts", 37 | "**/*.ts", 38 | "**/*.tsx", 39 | ".next/types/**/*.ts" 40 | ], 41 | "exclude": [ 42 | "node_modules" 43 | ] 44 | } 45 | --------------------------------------------------------------------------------