mirror of
https://github.com/logos-co/logos-web.git
synced 2026-08-27 12:11:14 +00:00
Initial commit
This commit is contained in:
+45
@@ -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
|
||||
@@ -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`
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
import nextConfig from '../../packages/config/eslint/next.mjs'
|
||||
|
||||
export default nextConfig
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
html {
|
||||
font-family: ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: #0b1020;
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import './globals.css'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
export default function FrontendLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
export default function CmsHomePage() {
|
||||
return (
|
||||
<main
|
||||
style={{
|
||||
display: 'grid',
|
||||
gap: '1rem',
|
||||
maxWidth: '720px',
|
||||
margin: '0 auto',
|
||||
minHeight: '100vh',
|
||||
alignContent: 'center',
|
||||
padding: '2rem',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 'fit-content',
|
||||
border: '1px solid rgba(255,255,255,0.15)',
|
||||
borderRadius: '999px',
|
||||
padding: '0.35rem 0.75rem',
|
||||
fontSize: '0.875rem',
|
||||
}}
|
||||
>
|
||||
Payload CMS
|
||||
</span>
|
||||
<h1 style={{ fontSize: '3rem', lineHeight: 1.05, margin: 0 }}>
|
||||
Standalone CMS app inside a Turborepo workspace.
|
||||
</h1>
|
||||
<p
|
||||
style={{
|
||||
color: 'rgba(243,244,246,0.72)',
|
||||
fontSize: '1.125rem',
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
Use this app for admin, content APIs, migrations, and generated types.
|
||||
Keep the public frontend deployed independently in `apps/web`.
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap' }}>
|
||||
<a
|
||||
href="/admin"
|
||||
style={{
|
||||
background: '#f3f4f6',
|
||||
color: '#0b1020',
|
||||
borderRadius: '999px',
|
||||
padding: '0.75rem 1rem',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
Open Admin
|
||||
</a>
|
||||
<a
|
||||
href="/api/pages"
|
||||
style={{
|
||||
border: '1px solid rgba(255,255,255,0.15)',
|
||||
borderRadius: '999px',
|
||||
padding: '0.75rem 1rem',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
REST API
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
/** @type {import('payload').ImportMap} */
|
||||
export const importMap = {}
|
||||
@@ -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)
|
||||
@@ -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<typeof handleServerFunctions>
|
||||
) => {
|
||||
'use server'
|
||||
|
||||
const [payloadArgs] = args
|
||||
return handleServerFunctions({
|
||||
...payloadArgs,
|
||||
config,
|
||||
importMap,
|
||||
})
|
||||
}
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<RootLayout
|
||||
config={config}
|
||||
importMap={importMap}
|
||||
serverFunction={serverFunction}
|
||||
>
|
||||
{children}
|
||||
</RootLayout>
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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 (
|
||||
<NextIntlClientProvider>
|
||||
<html lang={locale} className={`scroll-smooth`} suppressHydrationWarning>
|
||||
<head>
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/favicon.ico" />
|
||||
<link rel="icon" href="/favicon.ico" type="image/x-icon" />
|
||||
<meta name="msapplication-TileColor" content="#000000" />
|
||||
<meta
|
||||
name="theme-color"
|
||||
media="(prefers-color-scheme: light)"
|
||||
content="#fff"
|
||||
/>
|
||||
<meta
|
||||
name="theme-color"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
content="#000"
|
||||
/>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: themeInitScript,
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<Header />
|
||||
<main>{children}</main>
|
||||
<LocaleSwitcher />
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
</NextIntlClientProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<h1 className="text-4xl font-light text-gray-500">404 | Not Found</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import ThemeToggle from '@/components/theme-toggle'
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
import { createDefaultMetadata } from '@/utils/metadata'
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>
|
||||
}) {
|
||||
const { locale } = await params
|
||||
|
||||
const metadata = await createDefaultMetadata({
|
||||
title: 'Home',
|
||||
description: 'Home Description',
|
||||
locale,
|
||||
path: ROUTES.home,
|
||||
})
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const t = useTranslations('home')
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="space-y-2 pt-6 pb-8 md:space-y-5">
|
||||
<h1 className="text-3xl leading-9 font-extrabold tracking-tight text-gray-900 sm:text-4xl sm:leading-10 md:text-6xl md:leading-14 dark:text-gray-100">
|
||||
{t('title')}
|
||||
</h1>
|
||||
<br />
|
||||
<p className="text-lg leading-7 text-gray-500 dark:text-gray-400">
|
||||
Description
|
||||
</p>
|
||||
<br />
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { MetadataRoute } from 'next'
|
||||
import siteConfig from '@/data/siteConfig'
|
||||
|
||||
export const dynamic = 'force-static'
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: {
|
||||
userAgent: '*',
|
||||
allow: '/',
|
||||
},
|
||||
sitemap: `${siteConfig.url}/sitemap.xml`,
|
||||
host: siteConfig.url,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import ThemeToggle from '@/components/theme-toggle'
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
import { createDefaultMetadata } from '@/utils/metadata'
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>
|
||||
}) {
|
||||
const { locale } = await params
|
||||
|
||||
const metadata = await createDefaultMetadata({
|
||||
title: 'Test',
|
||||
description: 'Test page description ',
|
||||
locale,
|
||||
path: ROUTES.test,
|
||||
})
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
export default async function Page() {
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="space-y-2 pt-6 pb-8 md:space-y-5">
|
||||
<h1 className="text-3xl leading-9 font-extrabold tracking-tight text-gray-900 sm:text-4xl sm:leading-10 md:text-6xl md:leading-14 dark:text-gray-100">
|
||||
Test Page
|
||||
</h1>
|
||||
<br />
|
||||
<p className="text-lg leading-7 text-gray-500 dark:text-gray-400">
|
||||
Description
|
||||
</p>
|
||||
<br />
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { ImageResponse } from 'next/og'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const title = searchParams.get('title')
|
||||
const description = searchParams.get('description')
|
||||
|
||||
const siteConfig = await import('@/data/siteConfig')
|
||||
const { name } = siteConfig.default
|
||||
|
||||
return new ImageResponse(
|
||||
<div tw="flex w-full h-full text-black bg-white">
|
||||
{(title || description) && (
|
||||
<div tw="flex absolute right-24 bottom-24 flex-row justify-center items-center text-white">
|
||||
<div tw="text-black flex text-[32px] font-semibold tracking-tight ml-2">
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div tw="flex absolute inset-0 flex-col justify-center items-center p-24 w-full h-full">
|
||||
{title || description ? (
|
||||
<div tw="flex flex-col justify-center items-center w-full h-full text-center">
|
||||
<div tw="tracking-tight flex flex-col justify-center text-black text-balance font-semibold text-[80px]">
|
||||
{title}
|
||||
</div>
|
||||
<div tw="text-[40px] text-gray-600 mt-6 text-balance font-normal">
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div tw="flex flex-col justify-center items-center w-full h-full text-center">
|
||||
<div tw="flex flex-row justify-center items-center space-x-4"></div>
|
||||
<div tw="text-black flex text-[80px] font-semibold tracking-tight">
|
||||
{name}
|
||||
</div>
|
||||
<div tw="flex text-2xl text-gray-600">
|
||||
<p>Logos Next Tailwind Template</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>,
|
||||
{
|
||||
width: 1200,
|
||||
height: 628,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { MetadataRoute } from 'next'
|
||||
import siteConfig from '@/data/siteConfig'
|
||||
|
||||
export const dynamic = 'force-static'
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const siteUrl = siteConfig.url
|
||||
|
||||
const routes = [''].map((route) => ({
|
||||
url: `${siteUrl}/${route}`,
|
||||
lastModified: new Date().toISOString().split('T')[0],
|
||||
}))
|
||||
|
||||
return [...routes]
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
'use client'
|
||||
|
||||
import { ChangeEvent, ReactNode, useTransition } from 'react'
|
||||
import { usePathname } from '@/i18n/navigation'
|
||||
import clsx from 'clsx'
|
||||
import { Locale } from 'next-intl'
|
||||
|
||||
type Props = {
|
||||
children: ReactNode
|
||||
defaultValue: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export default function LocaleSwitcherSelect({
|
||||
children,
|
||||
defaultValue,
|
||||
}: Props) {
|
||||
const [isPending, startTransition] = useTransition()
|
||||
const pathname = usePathname()
|
||||
|
||||
function onSelectChange(event: ChangeEvent<HTMLSelectElement>) {
|
||||
const nextLocale = event.target.value as Locale
|
||||
startTransition(() => {
|
||||
window.location.pathname = `/${nextLocale}${pathname}`
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative inline-block w-24">
|
||||
<select
|
||||
className={clsx(
|
||||
'text-foreground w-full appearance-none border border-black px-3 py-1 text-sm dark:border-white',
|
||||
'cursor-pointer',
|
||||
isPending && 'transition-opacity [&:disabled]:opacity-30'
|
||||
)}
|
||||
defaultValue={defaultValue}
|
||||
disabled={isPending}
|
||||
onChange={onSelectChange}
|
||||
>
|
||||
<optgroup label="Language">{children}</optgroup>
|
||||
</select>
|
||||
|
||||
<span className="pointer-events-none absolute top-1/2 right-2 -translate-y-1/2 text-gray-500">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M5.23 7.21a.75.75 0 011.06 0L10 10.44l3.71-3.23a.75.75 0 111.06 1.06l-4 3.5a.75.75 0 01-1.06 0l-4-3.5z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { routing } from '@/i18n/routing'
|
||||
import { useLocale, useTranslations } from 'next-intl'
|
||||
|
||||
import LocaleSwitcherSelect from './locale-switcher-select'
|
||||
|
||||
export default function LocaleSwitcher() {
|
||||
const t = useTranslations('locale')
|
||||
const locale = useLocale()
|
||||
|
||||
return (
|
||||
<LocaleSwitcherSelect defaultValue={locale} label={t('language')}>
|
||||
{routing.locales.map((cur) => (
|
||||
<option key={cur} value={cur}>
|
||||
{t(cur, { locale: cur })}
|
||||
</option>
|
||||
))}
|
||||
</LocaleSwitcherSelect>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function Footer() {
|
||||
return <footer>Footer</footer>
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
const Header = () => {
|
||||
return (
|
||||
<header>
|
||||
<span>Header</span>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
@@ -0,0 +1,28 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export default function ThemeToggle() {
|
||||
const [theme, setTheme] = useState<'light' | 'dark'>('light')
|
||||
|
||||
useEffect(() => {
|
||||
const savedTheme = localStorage.getItem('theme') as 'light' | 'dark'
|
||||
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
.matches
|
||||
? 'dark'
|
||||
: 'light'
|
||||
const initialTheme = savedTheme || systemTheme
|
||||
|
||||
setTheme(initialTheme)
|
||||
document.documentElement.setAttribute('data-theme', initialTheme)
|
||||
}, [])
|
||||
|
||||
const toggleTheme = () => {
|
||||
const newTheme = theme === 'dark' ? 'light' : 'dark'
|
||||
setTheme(newTheme)
|
||||
document.documentElement.setAttribute('data-theme', newTheme)
|
||||
localStorage.setItem('theme', newTheme)
|
||||
}
|
||||
|
||||
return <button onClick={toggleTheme}>Theme Toggle</button>
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export const ROUTES = {
|
||||
home: '/',
|
||||
test: '/test',
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
/* Core theme configuration */
|
||||
@theme {
|
||||
/* Font families */
|
||||
--font-sans:
|
||||
var(--font-space-grotesk), ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
|
||||
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
|
||||
/* Colors */
|
||||
/* Copied from https://tailwindcss.com/docs/theme#default-theme-variable-reference */
|
||||
--color-primary-50: oklch(0.971 0.014 343.198);
|
||||
--color-primary-100: oklch(0.948 0.028 342.258);
|
||||
--color-primary-200: oklch(0.899 0.061 343.231);
|
||||
--color-primary-300: oklch(0.823 0.12 346.018);
|
||||
--color-primary-400: oklch(0.718 0.202 349.761);
|
||||
--color-primary-500: oklch(0.656 0.241 354.308);
|
||||
--color-primary-600: oklch(0.592 0.249 0.584);
|
||||
--color-primary-700: oklch(0.525 0.223 3.958);
|
||||
--color-primary-800: oklch(0.459 0.187 3.815);
|
||||
--color-primary-900: oklch(0.408 0.153 2.432);
|
||||
--color-primary-950: oklch(0.284 0.109 3.907);
|
||||
|
||||
--color-gray-50: oklch(0.985 0.002 247.839);
|
||||
--color-gray-100: oklch(0.967 0.003 264.542);
|
||||
--color-gray-200: oklch(0.928 0.006 264.531);
|
||||
--color-gray-300: oklch(0.872 0.01 258.338);
|
||||
--color-gray-400: oklch(0.707 0.022 261.325);
|
||||
--color-gray-500: oklch(0.551 0.027 264.364);
|
||||
--color-gray-600: oklch(0.446 0.03 256.802);
|
||||
--color-gray-700: oklch(0.373 0.034 259.733);
|
||||
--color-gray-800: oklch(0.278 0.033 256.848);
|
||||
--color-gray-900: oklch(0.21 0.034 264.665);
|
||||
--color-gray-950: oklch(0.13 0.028 261.692);
|
||||
|
||||
/* Line heights */
|
||||
--line-height-11: 2.75rem;
|
||||
--line-height-12: 3rem;
|
||||
--line-height-13: 3.25rem;
|
||||
--line-height-14: 3.5rem;
|
||||
|
||||
/* Z-index values */
|
||||
--z-60: 60;
|
||||
--z-70: 70;
|
||||
--z-80: 80;
|
||||
}
|
||||
|
||||
/*
|
||||
The default border color has changed to `currentColor` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentColor);
|
||||
}
|
||||
|
||||
a,
|
||||
button {
|
||||
outline-color: var(--color-primary-500);
|
||||
}
|
||||
|
||||
a:focus-visible,
|
||||
button:focus-visible {
|
||||
outline: 2px solid;
|
||||
border-radius: var(--radius-sm);
|
||||
outline-color: var(--color-primary-500);
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
touch-action: manipulation;
|
||||
text-rendering: optimizelegibility;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 20px;
|
||||
|
||||
background-color: rgb(var(--lsd-theme-secondary));
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
type SiteConfig = {
|
||||
name: string
|
||||
title: string
|
||||
description: string
|
||||
url: string
|
||||
defaultLocale: string
|
||||
keywords: string[]
|
||||
}
|
||||
|
||||
const siteConfig: SiteConfig = {
|
||||
name: 'Logos',
|
||||
title: 'Logos Next Tailwind Template',
|
||||
description: 'Template for Next.js, Tailwind CSS, and Acid Info LSD',
|
||||
url: 'https://logos.co',
|
||||
keywords: ['Logos', 'Web3'],
|
||||
defaultLocale: 'en',
|
||||
}
|
||||
|
||||
export default siteConfig
|
||||
@@ -0,0 +1,3 @@
|
||||
import nextConfig from '../../packages/config/eslint/next.mjs'
|
||||
|
||||
export default nextConfig
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
function useWindowSize() {
|
||||
const [windowSize, setWindowSize] = useState({
|
||||
width: 0,
|
||||
height: 0,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
function handleResize() {
|
||||
setWindowSize({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
})
|
||||
}
|
||||
window.addEventListener('resize', handleResize)
|
||||
handleResize()
|
||||
return () => window.removeEventListener('resize', handleResize)
|
||||
}, [])
|
||||
|
||||
const { width, height } = windowSize
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
export default useWindowSize
|
||||
@@ -0,0 +1,7 @@
|
||||
import { createNavigation } from 'next-intl/navigation'
|
||||
import { routing } from './routing'
|
||||
|
||||
// Lightweight wrappers around Next.js' navigation
|
||||
// APIs that consider the routing configuration
|
||||
export const { Link, redirect, usePathname, useRouter, getPathname } =
|
||||
createNavigation(routing)
|
||||
@@ -0,0 +1,18 @@
|
||||
import { getRequestConfig } from 'next-intl/server'
|
||||
import { hasLocale } from 'next-intl'
|
||||
import { routing } from './routing'
|
||||
|
||||
// docs: https://next-intl.dev/docs/getting-started/app-router/with-i18n-routing#i18n-request
|
||||
export default getRequestConfig(async ({ requestLocale }) => {
|
||||
// Typically corresponds to the `[locale]` segment
|
||||
const requested = await requestLocale
|
||||
|
||||
const locale = hasLocale(routing.locales, requested)
|
||||
? requested
|
||||
: routing.defaultLocale
|
||||
|
||||
return {
|
||||
locale,
|
||||
messages: (await import(`../messages/${locale}.json`)).default,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineRouting } from 'next-intl/routing'
|
||||
|
||||
export const routing = defineRouting({
|
||||
// A list of all locales that are supported
|
||||
locales: ['en', 'fr', 'ko'],
|
||||
|
||||
// Used when no locale matches
|
||||
defaultLocale: 'en',
|
||||
localePrefix: 'as-needed',
|
||||
localeDetection: false,
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"locale": { "language": "Language", "en": "English", "fr": "Français", "ko": "한국어" },
|
||||
"common": {
|
||||
"nav": {
|
||||
"resources": "Resources",
|
||||
"contributors": "Contributors",
|
||||
"miilestones": "Milestones"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"title": "main"
|
||||
},
|
||||
"footer": {}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"locale": { "language": "Langue", "en": "English", "fr": "Français", "ko": "한국어" },
|
||||
"common": {
|
||||
"nav": {
|
||||
"resources": "Ressources",
|
||||
"contributors": "Contributeurs",
|
||||
"milestones": "Jalons"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"title": "main"
|
||||
},
|
||||
"footer": {}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"locale": { "language": "언어", "en": "English", "fr": "Français", "ko": "한국어" },
|
||||
"common": {
|
||||
"nav": {
|
||||
"resources": "리소스",
|
||||
"contributors": "기여자",
|
||||
"milestones": "마일스톤"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"title": "메인"
|
||||
},
|
||||
"footer": {}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import createNextIntlPlugin from 'next-intl/plugin'
|
||||
|
||||
const withNextIntl = createNextIntlPlugin()
|
||||
const workspaceRoot = fileURLToPath(new URL('../..', import.meta.url))
|
||||
|
||||
const securityHeaders = [
|
||||
{
|
||||
key: 'Referrer-Policy',
|
||||
value: 'strict-origin-when-cross-origin',
|
||||
},
|
||||
{
|
||||
key: 'X-Frame-Options',
|
||||
value: 'DENY',
|
||||
},
|
||||
{
|
||||
key: 'X-Content-Type-Options',
|
||||
value: 'nosniff',
|
||||
},
|
||||
{
|
||||
key: 'X-DNS-Prefetch-Control',
|
||||
value: 'on',
|
||||
},
|
||||
{
|
||||
key: 'Strict-Transport-Security',
|
||||
value: 'max-age=31536000; includeSubDomains',
|
||||
},
|
||||
{
|
||||
key: 'Permissions-Policy',
|
||||
value: 'camera=(), microphone=(), geolocation=()',
|
||||
},
|
||||
]
|
||||
|
||||
const nextConfig = {
|
||||
basePath: process.env.BASE_PATH || undefined,
|
||||
images: {
|
||||
unoptimized: Boolean(process.env.UNOPTIMIZED),
|
||||
},
|
||||
output: process.env.EXPORT ? 'export' : undefined,
|
||||
reactStrictMode: true,
|
||||
transpilePackages: ['@repo/ui'],
|
||||
trailingSlash: false,
|
||||
turbopack: {
|
||||
root: workspaceRoot,
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: '/(.*)',
|
||||
headers: securityHeaders,
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
export default withNextIntl(nextConfig)
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "web",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --port 3000",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint . --max-warnings 0",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"check-types": "next typegen && tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/ui": "workspace:*",
|
||||
"clsx": "^2.1.1",
|
||||
"next": "16.2.4",
|
||||
"next-intl": "^4.9.1",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.2.2",
|
||||
"@types/node": "25.6.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"eslint": "^10.2.1",
|
||||
"postcss": "^8.5.10",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import createMiddleware from 'next-intl/middleware'
|
||||
|
||||
import { routing } from './i18n/routing'
|
||||
|
||||
const intlMiddleware = createMiddleware(routing)
|
||||
|
||||
export function proxy(request: import('next/server').NextRequest) {
|
||||
return intlMiddleware(request)
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ['/((?!_next|api|.*\\..*).*)'],
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 170 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 450 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="93" height="126" viewBox="0 0 93 126" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M71.2154 126C66.7864 126 62.9754 124.958 59.7824 122.873C56.5894 120.789 54.1688 117.506 52.5208 113.025C51.4908 110.002 50.7698 106.303 50.3578 101.926C49.9458 97.5484 49.6883 92.9628 49.5853 88.1687C49.4823 83.2705 49.3793 78.5285 49.2763 73.9429C49.2763 72.6923 48.9673 72.067 48.3493 72.067C47.8343 72.067 47.3193 72.4839 46.8043 73.3176C44.8473 76.9653 42.5298 81.134 39.8518 85.8238C37.2768 90.5136 34.7018 95.2035 32.1268 99.8933C29.5517 104.583 27.2342 108.804 25.1742 112.556C23.2172 116.203 21.8267 118.861 21.0027 120.529C19.9727 122.821 18.3247 124.28 16.0587 124.906C13.8957 125.635 11.1662 125.792 7.87017 125.375C4.57415 124.958 2.25664 123.655 0.91764 121.466C-0.524366 119.174 -0.266865 116.777 1.69014 114.275C3.02915 112.608 4.93466 110.107 7.40666 106.772C9.87867 103.333 12.6597 99.4764 15.7497 95.2035C18.8397 90.8263 21.9812 86.3449 25.1742 81.7593C28.4702 77.0695 31.5603 72.5881 34.4443 68.3151C37.3283 63.938 39.7488 60.134 41.7058 56.9032C43.7658 53.5682 45.1048 51.1191 45.7228 49.5558C46.3408 48.0968 47.0103 46.4814 47.7313 44.7097C48.4523 42.938 48.8128 41.1141 48.8128 39.2382C48.8128 32.3598 48.1433 27.201 46.8043 23.7618C45.5683 20.2184 43.8688 17.8734 41.7058 16.727C39.6458 15.4764 37.3798 14.8511 34.9078 14.8511C33.0538 14.8511 31.0453 15.2159 28.8822 15.9454C26.8222 16.6749 25.4832 17.3524 24.8652 17.9777C23.7322 19.1241 22.6507 19.3325 21.6207 18.603C20.5907 17.8734 20.2817 16.6749 20.6937 15.0074C21.8267 11.1514 23.8867 7.71216 26.8737 4.68982C29.8608 1.56327 34.0323 0 39.3883 0C45.0533 0 49.5338 1.61539 52.8298 4.84616C56.1259 8.07692 58.4949 13.2357 59.9369 20.3226C61.3789 27.4094 62.0999 36.7891 62.0999 48.4615C62.0999 62.2184 62.3059 73.2134 62.7179 81.4466C63.1299 89.6799 63.7994 95.8809 64.7264 100.05C65.6534 104.114 66.9409 106.824 68.5889 108.179C70.3399 109.534 72.5029 110.211 75.0779 110.211C77.5499 110.211 79.8674 109.69 82.0305 108.648C84.2965 107.605 86.2535 106.251 87.9015 104.583C88.6225 103.749 89.4465 103.437 90.3735 103.645C91.3005 103.854 92.0215 104.427 92.5365 105.365C93.1545 106.199 93.1545 107.293 92.5365 108.648C90.3735 113.442 87.4895 117.558 83.8845 120.998C80.3824 124.333 76.1594 126 71.2154 126Z" fill="#000"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"extends": "../../packages/config/typescript/next.json",
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./*"],
|
||||
"@/components/*": ["./components/*"],
|
||||
"@/data/*": ["./data/*"],
|
||||
"@/css/*": ["./css/*"]
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"strictNullChecks": true
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.mjs",
|
||||
"**/*.json",
|
||||
"proxy.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import siteConfig from '@/data/siteConfig'
|
||||
import { Metadata } from 'next'
|
||||
|
||||
type DefaultMetadataProps = {
|
||||
locale: string
|
||||
title?: string
|
||||
description?: string
|
||||
path?: string
|
||||
}
|
||||
|
||||
const baseUrl = siteConfig.url
|
||||
|
||||
export function absoluteUrl(
|
||||
path: string,
|
||||
locale: string = siteConfig.defaultLocale
|
||||
) {
|
||||
return `${siteConfig.url}${locale === siteConfig.defaultLocale ? '' : `/${locale}`}${path}`
|
||||
}
|
||||
|
||||
export async function createDefaultMetadata({
|
||||
title = '',
|
||||
description = '',
|
||||
locale,
|
||||
path = '',
|
||||
}: DefaultMetadataProps): Promise<Metadata> {
|
||||
const _title = title || siteConfig.title
|
||||
const _description = description || siteConfig.description
|
||||
|
||||
const fullUrl = absoluteUrl(path)
|
||||
|
||||
return {
|
||||
title: _title,
|
||||
description: _description,
|
||||
metadataBase: new URL(baseUrl),
|
||||
alternates: {
|
||||
canonical: fullUrl,
|
||||
languages: {
|
||||
en: fullUrl,
|
||||
'x-default': fullUrl,
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title: _title,
|
||||
description: _description,
|
||||
url: fullUrl,
|
||||
type: 'website',
|
||||
locale,
|
||||
siteName: siteConfig.name,
|
||||
images: [
|
||||
{
|
||||
url: absoluteUrl('/og'),
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: title,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title,
|
||||
description,
|
||||
images: [absoluteUrl('/og')],
|
||||
},
|
||||
icons: '/favicon.ico',
|
||||
creator: siteConfig.name,
|
||||
keywords: siteConfig.keywords,
|
||||
robots: {
|
||||
index: process.env.NEXT_PUBLIC_API_MODE === 'production',
|
||||
follow: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export const themeInitScript = `
|
||||
(function() {
|
||||
try {
|
||||
var theme = localStorage.getItem('theme');
|
||||
var systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
var initialTheme = theme || systemTheme;
|
||||
document.documentElement.setAttribute('data-theme', initialTheme);
|
||||
} catch (e) {}
|
||||
})();
|
||||
`
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "logos-turborepo-next-tailwind-i18n-template",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev",
|
||||
"lint": "turbo run lint",
|
||||
"lint:fix": "turbo run lint:fix",
|
||||
"format": "prettier --write \"**/*.{js,mjs,ts,tsx,json,md,css}\"",
|
||||
"check-types": "turbo run check-types",
|
||||
"generate-types": "turbo run generate-types --filter=cms"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.8.3",
|
||||
"turbo": "^2.9.6",
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
"@swc/core",
|
||||
"esbuild",
|
||||
"sharp"
|
||||
]
|
||||
},
|
||||
"packageManager": "pnpm@10.9.0",
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import js from '@eslint/js'
|
||||
import prettier from 'eslint-config-prettier'
|
||||
import prettierPlugin from 'eslint-plugin-prettier'
|
||||
import globals from 'globals'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ['**/.next/**', '**/dist/**', '**/node_modules/**'],
|
||||
},
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
prettier,
|
||||
{
|
||||
files: ['**/*.{js,mjs,ts,tsx}'],
|
||||
plugins: {
|
||||
prettier: prettierPlugin,
|
||||
},
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
||||
'prettier/prettier': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.js', '**/*.mjs'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
import baseConfig from './base.mjs'
|
||||
|
||||
export default baseConfig
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@repo/config",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./eslint/base": "./eslint/base.mjs",
|
||||
"./eslint/next": "./eslint/next.mjs",
|
||||
"./prettier": "./prettier/index.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"globals": "^16.4.0",
|
||||
"typescript-eslint": "^8.58.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'es5',
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"declaration": false,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"incremental": true,
|
||||
"isolatedModules": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"strictNullChecks": true,
|
||||
"target": "ES2022"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "./base.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "es2022"],
|
||||
"jsx": "preserve"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "./base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["dom", "dom.iterable", "es2022"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "@repo/types",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./payload": "./src/payload.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint . --config ../../packages/config/eslint/base.mjs --max-warnings 0",
|
||||
"lint:fix": "eslint . --config ../../packages/config/eslint/base.mjs --fix",
|
||||
"check-types": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "25.6.0",
|
||||
"eslint": "^10.2.1",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './payload'
|
||||
@@ -0,0 +1,364 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Supported timezones in IANA format.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "supportedTimezones".
|
||||
*/
|
||||
export type SupportedTimezones =
|
||||
| 'Pacific/Midway'
|
||||
| 'Pacific/Niue'
|
||||
| 'Pacific/Honolulu'
|
||||
| 'Pacific/Rarotonga'
|
||||
| 'America/Anchorage'
|
||||
| 'Pacific/Gambier'
|
||||
| 'America/Los_Angeles'
|
||||
| 'America/Tijuana'
|
||||
| 'America/Denver'
|
||||
| 'America/Phoenix'
|
||||
| 'America/Chicago'
|
||||
| 'America/Guatemala'
|
||||
| 'America/New_York'
|
||||
| 'America/Bogota'
|
||||
| 'America/Caracas'
|
||||
| 'America/Santiago'
|
||||
| 'America/Buenos_Aires'
|
||||
| 'America/Sao_Paulo'
|
||||
| 'Atlantic/South_Georgia'
|
||||
| 'Atlantic/Azores'
|
||||
| 'Atlantic/Cape_Verde'
|
||||
| 'Europe/London'
|
||||
| 'Europe/Berlin'
|
||||
| 'Africa/Lagos'
|
||||
| 'Europe/Athens'
|
||||
| 'Africa/Cairo'
|
||||
| 'Europe/Moscow'
|
||||
| 'Asia/Riyadh'
|
||||
| 'Asia/Dubai'
|
||||
| 'Asia/Baku'
|
||||
| 'Asia/Karachi'
|
||||
| 'Asia/Tashkent'
|
||||
| 'Asia/Calcutta'
|
||||
| 'Asia/Dhaka'
|
||||
| 'Asia/Almaty'
|
||||
| 'Asia/Jakarta'
|
||||
| 'Asia/Bangkok'
|
||||
| 'Asia/Shanghai'
|
||||
| 'Asia/Singapore'
|
||||
| 'Asia/Tokyo'
|
||||
| 'Asia/Seoul'
|
||||
| 'Australia/Brisbane'
|
||||
| 'Australia/Sydney'
|
||||
| 'Pacific/Guam'
|
||||
| 'Pacific/Noumea'
|
||||
| 'Pacific/Auckland'
|
||||
| 'Pacific/Fiji';
|
||||
|
||||
export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
blocks: {};
|
||||
collections: {
|
||||
users: User;
|
||||
pages: Page;
|
||||
'payload-kv': PayloadKv;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
collectionsJoins: {};
|
||||
collectionsSelect: {
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
pages: PagesSelect<false> | PagesSelect<true>;
|
||||
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: number;
|
||||
};
|
||||
fallbackLocale: null;
|
||||
globals: {
|
||||
siteSettings: SiteSetting;
|
||||
};
|
||||
globalsSelect: {
|
||||
siteSettings: SiteSettingsSelect<false> | SiteSettingsSelect<true>;
|
||||
};
|
||||
locale: null;
|
||||
widgets: {
|
||||
collections: CollectionsWidget;
|
||||
};
|
||||
user: User;
|
||||
jobs: {
|
||||
tasks: unknown;
|
||||
workflows: unknown;
|
||||
};
|
||||
}
|
||||
export interface UserAuthOperations {
|
||||
forgotPassword: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
login: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
registerFirstUser: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
unlock: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: number;
|
||||
name?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
resetPasswordToken?: string | null;
|
||||
resetPasswordExpiration?: string | null;
|
||||
salt?: string | null;
|
||||
hash?: string | null;
|
||||
loginAttempts?: number | null;
|
||||
lockUntil?: string | null;
|
||||
sessions?:
|
||||
| {
|
||||
id: string;
|
||||
createdAt?: string | null;
|
||||
expiresAt: string;
|
||||
}[]
|
||||
| null;
|
||||
password?: string | null;
|
||||
collection: 'users';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "pages".
|
||||
*/
|
||||
export interface Page {
|
||||
id: number;
|
||||
title: string;
|
||||
slug: string;
|
||||
content?: {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
type: any;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ('ltr' | 'rtl') | null;
|
||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||
indent: number;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-kv".
|
||||
*/
|
||||
export interface PayloadKv {
|
||||
id: number;
|
||||
key: string;
|
||||
data:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: number;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: number | User;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'pages';
|
||||
value: number | Page;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: number | User;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: number;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: number | User;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: number;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users_select".
|
||||
*/
|
||||
export interface UsersSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
email?: T;
|
||||
resetPasswordToken?: T;
|
||||
resetPasswordExpiration?: T;
|
||||
salt?: T;
|
||||
hash?: T;
|
||||
loginAttempts?: T;
|
||||
lockUntil?: T;
|
||||
sessions?:
|
||||
| T
|
||||
| {
|
||||
id?: T;
|
||||
createdAt?: T;
|
||||
expiresAt?: T;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "pages_select".
|
||||
*/
|
||||
export interface PagesSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
slug?: T;
|
||||
content?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
_status?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-kv_select".
|
||||
*/
|
||||
export interface PayloadKvSelect<T extends boolean = true> {
|
||||
key?: T;
|
||||
data?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents_select".
|
||||
*/
|
||||
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
|
||||
document?: T;
|
||||
globalSlug?: T;
|
||||
user?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences_select".
|
||||
*/
|
||||
export interface PayloadPreferencesSelect<T extends boolean = true> {
|
||||
user?: T;
|
||||
key?: T;
|
||||
value?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations_select".
|
||||
*/
|
||||
export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
batch?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "siteSettings".
|
||||
*/
|
||||
export interface SiteSetting {
|
||||
id: number;
|
||||
siteName: string;
|
||||
siteDescription?: string | null;
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "siteSettings_select".
|
||||
*/
|
||||
export interface SiteSettingsSelect<T extends boolean = true> {
|
||||
siteName?: T;
|
||||
siteDescription?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "collections_widget".
|
||||
*/
|
||||
export interface CollectionsWidget {
|
||||
data?: {
|
||||
[k: string]: unknown;
|
||||
};
|
||||
width: 'full';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
*/
|
||||
export interface Auth {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../config/typescript/react-library.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@repo/ui",
|
||||
"private": true,
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint . --config ../../packages/config/eslint/base.mjs --max-warnings 0",
|
||||
"lint:fix": "eslint . --config ../../packages/config/eslint/base.mjs --fix",
|
||||
"check-types": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { ButtonHTMLAttributes, PropsWithChildren } from 'react'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
type ButtonProps = PropsWithChildren<
|
||||
ButtonHTMLAttributes<HTMLButtonElement>
|
||||
> & {
|
||||
variant?: 'primary' | 'secondary'
|
||||
}
|
||||
|
||||
export function Button({
|
||||
children,
|
||||
className,
|
||||
type = 'button',
|
||||
variant = 'primary',
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
className={clsx(
|
||||
'inline-flex items-center justify-center rounded-full px-5 py-2 text-sm font-medium transition',
|
||||
variant === 'primary' &&
|
||||
'bg-black text-white hover:bg-neutral-800 dark:bg-white dark:text-black dark:hover:bg-neutral-200',
|
||||
variant === 'secondary' &&
|
||||
'border border-black/10 bg-white text-black hover:border-black/20 hover:bg-neutral-50 dark:border-white/20 dark:bg-transparent dark:text-white dark:hover:border-white/40 dark:hover:bg-white/5',
|
||||
className
|
||||
)}
|
||||
type={type}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { PropsWithChildren } from 'react'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
type ContainerProps = PropsWithChildren<{
|
||||
className?: string
|
||||
}>
|
||||
|
||||
export function Container({ children, className }: ContainerProps) {
|
||||
return (
|
||||
<div className={clsx('mx-auto w-full max-w-6xl px-6', className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './button'
|
||||
export * from './container'
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../config/typescript/react-library.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"strictNullChecks": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Generated
+6231
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
- "apps/*"
|
||||
- "packages/*"
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './packages/config/prettier/index.mjs'
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"$schema": "https://turborepo.dev/schema.json",
|
||||
"ui": "tui",
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"inputs": ["$TURBO_DEFAULT$", ".env*"],
|
||||
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
|
||||
},
|
||||
"lint": {
|
||||
"dependsOn": ["^lint"]
|
||||
},
|
||||
"lint:fix": {
|
||||
"cache": false
|
||||
},
|
||||
"check-types": {
|
||||
"dependsOn": ["^check-types"]
|
||||
},
|
||||
"generate-types": {
|
||||
"cache": false,
|
||||
"outputs": ["packages/types/src/payload.ts"]
|
||||
},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user