commit c0cb0ff0ed3cae9eec7592d68d75952da0628f6d Author: Jinho Jang <41753422+jinhojang6@users.noreply.github.com> Date: Mon Apr 20 22:24:39 2026 +0900 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..57dcc23051 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# Dependencies +node_modules +.pnp +.pnp.js + +# Local env files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Testing +coverage + +# Turbo +.turbo + +# Vercel +.vercel + +# Build Outputs +.next/ +out/ +build +dist +coverage +next-env.d.ts +*.tsbuildinfo +*.db +*.db-* +*.sqlite +*.sqlite3 + + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Misc +.DS_Store +*.pem diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000000..e69de29bb2 diff --git a/README.md b/README.md new file mode 100644 index 0000000000..8ae1cb142f --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# logos-turborepo-next-tailwind-i18n-template + +A pnpm + Turborepo starter with: + +- `apps/web`: Next.js frontend with Tailwind CSS v4 and `next-intl` +- `apps/cms`: standalone Payload CMS app with Admin dashboard +- `packages/ui`: shared React UI primitives +- `packages/config`: shared ESLint / TypeScript / Prettier config +- `packages/types`: shared application types, including Payload-generated types + +## Why this shape + +- Apps stay as entrypoints only. +- Shared code lives in purpose-specific packages. +- The CMS is isolated from the frontend deployment boundary. +- Payload-generated types can be shared without coupling the frontend to Payload runtime packages. + +## Commands + +```bash +pnpm install +pnpm dev +pnpm build +pnpm check-types +pnpm generate-types +``` + +## Local URLs + +- Web: `http://localhost:3000` +- CMS: `http://localhost:3001` +- Payload Admin: `http://localhost:3001/admin` diff --git a/apps/cms/.env.example b/apps/cms/.env.example new file mode 100644 index 0000000000..33a1ab3e09 --- /dev/null +++ b/apps/cms/.env.example @@ -0,0 +1,4 @@ +PAYLOAD_SECRET=change-me +DATABASE_URL=file:./cms.db +NEXT_PUBLIC_SERVER_URL=http://localhost:3001 +NEXT_PUBLIC_WEB_URL=http://localhost:3000 diff --git a/apps/cms/eslint.config.mjs b/apps/cms/eslint.config.mjs new file mode 100644 index 0000000000..99419874d8 --- /dev/null +++ b/apps/cms/eslint.config.mjs @@ -0,0 +1,3 @@ +import nextConfig from '../../packages/config/eslint/next.mjs' + +export default nextConfig diff --git a/apps/cms/next.config.mjs b/apps/cms/next.config.mjs new file mode 100644 index 0000000000..2c738ddd29 --- /dev/null +++ b/apps/cms/next.config.mjs @@ -0,0 +1,14 @@ +import { fileURLToPath } from 'node:url' + +import { withPayload } from '@payloadcms/next/withPayload' + +const workspaceRoot = fileURLToPath(new URL('../..', import.meta.url)) + +const nextConfig = { + reactStrictMode: true, + turbopack: { + root: workspaceRoot, + }, +} + +export default withPayload(nextConfig) diff --git a/apps/cms/package.json b/apps/cms/package.json new file mode 100644 index 0000000000..cb2820b3df --- /dev/null +++ b/apps/cms/package.json @@ -0,0 +1,32 @@ +{ + "name": "cms", + "private": true, + "type": "module", + "scripts": { + "dev": "next dev --port 3001", + "build": "next build", + "start": "next start", + "lint": "eslint . --max-warnings 0", + "lint:fix": "eslint . --fix", + "check-types": "next typegen && tsc --noEmit", + "generate-types": "PAYLOAD_CONFIG_PATH=payload.config.ts payload generate:types" + }, + "dependencies": { + "@payloadcms/db-sqlite": "3.83.0", + "@payloadcms/next": "3.83.0", + "@payloadcms/richtext-lexical": "3.83.0", + "graphql": "^16.13.2", + "next": "16.2.4", + "payload": "3.83.0", + "react": "^19.2.5", + "react-dom": "^19.2.5", + "sharp": "^0.34.5" + }, + "devDependencies": { + "@types/node": "25.6.0", + "@types/react": "^19.2.14", + "@types/react-dom": "19.2.3", + "eslint": "^10.2.1", + "typescript": "^6.0.3" + } +} diff --git a/apps/cms/payload.config.ts b/apps/cms/payload.config.ts new file mode 100644 index 0000000000..2da49160a0 --- /dev/null +++ b/apps/cms/payload.config.ts @@ -0,0 +1,37 @@ +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import { sqliteAdapter } from '@payloadcms/db-sqlite' +import { lexicalEditor } from '@payloadcms/richtext-lexical' +import { buildConfig } from 'payload' + +import { Pages } from './src/collections/Pages' +import { Users } from './src/collections/Users' +import { SiteSettings } from './src/globals/SiteSettings' + +const dirname = path.dirname(fileURLToPath(import.meta.url)) +const serverURL = process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:3001' +const frontendURL = process.env.NEXT_PUBLIC_WEB_URL || 'http://localhost:3000' + +export default buildConfig({ + admin: { + user: Users.slug, + }, + collections: [Users, Pages], + cors: [serverURL, frontendURL], + csrf: [serverURL, frontendURL], + db: sqliteAdapter({ + client: { + url: process.env.DATABASE_URL || 'file:./cms.db', + }, + wal: true, + }), + editor: lexicalEditor(), + globals: [SiteSettings], + secret: process.env.PAYLOAD_SECRET || 'dev-secret', + serverURL, + typescript: { + declare: false, + outputFile: path.resolve(dirname, '../../packages/types/src/payload.ts'), + }, +}) diff --git a/apps/cms/src/app/(frontend)/globals.css b/apps/cms/src/app/(frontend)/globals.css new file mode 100644 index 0000000000..b0db05b5c3 --- /dev/null +++ b/apps/cms/src/app/(frontend)/globals.css @@ -0,0 +1,13 @@ +html { + font-family: ui-sans-serif, system-ui, sans-serif; +} + +body { + margin: 0; + background: #0b1020; + color: #f3f4f6; +} + +a { + color: inherit; +} diff --git a/apps/cms/src/app/(frontend)/layout.tsx b/apps/cms/src/app/(frontend)/layout.tsx new file mode 100644 index 0000000000..a430819655 --- /dev/null +++ b/apps/cms/src/app/(frontend)/layout.tsx @@ -0,0 +1,11 @@ +import './globals.css' + +import type { ReactNode } from 'react' + +export default function FrontendLayout({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/apps/cms/src/app/(frontend)/page.tsx b/apps/cms/src/app/(frontend)/page.tsx new file mode 100644 index 0000000000..4217cb609a --- /dev/null +++ b/apps/cms/src/app/(frontend)/page.tsx @@ -0,0 +1,65 @@ +export default function CmsHomePage() { + return ( +
+ + Payload CMS + +

