-
Notifications
You must be signed in to change notification settings - Fork 378
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(recipes): add react-router v7 example
- Loading branch information
1 parent
f323610
commit 3f01c70
Showing
20 changed files
with
964 additions
and
49 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,6 +35,6 @@ | |
], | ||
"version": "0.17.1", | ||
"engines": { | ||
"node": ">=18.18" | ||
"node": ">=20.0.0" | ||
} | ||
} |
38 changes: 38 additions & 0 deletions
38
packages/create-puck-app/templates/react-router/package.json.hbs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
{ | ||
"name": "{{appName}}", | ||
"version": "1.0.0", | ||
"private": true, | ||
"sideEffects": false, | ||
"type": "module", | ||
"scripts": { | ||
"build": "cross-env NODE_ENV=production react-router build", | ||
"dev": "react-router dev", | ||
"start": "cross-env NODE_ENV=production react-router-serve ./build/server/index.js", | ||
"typecheck": "react-router typegen && tsc" | ||
}, | ||
"dependencies": { | ||
"@measured/puck": "{{puckVersion}}", | ||
"@react-router/node": "^7.0.2", | ||
"@react-router/serve": "^7.0.2", | ||
"isbot": "^5.1.17", | ||
"react": "^19.0.0", | ||
"react-dom": "^19.0.0", | ||
"react-router": "^7.0.2" | ||
}, | ||
"devDependencies": { | ||
"@react-router/dev": "^7.0.2", | ||
"@types/node": "^20", | ||
"@types/react": "^19.0.1", | ||
"@types/react-dom": "^19.0.1", | ||
"autoprefixer": "^10.4.20", | ||
"cross-env": "^7.0.3", | ||
"postcss": "^8.4.49", | ||
"tailwindcss": "^3.4.16", | ||
"typescript": "^5.7.2", | ||
"vite": "^5.4.11", | ||
"vite-tsconfig-paths": "^5.1.4" | ||
} | ||
"engines": { | ||
"node": ">=22.0.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
.DS_Store | ||
/node_modules/ | ||
|
||
# React Router | ||
/.react-router/ | ||
/build/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
# `react-router` recipe | ||
|
||
The `react-router` recipe showcases one of the most powerful ways to implement Puck using to provide an authoring tool for any route in your React Router app. | ||
|
||
## Demonstrates | ||
|
||
- React Router V7 (framework) implementation | ||
- JSON database implementation | ||
- Splat route to use puck for any route on the platform | ||
|
||
## Usage | ||
|
||
Run the generator and enter `react-router` when prompted | ||
|
||
``` | ||
npx create-puck-app my-app | ||
``` | ||
|
||
Start the server | ||
|
||
``` | ||
yarn dev | ||
``` | ||
|
||
Navigate to the homepage at http://localhost:5173/. To edit the homepage, access the Puck editor at http://localhost:5173/edit. | ||
|
||
You can do this for any **base** route on the application, **even if the page doesn't exist**. For example, visit http://localhost:5173/hello-world and you'll receive a 404. You can author and publish a page by visiting http://localhost:5173/hello-world/edit. After publishing, go back to the original URL to see your page. | ||
|
||
## Using this recipe | ||
|
||
To adopt this recipe you will need to: | ||
|
||
- **IMPORTANT** Add authentication to `/edit` routes. This can be done by modifying the [route module action](https://reactrouter.com/start/framework/route-module#action) in the splat route `/app/routes/puck-splat.tsx`. **If you don't do this, Puck will be completely public.** | ||
- Integrate your database into the functions in `/lib/pages.server.ts` | ||
- Implement a custom puck configuration in `/app/puck.config.tsx` | ||
|
||
## License | ||
|
||
MIT © [Measured Co.](https://github.com/measuredco) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
@tailwind base; | ||
@tailwind components; | ||
@tailwind utilities; | ||
|
||
html, | ||
body { | ||
@apply bg-white dark:bg-gray-950; | ||
|
||
@media (prefers-color-scheme: dark) { | ||
color-scheme: dark; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import path from "path"; | ||
import { fileURLToPath } from "url"; | ||
import fs from "fs/promises"; | ||
import type { Data } from "@measured/puck"; | ||
|
||
const __filename = fileURLToPath(import.meta.url); | ||
const __dirname = path.dirname(__filename); | ||
const databasePath = path.join(__dirname, "..", "..", "database.json"); | ||
|
||
export async function getPage(path: string) { | ||
const pages = await readDatabase(); | ||
return pages[path]; | ||
} | ||
|
||
export async function savePage(path: string, data: Data) { | ||
const pages = await readDatabase(); | ||
pages[path] = data; | ||
await fs.writeFile(databasePath, JSON.stringify(pages), { encoding: "utf8" }); | ||
} | ||
|
||
async function readDatabase() { | ||
try { | ||
const file = await fs.readFile(databasePath, "utf8"); | ||
return JSON.parse(file) as Record<string, Data>; | ||
} catch (error: unknown) { | ||
console.error(error); | ||
return {}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
export function resolvePuckPath(path: string = "") { | ||
const url = new URL(path, "https://placeholder.com/"); | ||
const segments = url.pathname.split("/"); | ||
const isEditorRoute = segments.at(-1) === "edit"; | ||
const pathname = isEditorRoute | ||
? segments.slice(0, -1).join("/") | ||
: url.pathname; | ||
|
||
return { | ||
isEditorRoute, | ||
path: new URL(pathname, "https://placeholder.com/").pathname, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import type { Config } from "@measured/puck"; | ||
|
||
type Props = { | ||
HeadingBlock: { title: string }; | ||
}; | ||
|
||
export const config: Config<Props> = { | ||
components: { | ||
HeadingBlock: { | ||
fields: { | ||
title: { type: "text" }, | ||
}, | ||
defaultProps: { | ||
title: "Heading", | ||
}, | ||
render: ({ title }) => ( | ||
<div style={{ padding: 64 }}> | ||
<h1>{title}</h1> | ||
</div> | ||
), | ||
}, | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import { | ||
isRouteErrorResponse, | ||
Links, | ||
Meta, | ||
Outlet, | ||
Scripts, | ||
ScrollRestoration, | ||
} from "react-router"; | ||
|
||
import type { Route } from "./+types/root"; | ||
import stylesheet from "./app.css?url"; | ||
|
||
export const links: Route.LinksFunction = () => [ | ||
{ rel: "preconnect", href: "https://fonts.googleapis.com" }, | ||
{ | ||
rel: "preconnect", | ||
href: "https://fonts.gstatic.com", | ||
crossOrigin: "anonymous", | ||
}, | ||
{ | ||
rel: "stylesheet", | ||
href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", | ||
}, | ||
{ rel: "stylesheet", href: stylesheet }, | ||
]; | ||
|
||
export function Layout({ children }: { children: React.ReactNode }) { | ||
return ( | ||
<html lang="en"> | ||
<head> | ||
<meta charSet="utf-8" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
<Meta /> | ||
<Links /> | ||
</head> | ||
<body> | ||
{children} | ||
<ScrollRestoration /> | ||
<Scripts /> | ||
</body> | ||
</html> | ||
); | ||
} | ||
|
||
export default function App() { | ||
return <Outlet />; | ||
} | ||
|
||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { | ||
let message = "Oops!"; | ||
let details = "An unexpected error occurred."; | ||
let stack: string | undefined; | ||
|
||
if (isRouteErrorResponse(error)) { | ||
message = error.status === 404 ? "404" : "Error"; | ||
details = | ||
error.status === 404 | ||
? "The requested page could not be found." | ||
: error.statusText || details; | ||
} else if (import.meta.env.DEV && error && error instanceof Error) { | ||
details = error.message; | ||
stack = error.stack; | ||
} | ||
|
||
return ( | ||
<main className="pt-16 p-4 container mx-auto"> | ||
<h1>{message}</h1> | ||
<p>{details}</p> | ||
{stack && ( | ||
<pre className="w-full p-4 overflow-x-auto"> | ||
<code>{stack}</code> | ||
</pre> | ||
)} | ||
</main> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { type RouteConfig, route } from "@react-router/dev/routes"; | ||
|
||
export default [route("*", "routes/puck-splat.tsx")] satisfies RouteConfig; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { useFetcher, useLoaderData } from "react-router"; | ||
import type { Data } from "@measured/puck"; | ||
import { Puck, Render } from "@measured/puck"; | ||
|
||
import "@measured/puck/puck.css"; | ||
|
||
import type { Route } from "./+types/puck-splat"; | ||
import { config } from "~/puck.config"; | ||
import { resolvePuckPath } from "~/lib/resolve-puck-path.server"; | ||
import { getPage, savePage } from "~/lib/pages.server"; | ||
|
||
export async function loader({ params }: Route.LoaderArgs) { | ||
const pathname = params["*"]; | ||
const { isEditorRoute, path } = resolvePuckPath(pathname); | ||
let page = await getPage(path); | ||
|
||
// Throw a 404 if we're not rendering the editor and data for the page does not exist | ||
if (!isEditorRoute && !page) { | ||
throw new Response("Not Found", { status: 404 }); | ||
} | ||
|
||
// Empty shell for new pages | ||
if (isEditorRoute && !page) { | ||
page = { | ||
content: [], | ||
root: { | ||
props: { | ||
title: "", | ||
}, | ||
}, | ||
}; | ||
} | ||
|
||
return { | ||
isEditorRoute, | ||
path, | ||
data: page, | ||
}; | ||
} | ||
|
||
export function meta({ data: loaderData }: Route.MetaArgs) { | ||
return [ | ||
{ | ||
title: loaderData.isEditorRoute | ||
? `Edit: ${loaderData.path}` | ||
: loaderData.data.root.title, | ||
}, | ||
]; | ||
} | ||
|
||
export default function PuckSplatRoute({ loaderData }: Route.ComponentProps) { | ||
return ( | ||
<div> | ||
{loaderData.isEditorRoute ? ( | ||
<Editor /> | ||
) : ( | ||
<Render config={config} data={loaderData.data} /> | ||
)} | ||
</div> | ||
); | ||
} | ||
|
||
export async function action({ params, request }: Route.ActionArgs) { | ||
const pathname = params["*"]; | ||
const { path } = resolvePuckPath(pathname); | ||
const body = (await request.json()) as { data: Data }; | ||
|
||
await savePage(path, body.data); | ||
} | ||
|
||
function Editor() { | ||
const loaderData = useLoaderData<typeof loader>(); | ||
const fetcher = useFetcher<typeof action>(); | ||
|
||
return ( | ||
<Puck | ||
config={config} | ||
data={loaderData.data} | ||
onPublish={async (data) => { | ||
await fetcher.submit( | ||
{ | ||
data, | ||
}, | ||
{ | ||
action: "", | ||
method: "post", | ||
encType: "application/json", | ||
}, | ||
); | ||
}} | ||
/> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{"/":{"content":[{"type":"HeadingBlock","props":{"title":"Edit this page by adding /edit to the end of the URL","id":"HeadingBlock-1694032984497"}}],"root":{"props":{"title":"Puck + React Router 7 demo"}},"zones":{}}} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
{ | ||
"name": "react-router-recipe", | ||
"private": true, | ||
"type": "module", | ||
"version": "1.0.0", | ||
"scripts": { | ||
"build": "cross-env NODE_ENV=production react-router build", | ||
"dev": "react-router dev", | ||
"start": "cross-env NODE_ENV=production react-router-serve ./build/server/index.js", | ||
"typecheck": "react-router typegen && tsc" | ||
}, | ||
"dependencies": { | ||
"@measured/puck": "^0.17.1", | ||
"@react-router/node": "^7.0.2", | ||
"@react-router/serve": "^7.0.2", | ||
"isbot": "^5.1.17", | ||
"react": "^19.0.0", | ||
"react-dom": "^19.0.0", | ||
"react-router": "^7.0.2" | ||
}, | ||
"devDependencies": { | ||
"@react-router/dev": "^7.0.2", | ||
"@types/node": "^20", | ||
"@types/react": "^19.0.1", | ||
"@types/react-dom": "^19.0.1", | ||
"autoprefixer": "^10.4.20", | ||
"cross-env": "^7.0.3", | ||
"postcss": "^8.4.49", | ||
"tailwindcss": "^3.4.16", | ||
"typescript": "^5.7.2", | ||
"vite": "^5.4.11", | ||
"vite-tsconfig-paths": "^5.1.4" | ||
} | ||
} |
Binary file not shown.
Oops, something went wrong.