diff --git a/examples/blog/README.md b/examples/blog/README.md new file mode 100644 index 00000000..e215bc4c --- /dev/null +++ b/examples/blog/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/examples/blog/next.config.ts b/examples/blog/next.config.ts new file mode 100644 index 00000000..73290639 --- /dev/null +++ b/examples/blog/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from 'next' + +const nextConfig: NextConfig = { + /* config options here */ +} + +export default nextConfig diff --git a/examples/blog/package.json b/examples/blog/package.json new file mode 100644 index 00000000..a302ff33 --- /dev/null +++ b/examples/blog/package.json @@ -0,0 +1,28 @@ +{ + "name": "@deepdish/blog", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "@deepdish/cms": "workspace:0.8.0", + "@deepdish/nextjs": "workspace:0.8.0", + "@deepdish/resolvers": "workspace:0.13.0", + "@deepdish/ui": "workspace:0.15.0", + "next": "15.1.4", + "react": "catalog:react", + "react-dom": "catalog:react", + "valibot": "1.0.0-rc.0" + }, + "devDependencies": { + "@tailwindcss/postcss": "catalog:tailwind", + "@types/node": "catalog:repo", + "@types/react": "catalog:react", + "@types/react-dom": "catalog:react", + "tailwindcss": "catalog:tailwind", + "typescript": "catalog:repo" + } +} diff --git a/examples/blog/postcss.config.mjs b/examples/blog/postcss.config.mjs new file mode 100644 index 00000000..86e8e3c4 --- /dev/null +++ b/examples/blog/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ['@tailwindcss/postcss'], +} + +export default config diff --git a/examples/blog/src/app/blog/[slug]/page.tsx b/examples/blog/src/app/blog/[slug]/page.tsx new file mode 100644 index 00000000..551abdc7 --- /dev/null +++ b/examples/blog/src/app/blog/[slug]/page.tsx @@ -0,0 +1,17 @@ +import { BlogPost } from '@/cms' + +export default async function BlogArticle({ + params, +}: { + params: Promise<{ slug: string }> +}) { + const { slug } = await params + + return ( +
+
+ +
+
+ ) +} diff --git a/examples/blog/src/app/blog/page.tsx b/examples/blog/src/app/blog/page.tsx new file mode 100644 index 00000000..4da16ce0 --- /dev/null +++ b/examples/blog/src/app/blog/page.tsx @@ -0,0 +1,17 @@ +import { BlogCard } from '@/cms' + +export default function Blog() { + return ( +
+
+

Johnny Pizza Blog

+

+ Check out our cool blog articles and learn more about our pizza! +

+
+ +
+
+
+ ) +} diff --git a/examples/blog/src/app/favicon.ico b/examples/blog/src/app/favicon.ico new file mode 100644 index 00000000..718d6fea Binary files /dev/null and b/examples/blog/src/app/favicon.ico differ diff --git a/examples/blog/src/app/globals.css b/examples/blog/src/app/globals.css new file mode 100644 index 00000000..2a89a481 --- /dev/null +++ b/examples/blog/src/app/globals.css @@ -0,0 +1,19 @@ +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); +} + +body { + background: var(--background); + color: var(--foreground); + font-family: Arial, Helvetica, sans-serif; +} diff --git a/examples/blog/src/app/layout.tsx b/examples/blog/src/app/layout.tsx new file mode 100644 index 00000000..b86befea --- /dev/null +++ b/examples/blog/src/app/layout.tsx @@ -0,0 +1,40 @@ +import type { Metadata } from 'next' +import { Geist, Geist_Mono } from 'next/font/google' +import './globals.css' +import { Navbar } from '@/components/navbar' + +import { DeepDishProvider } from '@/cms' + +const geistSans = Geist({ + variable: '--font-geist-sans', + subsets: ['latin'], +}) + +const geistMono = Geist_Mono({ + variable: '--font-geist-mono', + subsets: ['latin'], +}) + +export const metadata: Metadata = { + title: 'DeepDish Blog Example', + description: 'Blog example showcasing the DeepDish CMS', +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + + +
{children}
+
+ + + ) +} diff --git a/examples/blog/src/app/page.tsx b/examples/blog/src/app/page.tsx new file mode 100644 index 00000000..9438f8bd --- /dev/null +++ b/examples/blog/src/app/page.tsx @@ -0,0 +1,25 @@ +import { Paragraph } from '@/cms' +import Link from 'next/link' + +export default function Home() { + return ( +
+
+ + Welcome to Johnny Pizza + + + We pride ourselves on our pizza. We use only the freshest ingredients + and our team of expert chefs to create the best pizza in town. + + Check out our blog! +
+
+ ) +} diff --git a/examples/blog/src/cms.tsx b/examples/blog/src/cms.tsx new file mode 100644 index 00000000..ed2cbcde --- /dev/null +++ b/examples/blog/src/cms.tsx @@ -0,0 +1,181 @@ +import fs from 'node:fs/promises' +import { truncate } from '@/utils' +import { DeepDishProvider as InternalDeepDishProvider } from '@deepdish/cms' +import { getBaseUrl } from '@deepdish/cms/vercel' +import { createJsonResolver } from '@deepdish/resolvers/json' +import { createComponents } from '@deepdish/ui/components' +import { configure } from '@deepdish/ui/config' +import type { IntrinsicElement, SetChildren } from '@deepdish/ui/deepdish' +import Link from 'next/link' +import * as v from 'valibot' + +// TODO: get local data dir to work +export const contentPaths = { + blog: '/tmp/deepdish_blog_blog.json', + text: '/tmp/deepdish_blog_text.json', +} as const + +async function initContent() { + for (const path of Object.values(contentPaths)) { + const exists = await fs + .stat(path) + .then(() => true) + .catch(() => false) + + if (!exists) { + await fs.writeFile(path, JSON.stringify({})) + } + } +} + +const blogSchema = v.object({ + title: v.string(), + author: v.object({ + name: v.string(), + email: v.string(), + }), + body: v.string(), + slug: v.string(), +}) + +const textSchema = v.string() + +const contracts = { + blog: { + resolver: createJsonResolver(contentPaths.blog, blogSchema, { + maxBatchSize: 10, + }), + schema: blogSchema, + }, + text: { + resolver: createJsonResolver(contentPaths.text, textSchema, { + maxBatchSize: 10, + }), + schema: textSchema, + }, +} + +async function cms() { + await initContent() + + await configure({ + contracts, + logging: { + enabled: process.env.NODE_ENV === 'development', + }, + settings: { + baseUrl: getBaseUrl({}), + draft: true, + }, + }) + + return createComponents(contracts) +} + +const components = await cms() + +const Blog = components.blog +const Text = components.text + +type DeepDishWithoutContract = + | { + key: string + collection?: never + } + | { + collection: string + } + +type ElementProps = SetChildren< + JSX.IntrinsicElements[E], + C +> & { + deepdish: DeepDishWithoutContract +} + +type ParagraphProps = ElementProps + +export function Paragraph(props: ParagraphProps<'p'>) { + return ( + { + return