+ Standalone CMS app inside a Turborepo workspace. +

+

+ Use this app for admin, content APIs, migrations, and generated types. + Keep the public frontend deployed independently in `apps/web`. +

+
+ + Open Admin + + + REST API + +
+
+ ) +} diff --git a/apps/cms/src/app/(payload)/admin/[[...segments]]/not-found.tsx b/apps/cms/src/app/(payload)/admin/[[...segments]]/not-found.tsx new file mode 100644 index 0000000000..9ed6b7a65e --- /dev/null +++ b/apps/cms/src/app/(payload)/admin/[[...segments]]/not-found.tsx @@ -0,0 +1,14 @@ +import { NotFoundPage } from '@payloadcms/next/views' + +import config from '@payload-config' +import { importMap } from '../importMap' + +export default function NotFound({ + params, + searchParams, +}: { + params: Promise<{ segments: string[] }> + searchParams: Promise<{ [key: string]: string | string[] }> +}) { + return NotFoundPage({ config, importMap, params, searchParams }) +} diff --git a/apps/cms/src/app/(payload)/admin/[[...segments]]/page.tsx b/apps/cms/src/app/(payload)/admin/[[...segments]]/page.tsx new file mode 100644 index 0000000000..337b788e16 --- /dev/null +++ b/apps/cms/src/app/(payload)/admin/[[...segments]]/page.tsx @@ -0,0 +1,22 @@ +import { generatePageMetadata, RootPage } from '@payloadcms/next/views' + +import config from '@payload-config' +import { importMap } from '../importMap' + +export const generateMetadata = async ({ + params, + searchParams, +}: { + params: Promise<{ segments: string[] }> + searchParams: Promise<{ [key: string]: string | string[] }> +}) => generatePageMetadata({ config, params, searchParams }) + +export default function Page({ + params, + searchParams, +}: { + params: Promise<{ segments: string[] }> + searchParams: Promise<{ [key: string]: string | string[] }> +}) { + return RootPage({ config, importMap, params, searchParams }) +} diff --git a/apps/cms/src/app/(payload)/admin/importMap.js b/apps/cms/src/app/(payload)/admin/importMap.js new file mode 100644 index 0000000000..cbb310a255 --- /dev/null +++ b/apps/cms/src/app/(payload)/admin/importMap.js @@ -0,0 +1,2 @@ +/** @type {import('payload').ImportMap} */ +export const importMap = {} diff --git a/apps/cms/src/app/(payload)/api/[...slug]/route.ts b/apps/cms/src/app/(payload)/api/[...slug]/route.ts new file mode 100644 index 0000000000..e46003ea5e --- /dev/null +++ b/apps/cms/src/app/(payload)/api/[...slug]/route.ts @@ -0,0 +1,17 @@ +import { + REST_DELETE, + REST_GET, + REST_OPTIONS, + REST_PATCH, + REST_POST, + REST_PUT, +} from '@payloadcms/next/routes' + +import config from '@payload-config' + +export const GET = REST_GET(config) +export const POST = REST_POST(config) +export const DELETE = REST_DELETE(config) +export const PATCH = REST_PATCH(config) +export const PUT = REST_PUT(config) +export const OPTIONS = REST_OPTIONS(config) diff --git a/apps/cms/src/app/(payload)/layout.tsx b/apps/cms/src/app/(payload)/layout.tsx new file mode 100644 index 0000000000..728e49fc6e --- /dev/null +++ b/apps/cms/src/app/(payload)/layout.tsx @@ -0,0 +1,37 @@ +import type { ReactNode } from 'react' + +import { + RootLayout, + handleServerFunctions, + metadata, +} from '@payloadcms/next/layouts' + +import config from '@payload-config' +import { importMap } from './admin/importMap' + +export { metadata } + +const serverFunction = async ( + ...args: Parameters +) => { + 'use server' + + const [payloadArgs] = args + return handleServerFunctions({ + ...payloadArgs, + config, + importMap, + }) +} + +export default function Layout({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/apps/cms/src/collections/Pages.ts b/apps/cms/src/collections/Pages.ts new file mode 100644 index 0000000000..773b3c6989 --- /dev/null +++ b/apps/cms/src/collections/Pages.ts @@ -0,0 +1,30 @@ +import type { CollectionConfig } from 'payload' + +export const Pages: CollectionConfig = { + slug: 'pages', + admin: { + defaultColumns: ['title', 'slug', 'updatedAt'], + useAsTitle: 'title', + }, + fields: [ + { + name: 'title', + type: 'text', + required: true, + }, + { + name: 'slug', + type: 'text', + index: true, + required: true, + unique: true, + }, + { + name: 'content', + type: 'richText', + }, + ], + versions: { + drafts: true, + }, +} diff --git a/apps/cms/src/collections/Users.ts b/apps/cms/src/collections/Users.ts new file mode 100644 index 0000000000..4c067db214 --- /dev/null +++ b/apps/cms/src/collections/Users.ts @@ -0,0 +1,15 @@ +import type { CollectionConfig } from 'payload' + +export const Users: CollectionConfig = { + slug: 'users', + admin: { + useAsTitle: 'email', + }, + auth: true, + fields: [ + { + name: 'name', + type: 'text', + }, + ], +} diff --git a/apps/cms/src/globals/SiteSettings.ts b/apps/cms/src/globals/SiteSettings.ts new file mode 100644 index 0000000000..e6c911100f --- /dev/null +++ b/apps/cms/src/globals/SiteSettings.ts @@ -0,0 +1,16 @@ +import type { GlobalConfig } from 'payload' + +export const SiteSettings: GlobalConfig = { + slug: 'siteSettings', + fields: [ + { + name: 'siteName', + type: 'text', + required: true, + }, + { + name: 'siteDescription', + type: 'textarea', + }, + ], +} diff --git a/apps/cms/tsconfig.json b/apps/cms/tsconfig.json new file mode 100644 index 0000000000..4a69cb1c2b --- /dev/null +++ b/apps/cms/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../packages/config/typescript/next.json", + "compilerOptions": { + "paths": { + "@/*": ["./src/*"], + "@payload-config": ["./payload.config.ts"] + }, + "plugins": [ + { + "name": "next" + } + ], + "strictNullChecks": true + }, + "include": [ + "next-env.d.ts", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + "**/*.ts", + "**/*.tsx", + "**/*.mjs", + "**/*.json" + ], + "exclude": ["node_modules"] +} diff --git a/apps/web/app/[locale]/layout.tsx b/apps/web/app/[locale]/layout.tsx new file mode 100644 index 0000000000..4678406a59 --- /dev/null +++ b/apps/web/app/[locale]/layout.tsx @@ -0,0 +1,51 @@ +import '@/css/tailwind.css' + +import Header from '@/components/site-headaer' +import Footer from '@/components/site-footer' +import { themeInitScript } from '@/utils/theme' + +import { NextIntlClientProvider } from 'next-intl' +import LocaleSwitcher from '@/components/locale/locale-switcher' + +export default async function RootLayout({ + children, + params, +}: { + children: React.ReactNode + params: Promise<{ locale: string }> +}) { + const { locale } = await params + + return ( + + + + + + + + +