├── .nvmrc ├── LICENSE.md ├── src ├── access │ ├── anyone.ts │ ├── authenticated.ts │ ├── authenticated-or-published.ts │ └── admin.ts ├── lib │ ├── payload │ │ ├── can-use-dom.ts │ │ ├── get-payload.ts │ │ ├── index.ts │ │ ├── merge-open-graph.ts │ │ ├── get-globals.ts │ │ ├── get-url.ts │ │ ├── deep-merge.ts │ │ ├── format-authors.ts │ │ ├── get-media-url.ts │ │ ├── generate-meta.ts │ │ ├── generate-preview-path.ts │ │ └── get-cached-document.ts │ ├── email │ │ ├── constants.ts │ │ └── email-template.tsx │ ├── auth │ │ ├── index.ts │ │ ├── types.ts │ │ ├── client.ts │ │ └── context │ │ │ ├── index.tsx │ │ │ └── get-context-props.ts │ ├── animation.ts │ ├── utils.ts │ ├── email-adapter.ts │ └── constants.ts ├── collections │ ├── global │ │ ├── index.ts │ │ ├── footer │ │ │ ├── row-label.tsx │ │ │ └── config.ts │ │ ├── privacy │ │ │ └── config.ts │ │ ├── terms-of-use │ │ │ └── config.ts │ │ └── hooks │ │ │ └── revalidate-global.ts │ ├── users.ts │ ├── uploads │ │ ├── hooks │ │ │ └── generate-blur-data-url.ts │ │ ├── private-uploads.ts │ │ └── payload-uploads.ts │ └── blog │ │ └── hooks │ │ ├── populate-authors.ts │ │ └── revalidate-post.ts ├── app │ ├── (frontend) │ │ ├── (public) │ │ │ ├── blog │ │ │ │ ├── opengraph-image.png │ │ │ │ ├── layout.tsx │ │ │ │ ├── [slug] │ │ │ │ │ ├── blog-sidebar.tsx │ │ │ │ │ ├── blog-content.tsx │ │ │ │ │ ├── blog-header.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── search-params.ts │ │ │ │ ├── page.tsx │ │ │ │ └── pagination.tsx │ │ │ ├── page.tsx │ │ │ ├── (auth) │ │ │ │ ├── account │ │ │ │ │ └── [accountView] │ │ │ │ │ │ └── page.tsx │ │ │ │ └── [authView] │ │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── (aux) │ │ │ │ ├── terms │ │ │ │ └── page.tsx │ │ │ │ └── privacy │ │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── providers.tsx │ │ └── dashboard │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ ├── api │ │ └── auth │ │ │ └── [...all] │ │ │ └── route.ts │ └── (payload) │ │ ├── api │ │ ├── graphql-playground │ │ │ └── route.ts │ │ ├── graphql │ │ │ └── route.ts │ │ └── [...slug] │ │ │ └── route.ts │ │ ├── admin │ │ └── [[...segments]] │ │ │ ├── page.tsx │ │ │ └── not-found.tsx │ │ ├── custom.scss │ │ └── layout.tsx ├── plugins │ ├── import-export-plugin.ts │ ├── index.ts │ ├── seo-plugin.ts │ └── s3-storage-plugin.ts ├── components │ ├── payload │ │ ├── admin-icon.tsx │ │ ├── admin-logo.tsx │ │ ├── live-preview-listener.tsx │ │ ├── media │ │ │ ├── index.tsx │ │ │ ├── types.ts │ │ │ └── video-media │ │ │ │ └── index.tsx │ │ ├── render-blocks.tsx │ │ ├── page-range.tsx │ │ ├── collection-card.tsx │ │ ├── cms-link.tsx │ │ └── rich-text │ │ │ └── index.tsx │ ├── layout │ │ ├── main.tsx │ │ ├── theme-provider.tsx │ │ ├── better-auth-ui-provider.tsx │ │ ├── theme-switch.tsx │ │ └── footer.tsx │ ├── ui │ │ ├── skeleton.tsx │ │ ├── sonner.tsx │ │ ├── label.tsx │ │ ├── separator.tsx │ │ ├── textarea.tsx │ │ ├── highlight-border.tsx │ │ ├── input.tsx │ │ ├── dynamic-image.tsx │ │ ├── avatar.tsx │ │ ├── checkbox.tsx │ │ ├── copy-button.tsx │ │ ├── badge.tsx │ │ ├── popover.tsx │ │ ├── scroll-area.tsx │ │ ├── alert.tsx │ │ ├── password-input.tsx │ │ ├── tooltip.tsx │ │ ├── tabs.tsx │ │ ├── typography.tsx │ │ ├── card.tsx │ │ └── table.tsx │ ├── screen │ │ └── home │ │ │ ├── signed-in-content.tsx │ │ │ ├── signed-out-content.tsx │ │ │ ├── dev-tools.tsx │ │ │ ├── user-card.tsx │ │ │ ├── theme-colors.tsx │ │ │ ├── services.tsx │ │ │ └── features.tsx │ ├── icons.tsx │ ├── core │ │ ├── image-zoom.tsx │ │ ├── image-zoom.css │ │ └── image-sequence-player.tsx │ └── motion-primitives │ │ └── in-view.tsx ├── hooks │ ├── use-media-query.tsx │ ├── use-mobile.ts │ └── use-hash.tsx ├── blocks │ ├── media-block │ │ ├── config.ts │ │ └── component.tsx │ ├── copyright-inline-block │ │ ├── component.tsx │ │ └── config.ts │ ├── content-block │ │ ├── config.ts │ │ └── component.tsx │ └── gallery-block │ │ └── config.ts ├── env.d.ts ├── fields │ └── default-lexical.ts └── payload.config.ts ├── postcss.config.mjs ├── public ├── favicon.ico ├── website-template-OG.png └── images │ ├── blocks │ ├── media-block.png │ ├── content-block.png │ ├── gallery-block.png │ └── copyright-inline-block.png │ └── pattern-plus.svg ├── .changeset ├── config.json └── README.md ├── extra ├── form │ ├── Error │ │ └── index.tsx │ ├── Message │ │ └── index.tsx │ ├── Width │ │ └── index.tsx │ ├── fields.tsx │ ├── config.ts │ ├── Email │ │ └── index.tsx │ ├── Text │ │ └── index.tsx │ ├── Phone │ │ └── index.tsx │ ├── Number │ │ └── index.tsx │ ├── Textarea │ │ └── index.tsx │ ├── Checkbox │ │ └── index.tsx │ ├── UserInfo │ │ ├── index.tsx │ │ └── config.ts │ ├── buildInitialFormState.tsx │ ├── fields-config.ts │ ├── State │ │ ├── index.tsx │ │ └── options.ts │ ├── Country │ │ └── index.tsx │ ├── Select │ │ └── index.tsx │ └── FormInput │ │ └── index.tsx └── plugins │ └── form-plugin │ └── before-email.tsx ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── general_issue.md │ ├── bug_report.md │ └── feature_request.md └── ISSUE_TEMPLATE.md ├── components.json ├── .gitignore ├── .env.example ├── .vscode ├── launch.json └── settings.json ├── next.config.mjs ├── tsconfig.json ├── .biomeignore ├── CHANGELOG.md ├── biome.json └── docker-compose.yml /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.5 -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | MIT License 4 | 5 | Go nuts 🥜 6 | -------------------------------------------------------------------------------- /src/access/anyone.ts: -------------------------------------------------------------------------------- 1 | import type { Access } from 'payload' 2 | 3 | export const anyone: Access = () => true 4 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ['@tailwindcss/postcss'], 3 | } 4 | 5 | export default config 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluid-design-io/payload-better-auth-starter/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/lib/payload/can-use-dom.ts: -------------------------------------------------------------------------------- 1 | export default !!(typeof window !== 'undefined' && window.document && window.document.createElement) 2 | -------------------------------------------------------------------------------- /public/website-template-OG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluid-design-io/payload-better-auth-starter/HEAD/public/website-template-OG.png -------------------------------------------------------------------------------- /src/collections/global/index.ts: -------------------------------------------------------------------------------- 1 | export * from './footer/config' 2 | export * from './privacy/config' 3 | export * from './terms-of-use/config' 4 | -------------------------------------------------------------------------------- /src/lib/email/constants.ts: -------------------------------------------------------------------------------- 1 | import { getClientSideURL } from '@/lib/payload' 2 | 3 | export const LOGO_URL = `${getClientSideURL()}/favicon.ico` 4 | -------------------------------------------------------------------------------- /public/images/blocks/media-block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluid-design-io/payload-better-auth-starter/HEAD/public/images/blocks/media-block.png -------------------------------------------------------------------------------- /public/images/blocks/content-block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluid-design-io/payload-better-auth-starter/HEAD/public/images/blocks/content-block.png -------------------------------------------------------------------------------- /public/images/blocks/gallery-block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluid-design-io/payload-better-auth-starter/HEAD/public/images/blocks/gallery-block.png -------------------------------------------------------------------------------- /public/images/blocks/copyright-inline-block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluid-design-io/payload-better-auth-starter/HEAD/public/images/blocks/copyright-inline-block.png -------------------------------------------------------------------------------- /src/app/(frontend)/(public)/blog/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluid-design-io/payload-better-auth-starter/HEAD/src/app/(frontend)/(public)/blog/opengraph-image.png -------------------------------------------------------------------------------- /src/app/(frontend)/(public)/blog/layout.tsx: -------------------------------------------------------------------------------- 1 | import { NuqsAdapter } from 'nuqs/adapters/next/app' 2 | 3 | export default function BlogLayout({ children }: { children: React.ReactNode }) { 4 | return {children} 5 | } 6 | -------------------------------------------------------------------------------- /src/plugins/import-export-plugin.ts: -------------------------------------------------------------------------------- 1 | import { importExportPlugin as importExportPluginConfig } from '@payloadcms/plugin-import-export' 2 | import type { Plugin } from 'payload' 3 | 4 | export const importExportPlugin: Plugin = importExportPluginConfig({}) 5 | -------------------------------------------------------------------------------- /src/lib/payload/get-payload.ts: -------------------------------------------------------------------------------- 1 | import configPromise from '@payload-config' 2 | import { type BetterAuthPluginOptions, getPayloadAuth } from 'payload-auth/better-auth' 3 | 4 | export const getPayload = async () => getPayloadAuth(configPromise) 5 | -------------------------------------------------------------------------------- /src/lib/payload/index.ts: -------------------------------------------------------------------------------- 1 | export * from './can-use-dom' 2 | export * from './deep-merge' 3 | export * from './format-authors' 4 | export * from './generate-meta' 5 | export * from './get-media-url' 6 | export * from './get-url' 7 | export * from './merge-open-graph' 8 | -------------------------------------------------------------------------------- /src/app/api/auth/[...all]/route.ts: -------------------------------------------------------------------------------- 1 | import { getPayload } from '@/lib/payload/get-payload' 2 | 3 | import { toNextJsHandler } from 'better-auth/next-js' 4 | 5 | const payload = await getPayload() 6 | 7 | export const { POST, GET } = toNextJsHandler(payload.betterAuth) 8 | -------------------------------------------------------------------------------- /src/components/payload/admin-icon.tsx: -------------------------------------------------------------------------------- 1 | import { AcmeLogoIcon } from '../icons' 2 | 3 | export default function AdminIcon() { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /src/access/authenticated.ts: -------------------------------------------------------------------------------- 1 | import type { AccessArgs } from 'payload' 2 | import type { User } from '@/payload-types' 3 | 4 | type isAuthenticated = (args: AccessArgs) => boolean 5 | 6 | export const authenticated: isAuthenticated = ({ req: { user } }) => { 7 | return Boolean(user) 8 | } 9 | -------------------------------------------------------------------------------- /src/components/layout/main.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils' 2 | 3 | export function Main({ children, className }: { children: React.ReactNode; className?: string }) { 4 | return ( 5 |
{children}
6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /src/access/authenticated-or-published.ts: -------------------------------------------------------------------------------- 1 | import type { Access } from 'payload' 2 | 3 | export const authenticatedOrPublished: Access = ({ req: { user } }) => { 4 | if (user) { 5 | return true 6 | } 7 | 8 | return { 9 | _status: { 10 | equals: 'published', 11 | }, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /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/lib/auth/index.ts: -------------------------------------------------------------------------------- 1 | // import configPromise from '@payload-config' 2 | // import { betterAuth } from 'better-auth' 3 | // import { withPayloadAuth } from 'payload-auth/better-auth' 4 | 5 | // export const auth = betterAuth( 6 | // withPayloadAuth({ 7 | // payloadConfig: await configPromise 8 | // }) 9 | // ) 10 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /src/components/payload/admin-logo.tsx: -------------------------------------------------------------------------------- 1 | import { AcmeLogoIcon } from '../icons' 2 | 3 | export default function AdminLogo() { 4 | return ( 5 |
6 | 7 | Acme Admin Portal 8 |
9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils' 2 | 3 | function Skeleton({ className, ...props }: React.ComponentProps<'div'>) { 4 | return ( 5 |
10 | ) 11 | } 12 | 13 | export { Skeleton } 14 | -------------------------------------------------------------------------------- /public/images/pattern-plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/(payload)/api/graphql-playground/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import '@payloadcms/next/css' 5 | 6 | import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes' 7 | 8 | export const GET = GRAPHQL_PLAYGROUND_GET(config) 9 | -------------------------------------------------------------------------------- /src/app/(payload)/api/graphql/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes' 5 | 6 | export const POST = GRAPHQL_POST(config) 7 | 8 | export const OPTIONS = REST_OPTIONS(config) 9 | -------------------------------------------------------------------------------- /src/components/layout/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { ThemeProvider as NextThemesProvider, type ThemeProviderProps } from 'next-themes' 4 | 5 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 6 | return ( 7 | 8 | {children} 9 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /extra/form/Error/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useFormContext } from 'react-hook-form' 4 | 5 | export const Error = ({ name }: { name: string }) => { 6 | const { 7 | formState: { errors }, 8 | } = useFormContext() 9 | return ( 10 |
11 | {(errors[name]?.message as string) || 'This field is required'} 12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/components/payload/live-preview-listener.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useRouter } from 'next/navigation' 4 | import type React from 'react' 5 | 6 | import { getServerSideURL } from '@/lib/payload' 7 | 8 | import { RefreshRouteOnSave as PayloadLivePreview } from '@payloadcms/live-preview-react' 9 | 10 | export const LivePreviewListener: React.FC = () => { 11 | const router = useRouter() 12 | return 13 | } 14 | -------------------------------------------------------------------------------- /src/components/screen/home/signed-in-content.tsx: -------------------------------------------------------------------------------- 1 | import { Container } from '@/components/layout/elements' 2 | import { AnimatedGroup } from '@/components/motion-primitives/animated-group' 3 | import { UserCard } from '@/components/screen/home/user-card' 4 | 5 | export const SignedInContent = () => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 📚 Documentation 4 | url: https://payloadcms.com/docs 5 | about: Check the official PayloadCMS documentation 6 | - name: 🔐 Better Auth Docs 7 | url: https://better-auth.com/docs 8 | about: Check the Better Auth documentation 9 | - name: 💬 Discussions 10 | url: https://github.com/your-username/payload-better-auth-starter/discussions 11 | about: Ask questions and share ideas in Discussions -------------------------------------------------------------------------------- /extra/form/Message/index.tsx: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | 3 | import RichText from '@/components/payload/rich-text' 4 | 5 | import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' 6 | import { Width } from '../Width' 7 | 8 | // @ts-expect-error - message is required 9 | export const Message: React.FC = ({ message }: { message: SerializedEditorState }) => { 10 | return ( 11 | 12 | {message && } 13 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/app/(frontend)/globals.css", 9 | "baseColor": "slate", 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 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /src/collections/global/footer/row-label.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { type RowLabelProps, useRowLabel } from '@payloadcms/ui' 4 | import type { GlobalFooter } from '@/payload-types' 5 | 6 | export const RowLabel: React.FC = (_props) => { 7 | const data = useRowLabel[number]>() 8 | 9 | const label = data?.data?.link?.label 10 | ? `Nav item ${data.rowNumber !== undefined ? data.rowNumber + 1 : ''}: ${data?.data?.link?.label}` 11 | : 'Row' 12 | 13 | return
{label}
14 | } 15 | -------------------------------------------------------------------------------- /src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import { betterAuthPluginOptions } from '@/lib/auth/options' 2 | 3 | import type { Plugin } from 'payload' 4 | import { betterAuthPlugin } from 'payload-auth/better-auth' 5 | import { s3StoragePluginPrivate, s3StoragePluginPublic } from './s3-storage-plugin' 6 | import { seoPlugin } from './seo-plugin' 7 | 8 | export const plugins: Plugin[] = [ 9 | betterAuthPlugin(betterAuthPluginOptions), 10 | seoPlugin, 11 | //* [Extra] Form Plugin *// 12 | // formPlugin, 13 | s3StoragePluginPublic, 14 | s3StoragePluginPrivate, 15 | ] 16 | -------------------------------------------------------------------------------- /src/hooks/use-media-query.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export function useMediaQuery(query: string) { 4 | const [value, setValue] = React.useState(false) 5 | 6 | React.useEffect(() => { 7 | function onChange(event: MediaQueryListEvent) { 8 | setValue(event.matches) 9 | } 10 | 11 | const result = matchMedia(query) 12 | result.addEventListener('change', onChange) 13 | setValue(result.matches) 14 | 15 | return () => result.removeEventListener('change', onChange) 16 | }, [query]) 17 | 18 | return value 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/animation.ts: -------------------------------------------------------------------------------- 1 | import type { UseInViewOptions } from 'motion/react' 2 | 3 | export const inViewOptions = ( 4 | margin: UseInViewOptions['margin'] = '0px 0px -300px 0px', 5 | delay?: number 6 | ) => 7 | ({ 8 | variants: { 9 | visible: { 10 | opacity: 1, 11 | }, 12 | hidden: { 13 | opacity: 0, 14 | }, 15 | }, 16 | transition: { 17 | type: 'spring', 18 | bounce: 0.3, 19 | duration: 1.5, 20 | delay, 21 | }, 22 | viewOptions: { 23 | once: true, 24 | margin, 25 | }, 26 | }) as const 27 | -------------------------------------------------------------------------------- /src/lib/auth/types.ts: -------------------------------------------------------------------------------- 1 | import type { getPayload } from '@/lib/payload/get-payload' 2 | 3 | import type { betterAuthPlugins } from './options' 4 | 5 | type PayloadWithBetterAuth = Awaited> 6 | 7 | export type Session = PayloadWithBetterAuth['betterAuth']['$Infer']['Session'] 8 | export type Account = Awaited< 9 | ReturnType 10 | >[number] 11 | export type DeviceSession = Awaited< 12 | ReturnType 13 | >[number] 14 | export type BetterAuthPlugins = typeof betterAuthPlugins 15 | -------------------------------------------------------------------------------- /src/app/(frontend)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Analytics } from '@vercel/analytics/next' 2 | import { GeistMono } from 'geist/font/mono' 3 | import { GeistSans } from 'geist/font/sans' 4 | import { Providers } from './providers' 5 | 6 | export default function RootLayout({ children }: { children: React.ReactNode }) { 7 | return ( 8 | 9 | 12 | {children} 13 | 14 | 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/collections/global/privacy/config.ts: -------------------------------------------------------------------------------- 1 | import type { GlobalConfig } from 'payload' 2 | import { defaultLexical } from '@/fields/default-lexical' 3 | import { revalidateGlobal } from '../hooks/revalidate-global' 4 | 5 | export const GlobalPrivacy: GlobalConfig<'global-privacy'> = { 6 | slug: 'global-privacy', 7 | access: { 8 | read: () => true, 9 | }, 10 | fields: [ 11 | { 12 | name: 'content', 13 | type: 'richText', 14 | required: true, 15 | editor: defaultLexical, 16 | }, 17 | ], 18 | hooks: { 19 | afterChange: [(args) => revalidateGlobal(args, 'global-privacy')], 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /src/collections/global/terms-of-use/config.ts: -------------------------------------------------------------------------------- 1 | import type { GlobalConfig } from 'payload' 2 | import { defaultLexical } from '@/fields/default-lexical' 3 | import { revalidateGlobal } from '../hooks/revalidate-global' 4 | 5 | export const GlobalTerms: GlobalConfig<'global-terms'> = { 6 | slug: 'global-terms', 7 | access: { 8 | read: () => true, 9 | }, 10 | fields: [ 11 | { 12 | name: 'content', 13 | type: 'richText', 14 | required: true, 15 | editor: defaultLexical, 16 | }, 17 | ], 18 | hooks: { 19 | afterChange: [(args) => revalidateGlobal(args, 'global-terms')], 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /extra/form/Width/index.tsx: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | 3 | import { cn } from '@/lib/utils' 4 | 5 | export const Width = ({ 6 | children, 7 | width, 8 | className, 9 | }: { 10 | children: React.ReactNode 11 | width: string 12 | className?: string 13 | }) => { 14 | let calcWidth: string 15 | switch (width) { 16 | case 'full': 17 | calcWidth = `100%` 18 | break 19 | default: 20 | calcWidth = `calc(${width} * 100% - 0.5rem)` 21 | break 22 | } 23 | return ( 24 |
25 | {children} 26 |
27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | /.idea/* 10 | !/.idea/runConfigurations 11 | 12 | # testing 13 | /coverage 14 | 15 | # next.js 16 | /.next/ 17 | /out/ 18 | 19 | # production 20 | /build 21 | 22 | # misc 23 | .DS_Store 24 | *.pem 25 | 26 | # debug 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | next-env.d.ts 40 | 41 | .env 42 | .env.*.local 43 | 44 | /media 45 | -------------------------------------------------------------------------------- /extra/plugins/form-plugin/before-email.tsx: -------------------------------------------------------------------------------- 1 | import AcmeTemplate from '@/lib/email/email-template' 2 | 3 | import type { BeforeEmail } from '@payloadcms/plugin-form-builder/types' 4 | import { render } from '@react-email/render' 5 | import parse from 'html-react-parser' 6 | 7 | const beforeEmail: BeforeEmail = (emailsToSend, _beforeChangeParams) => { 8 | // modify the emails in any way before they are sent 9 | return Promise.all( 10 | emailsToSend.map(async (email) => ({ 11 | ...email, 12 | html: await render(), 13 | })) 14 | ) 15 | } 16 | 17 | export default beforeEmail 18 | -------------------------------------------------------------------------------- /src/blocks/media-block/config.ts: -------------------------------------------------------------------------------- 1 | import type { Block } from 'payload' 2 | 3 | export const MediaBlock: Block = { 4 | slug: 'mediaBlock', 5 | interfaceName: 'MediaBlock', 6 | imageURL: '/images/blocks/media-block.png', 7 | fields: [ 8 | { 9 | name: 'media', 10 | type: 'upload', 11 | relationTo: 'payload-uploads', 12 | required: true, 13 | }, 14 | { 15 | name: 'zoom', 16 | type: 'checkbox', 17 | label: 'Enable Zoom', 18 | defaultValue: false, 19 | admin: { 20 | description: 'When enabled, the image will be zoomed to full screen when clicked.', 21 | }, 22 | }, 23 | ], 24 | } 25 | -------------------------------------------------------------------------------- /src/hooks/use-mobile.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | const MOBILE_BREAKPOINT = 768 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState(undefined) 7 | 8 | React.useEffect(() => { 9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) 10 | const onChange = () => { 11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 12 | } 13 | mql.addEventListener('change', onChange) 14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 15 | return () => mql.removeEventListener('change', onChange) 16 | }, []) 17 | 18 | return !!isMobile 19 | } 20 | -------------------------------------------------------------------------------- /src/app/(payload)/api/[...slug]/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import '@payloadcms/next/css' 5 | 6 | import { 7 | REST_DELETE, 8 | REST_GET, 9 | REST_OPTIONS, 10 | REST_PATCH, 11 | REST_POST, 12 | REST_PUT, 13 | } from '@payloadcms/next/routes' 14 | 15 | export const GET = REST_GET(config) 16 | export const POST = REST_POST(config) 17 | export const DELETE = REST_DELETE(config) 18 | export const PATCH = REST_PATCH(config) 19 | export const PUT = REST_PUT(config) 20 | export const OPTIONS = REST_OPTIONS(config) 21 | -------------------------------------------------------------------------------- /src/lib/payload/merge-open-graph.ts: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | 3 | import { getServerSideURL } from './get-url' 4 | 5 | const defaultOpenGraph: Metadata['openGraph'] = { 6 | type: 'website', 7 | description: 'Acme. A better way to manage your business.', 8 | images: [ 9 | { 10 | url: `${getServerSideURL()}/website-template-OG.png`, 11 | }, 12 | ], 13 | siteName: 'Acme', 14 | title: 'Acme', 15 | } 16 | 17 | export const mergeOpenGraph = (og?: Metadata['openGraph']): Metadata['openGraph'] => { 18 | return { 19 | ...defaultOpenGraph, 20 | ...og, 21 | images: og?.images ? og.images : defaultOpenGraph.images, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useTheme } from 'next-themes' 4 | import { Toaster as Sonner, type ToasterProps } from 'sonner' 5 | 6 | const Toaster = ({ ...props }: ToasterProps) => { 7 | const { theme = 'system' } = useTheme() 8 | 9 | return ( 10 | 22 | ) 23 | } 24 | 25 | export { Toaster } 26 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import type * as React from 'react' 4 | 5 | import { cn } from '@/lib/utils' 6 | 7 | import { Label as LabelPrimitive } from 'radix-ui' 8 | 9 | function Label({ className, ...props }: React.ComponentProps) { 10 | return ( 11 | 19 | ) 20 | } 21 | 22 | export { Label } 23 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod' 2 | 3 | export const envSchema = z.object({ 4 | DATABASE_URI: z.string(), 5 | PAYLOAD_SECRET: z.string(), 6 | NEXT_PUBLIC_SERVER_URL: z.string(), 7 | CRON_SECRET: z.string(), 8 | PREVIEW_SECRET: z.string(), 9 | BETTER_AUTH_SECRET: z.string(), 10 | NEXT_PUBLIC_BETTER_AUTH_URL: z.string(), 11 | S3_BUCKET: z.string(), 12 | S3_ACCESS_KEY_ID: z.string(), 13 | S3_SECRET_ACCESS_KEY: z.string(), 14 | S3_REGION: z.string(), 15 | S3_ENDPOINT: z.string(), 16 | RESEND_API_KEY: z.string(), 17 | NEXT_PUBLIC_HCAPTCHA_SITE_KEY: z.string(), 18 | }) 19 | 20 | declare global { 21 | namespace NodeJS { 22 | interface ProcessEnv extends z.infer {} 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/plugins/seo-plugin.ts: -------------------------------------------------------------------------------- 1 | import { getServerSideURL } from '@/lib/payload' 2 | 3 | import { seoPlugin as seoPluginConfig } from '@payloadcms/plugin-seo' 4 | import type { GenerateTitle, GenerateURL } from '@payloadcms/plugin-seo/types' 5 | import type { Plugin } from 'payload' 6 | import type { Blog } from '@/payload-types' 7 | 8 | const generateTitle: GenerateTitle = ({ doc }) => { 9 | return doc?.title ? `${doc.title} | Acme` : 'Acme' 10 | } 11 | 12 | const generateURL: GenerateURL = ({ doc }) => { 13 | const url = getServerSideURL() 14 | 15 | return doc?.slug ? `${url}/${doc.slug}` : url 16 | } 17 | 18 | export const seoPlugin: Plugin = seoPluginConfig({ 19 | generateTitle, 20 | generateURL, 21 | }) 22 | -------------------------------------------------------------------------------- /extra/form/fields.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox } from './Checkbox' 2 | import { Country } from './Country' 3 | import { Email } from './Email' 4 | import { Message } from './Message' 5 | import { Number } from './Number' 6 | import { Phone } from './Phone' 7 | import { Select } from './Select' 8 | import { State } from './State' 9 | import { Text } from './Text' 10 | import { Textarea } from './Textarea' 11 | import { UserInfo } from './UserInfo' 12 | 13 | export const fields = { 14 | checkbox: Checkbox, 15 | country: Country, 16 | email: Email, 17 | message: Message, 18 | number: Number, 19 | select: Select, 20 | state: State, 21 | text: Text, 22 | textarea: Textarea, 23 | phone: Phone, 24 | userInfo: UserInfo, 25 | } 26 | -------------------------------------------------------------------------------- /src/app/(frontend)/providers.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react' 2 | 3 | import { BetterAuthUIProvider } from '@/components/layout/better-auth-ui-provider' 4 | import { ThemeProvider } from '@/components/layout/theme-provider' 5 | import { Toaster } from '@/components/ui/sonner' 6 | 7 | import { BetterAuthProvider } from '@/lib/auth/context' 8 | import { getContextProps } from '@/lib/auth/context/get-context-props' 9 | 10 | export function Providers({ children }: { children: ReactNode }) { 11 | return ( 12 | 13 | 14 | {children} 15 | 16 | 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/components/payload/media/index.tsx: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | import { Fragment } from 'react' 3 | 4 | import { ImageMedia } from './image-media' 5 | import type { Props } from './types' 6 | import { VideoMedia } from './video-media' 7 | 8 | export const Media: React.FC = (props) => { 9 | const { className, htmlElement = 'div', resource } = props 10 | 11 | const isVideo = typeof resource === 'object' && resource?.mimeType?.includes('video') 12 | const Tag = htmlElement || Fragment 13 | 14 | return ( 15 | 22 | {isVideo ? : } 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/blocks/copyright-inline-block/component.tsx: -------------------------------------------------------------------------------- 1 | import type { HTMLAttributes } from 'react' 2 | 3 | import { cn } from '@/lib/utils' 4 | 5 | import type { CopyRightInlineBlock as CopyRightInlineBlockProps } from '@/payload-types' 6 | 7 | type Props = CopyRightInlineBlockProps & HTMLAttributes 8 | 9 | /** 10 | * A simple block that displays a legal disclaimer upto the current year. 11 | */ 12 | export const CopyRightInlineBlock: React.FC = ({ 13 | className, 14 | fromYear, 15 | toYearType, 16 | toYearFixed, 17 | text, 18 | }: Props) => { 19 | return ( 20 | 21 | {`© Copyright ${fromYear}~${toYearType === 'current' ? new Date().getFullYear() : toYearFixed}, ${text}.`} 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/lib/payload/get-globals.ts: -------------------------------------------------------------------------------- 1 | import { unstable_cache } from 'next/cache' 2 | 3 | import configPromise from '@payload-config' 4 | import { getPayload } from 'payload' 5 | import type { Config } from 'src/payload-types' 6 | 7 | type Global = keyof Config['globals'] 8 | 9 | async function getGlobal(slug: Global, depth = 0) { 10 | const payload = await getPayload({ config: configPromise }) 11 | 12 | const global = await payload.findGlobal({ 13 | slug, 14 | depth, 15 | }) 16 | 17 | return global 18 | } 19 | 20 | /** 21 | * Returns a unstable_cache function mapped with the cache tag for the slug 22 | */ 23 | export const getCachedGlobal = (slug: Global, depth = 0) => 24 | unstable_cache(async () => getGlobal(slug, depth), [slug], { 25 | tags: [slug], 26 | }) 27 | -------------------------------------------------------------------------------- /src/collections/global/hooks/revalidate-global.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | 3 | import { revalidateTag } from 'next/cache' 4 | 5 | import type { PayloadRequest, RequestContext, SanitizedGlobalConfig } from 'payload' 6 | 7 | export const revalidateGlobal = ( 8 | { 9 | doc, 10 | req: { payload, context }, 11 | }: { 12 | context: RequestContext 13 | data: any 14 | doc: any 15 | /** The global which this hook is being run on */ 16 | global: SanitizedGlobalConfig 17 | previousDoc: any 18 | req: PayloadRequest 19 | }, 20 | tag: string 21 | ) => { 22 | if (!context.disableRevalidate) { 23 | payload.logger.info(`✨ Revalidating ${tag}`) 24 | 25 | revalidateTag(tag, "max") 26 | } 27 | 28 | return doc 29 | } 30 | -------------------------------------------------------------------------------- /src/app/(frontend)/(public)/page.tsx: -------------------------------------------------------------------------------- 1 | import { LayoutHeader, SectionSpacing } from '@/components/layout/elements' 2 | import { Main } from '@/components/layout/main' 3 | import { DevTools } from '@/components/screen/home/dev-tools' 4 | import { Features } from '@/components/screen/home/features' 5 | import { ThemeColors } from '@/components/screen/home/theme-colors' 6 | 7 | export default function Home() { 8 | return ( 9 |
10 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/access/admin.ts: -------------------------------------------------------------------------------- 1 | import type { Access, AccessArgs, TypedUser } from 'payload' 2 | 3 | type isAdmin = (args: AccessArgs) => boolean 4 | type isAdminOrSelf = Access 5 | 6 | const checkIsAdmin = (user: TypedUser | null) => { 7 | if (!user || !user.role || !user.role.includes('admin')) return false 8 | return true 9 | } 10 | 11 | export const isAdmin: isAdmin = ({ req: { user } }) => { 12 | return checkIsAdmin(user) 13 | } 14 | 15 | export const isAdminOrSelf: isAdminOrSelf = ({ req: { user } }) => { 16 | if (checkIsAdmin(user)) return true 17 | return { 18 | userId: { 19 | equals: user?.id, 20 | }, 21 | } 22 | } 23 | 24 | export const adminOrPublished: Access = ({ req: { user } }) => { 25 | if (checkIsAdmin(user)) return true 26 | return { 27 | _status: { 28 | equals: 'published', 29 | }, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import type * as React from 'react' 4 | 5 | import { cn } from '@/lib/utils' 6 | 7 | import { Separator as SeparatorPrimitive } from 'radix-ui' 8 | 9 | function Separator({ 10 | className, 11 | orientation = 'horizontal', 12 | decorative = true, 13 | ...props 14 | }: React.ComponentProps) { 15 | return ( 16 | 26 | ) 27 | } 28 | 29 | export { Separator } 30 | -------------------------------------------------------------------------------- /src/app/(payload)/admin/[[...segments]]/page.tsx: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import type { Metadata } from 'next' 4 | 5 | import config from '@payload-config' 6 | import { generatePageMetadata, RootPage } from '@payloadcms/next/views' 7 | import { importMap } from '../importMap' 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) => 22 | RootPage({ config, params, searchParams, importMap }) 23 | 24 | export default Page 25 | -------------------------------------------------------------------------------- /src/app/(payload)/custom.scss: -------------------------------------------------------------------------------- 1 | [data-theme="dark"] { 2 | .admin-logo { 3 | fill: #fff; 4 | } 5 | .admin-icon { 6 | stroke: #fff; 7 | } 8 | } 9 | 10 | [data-theme="light"] { 11 | .admin-logo { 12 | fill: #000; 13 | } 14 | .admin-icon { 15 | stroke: #000; 16 | } 17 | } 18 | 19 | .admin-icon-container { 20 | height: 18px; 21 | width: 18px; 22 | .admin-icon { 23 | width: 18px; 24 | height: 18px; 25 | } 26 | } 27 | 28 | .admin-logo-container { 29 | display: flex; 30 | flex-direction: column; 31 | align-items: center; 32 | justify-content: center; 33 | width: 100%; 34 | 35 | .admin-logo-icon { 36 | width: 4rem; 37 | height: 4rem; 38 | } 39 | .admin-logo-text { 40 | font-size: 14px; 41 | font-weight: medium; 42 | opacity: 0.5; 43 | margin-top: 0.5rem; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import type * as React from 'react' 2 | 3 | import { cn } from '@/lib/utils' 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) { 6 | return ( 7 |