{content}

+ }} + /> + ) +} + +export function BlogCard(props: { + deepdish: DeepDishWithoutContract +}) { + return ( + { + if (!content) { + return
Blog Card Placeholder
+ } + + return ( +
+

{content.title}

+

+ Written by {content.author.name} +

+

{truncate(content.body, 100)}

+ + Read article + +
+ ) + }} + /> + ) +} + +export function BlogPost(props: { + deepdish: DeepDishWithoutContract +}) { + return ( + { + if (!content) { + return
Blog Article Placeholder
+ } + + return ( +
+

+ {content.title} +

+

+ Written by {content.author.name} +

+

{content.body}

+
+ ) + }} + /> + ) +} + +export function DeepDishProvider(props: { + children: React.ReactNode +}) { + return +} diff --git a/examples/blog/src/components/navbar.tsx b/examples/blog/src/components/navbar.tsx new file mode 100644 index 00000000..907e958d --- /dev/null +++ b/examples/blog/src/components/navbar.tsx @@ -0,0 +1,17 @@ +import Link from 'next/link' + +export function Navbar() { + return ( +
+
+
+ Johnny Pizza +
+
+ Home + Blog +
+
+
+ ) +} diff --git a/examples/blog/src/middleware.ts b/examples/blog/src/middleware.ts new file mode 100644 index 00000000..148a1317 --- /dev/null +++ b/examples/blog/src/middleware.ts @@ -0,0 +1,36 @@ +import { getBaseUrl } from '@deepdish/cms/vercel' +import { deepdishMiddleware } from '@deepdish/nextjs' +import { type NextRequest, NextResponse } from 'next/server' + +let signedIn = false + +async function signIn() { + await new Promise((resolve) => setTimeout(resolve, 1000)) + signedIn = true + + return NextResponse.redirect(getBaseUrl({})) +} + +async function signOut() { + await new Promise((resolve) => setTimeout(resolve, 1000)) + signedIn = false + + return NextResponse.redirect(getBaseUrl({})) +} + +async function verify() { + await new Promise((resolve) => setTimeout(resolve, 1000)) + return signedIn +} + +export default (request: NextRequest) => { + return deepdishMiddleware( + { + draft: true, + verify, + signIn, + signOut, + }, + request, + ) +} diff --git a/examples/blog/src/utils.ts b/examples/blog/src/utils.ts new file mode 100644 index 00000000..ea474def --- /dev/null +++ b/examples/blog/src/utils.ts @@ -0,0 +1,7 @@ +export function truncate(str: string, maxLength: number) { + if (str.length <= maxLength) { + return str + } + + return `${str.slice(0, maxLength)}...` +} diff --git a/examples/blog/tsconfig.json b/examples/blog/tsconfig.json new file mode 100644 index 00000000..c1334095 --- /dev/null +++ b/examples/blog/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/package.json b/package.json index cef81f26..1fa39966 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "private": true, "workspaces": [ "apps/*", + "examples/*", "packages/*" ], "scripts": { diff --git a/packages/ui/src/deepdish.tsx b/packages/ui/src/deepdish.tsx index 6b39c0b9..c082ad6c 100644 --- a/packages/ui/src/deepdish.tsx +++ b/packages/ui/src/deepdish.tsx @@ -1,3 +1,4 @@ +/// import 'server-only' import { withResult } from '@byteslice/result' @@ -10,6 +11,9 @@ import type { DeepDishCollectionProps, DeepDishElementProps, DeepDishProps, + ElementProps, + IntrinsicElement, + SetChildren, } from './types' const logger = getLogger(['deepdish', 'ui']) @@ -250,4 +254,4 @@ export async function DeepDish(props: { ) } -export type { DeepDishProps } +export type { DeepDishProps, ElementProps, IntrinsicElement, SetChildren } diff --git a/packages/ui/src/types.ts b/packages/ui/src/types.ts index 0c217760..d096c2d8 100644 --- a/packages/ui/src/types.ts +++ b/packages/ui/src/types.ts @@ -28,4 +28,4 @@ export type ElementProps< export type IntrinsicElement = keyof JSX.IntrinsicElements -type SetChildren = Omit & { children?: C } +export type SetChildren = Omit & { children?: C } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64514145..ff619b41 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,12 @@ catalogs: specifier: ^19.0.0 || ^19.0.0-0 version: 19.0.0 react: + '@types/react': + specifier: 19.0.0 + version: 19.0.0 + '@types/react-dom': + specifier: 19.0.0 + version: 19.0.0 react: specifier: 19.0.0 version: 19.0.0 @@ -248,6 +254,52 @@ importers: specifier: 5.5.4 version: 5.5.4 + examples/blog: + dependencies: + '@deepdish/cms': + specifier: workspace:0.8.0 + version: link:../../packages/cms + '@deepdish/nextjs': + specifier: workspace:0.8.0 + version: link:../../packages/nextjs + '@deepdish/resolvers': + specifier: workspace:0.13.0 + version: link:../../packages/resolvers + '@deepdish/ui': + specifier: workspace:0.15.0 + version: link:../../packages/ui + next: + specifier: 15.1.4 + version: 15.1.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + react: + specifier: catalog:react + version: 19.0.0 + react-dom: + specifier: catalog:react + version: 19.0.0(react@19.0.0) + valibot: + specifier: 1.0.0-rc.0 + version: 1.0.0-rc.0(typescript@5.7.2) + devDependencies: + '@tailwindcss/postcss': + specifier: catalog:tailwind + version: 4.0.3 + '@types/node': + specifier: catalog:repo + version: 20.9.0 + '@types/react': + specifier: catalog:react + version: 19.0.0 + '@types/react-dom': + specifier: catalog:react + version: 19.0.0 + tailwindcss: + specifier: catalog:tailwind + version: 4.0.3 + typescript: + specifier: catalog:repo + version: 5.7.2 + packages/cms: dependencies: '@deepdish-cloud/resolvers': @@ -3302,6 +3354,9 @@ packages: '@types/react-dom@18.3.0': resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} + '@types/react-dom@19.0.0': + resolution: {integrity: sha512-1KfiQKsH1o00p9m5ag12axHQSb3FOU9H20UTrujVSkNhuCrRHiQWFqgEnTNK5ZNfnzZv8UWrnXVqCmCF9fgY3w==} + '@types/react-dom@19.0.4': resolution: {integrity: sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==} peerDependencies: @@ -3313,6 +3368,9 @@ packages: '@types/react@18.3.3': resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==} + '@types/react@19.0.0': + resolution: {integrity: sha512-MY3oPudxvMYyesqs/kW1Bh8y9VqSmf+tzqw3ae8a9DZW68pUe3zAdHeI1jc6iAysuRdACnVknHP8AhwD4/dxtg==} + '@types/statuses@2.0.5': resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==} @@ -3445,7 +3503,6 @@ packages: bun@1.2.4: resolution: {integrity: sha512-ZY0EZ/UKqheaLeAtMsfJA6jWoWvV9HAtfFaOJSmS3LrNpFKs1Sg5fZLSsczN1h3a+Dtheo4O3p3ZYWrf40kRGw==} - cpu: [arm64, x64, aarch64] os: [darwin, linux, win32] hasBin: true @@ -8087,6 +8144,10 @@ snapshots: dependencies: '@types/react': 18.3.3 + '@types/react-dom@19.0.0': + dependencies: + '@types/react': 18.3.3 + '@types/react-dom@19.0.4(@types/react@18.3.3)': dependencies: '@types/react': 18.3.3 @@ -8101,6 +8162,10 @@ snapshots: '@types/prop-types': 15.7.14 csstype: 3.1.3 + '@types/react@19.0.0': + dependencies: + csstype: 3.1.3 + '@types/statuses@2.0.5': {} '@types/tough-cookie@4.0.5': {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9979b851..5ff84a27 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,7 @@ packages: - - "packages/*" - "apps/*" + - "examples/*" + - "packages/*" catalogs: react: