Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RSC: Add blog pages to kitchen sink test project #11420

Merged
merged 4 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
Date: 2016-10-14
Author: Monica T. Hoke
---

# Butterflies

Butterflies are beautiful creatures. They are a symbol of transformation and
change. They are also a symbol of hope and life. Butterflies are a reminder that
life is short and we should make the most of it. They are a reminder that we
should embrace change and not be afraid of it. Butterflies are a reminder that
we should be grateful for the time we have and make the most of it.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
Date: 2016-09-01
Author: Jared Dunbar
---

# Hello World

This is the first blog post using our new RSC based blog. Each post is a
mardown file in the `posts` directory. The file name is the URL slug for the
post. The file name is also the title of the post.
6 changes: 6 additions & 0 deletions __fixtures__/test-project-rsc-kitchen-sink/web/src/Routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Set, PrivateSet } from '@redwoodjs/router/Set'

import { useAuth } from './auth'
import AuthLayout from './layouts/AuthLayout/AuthLayout'
import BlogLayout from './layouts/BlogLayout/BlogLayout'
import NavigationLayout from './layouts/NavigationLayout/NavigationLayout'
import ScaffoldLayout from './layouts/ScaffoldLayout/ScaffoldLayout'

Expand All @@ -24,6 +25,11 @@ const Routes = () => {
<Route path="/about" page={AboutPage} name="about" />
<Route path="/multi-cell" page={MultiCellPage} name="multiCell" />

<Set wrap={BlogLayout}>
<Route path="/blog" page={BlogPage} name="blog" />
<Route path="/blog/{slug}" page={BlogPostPage} name="blogPost" />+{' '}
</Set>

<Set wrap={AuthLayout}>
<Route path="/request" page={RequestPage} name="request" />
<Route path="/login" page={LoginPage} name="login" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web'

export const data = async ({ slug }: { slug: string }) => {
const blogPostPath = path.join(
path.dirname(fileURLToPath(import.meta.url)),
'..',
'..',
'..',
'..',
'api',
'db',
'blog',
`${slug}.md`
)

const blogPost = await fs.promises.readFile(blogPostPath, 'utf-8')

return { blogPost }
}

export const Loading = () => <div>Loading...</div>

export const Empty = () => <div>Empty</div>

export const Failure = ({ error }: CellFailureProps) => (
<div style={{ color: 'red' }}>Error: {error?.message}</div>
)

type SuccessProps = CellSuccessProps<Awaited<ReturnType<typeof data>>>
export const Success = ({ blogPost }: SuccessProps) => {
return (
<div>
<pre>{blogPost}</pre>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

import { Link } from '@redwoodjs/router/Link'
import { namedRoutes as routes } from '@redwoodjs/router/namedRoutes'
import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web'

type BlogPost = {
slug: string
title: string
}

export const data = async () => {
const blogDir = path.join(
path.dirname(fileURLToPath(import.meta.url)),
'..',
'..',
'..',
'..',
'api',
'db',
'blog'
)

const fileNames = await fs.promises.readdir(blogDir)

const blogPosts: BlogPost[] = []

for (const fileName of fileNames) {
const slug = fileName.replace(/\.md$/, '')
console.log('slug:', slug)
const postContent = await fs.promises.readFile(
path.join(blogDir, fileName),
'utf-8'
)

const title = postContent
.split('\n')
.find((line) => line.startsWith('# '))
?.replace('# ', '')

blogPosts.push({ slug, title: title || 'Untitled' })
}

// DX: Can we return a single value instead of an object?
return { blogPosts }
}

export const Loading = () => <div>Loading blog posts...</div>

export const Empty = () => <div>No posts yet</div>

export const Failure = ({ error }: CellFailureProps) => (
<div className="rw-cell-error">{error?.message}</div>
)

type SuccessProps = CellSuccessProps<Awaited<ReturnType<typeof data>>>
export const Success = ({ blogPosts }: SuccessProps) => {
return (
<ul>
{blogPosts.map((post) => (
<li key={post.slug}>
<Link to={routes.blogPost({ slug: post.slug })}>{post.title}</Link>
</li>
))}
</ul>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web'

import { savePost } from 'src/lib/actions'

export const data = async ({ slug }: { slug: string }) => {
const blogPostPath = path.join(
path.dirname(fileURLToPath(import.meta.url)),
'..',
'..',
'..',
'..',
'api',
'db',
'blog',
`${slug}.md`
)

const blogPost = await fs.promises.readFile(blogPostPath, 'utf-8')
let isInFrontmatter = false
let hasExitedFrontmatter = false

const { author, body } = blogPost.split('\n').reduce(
(prev, curr) => {
if (curr === '---' && !isInFrontmatter && !hasExitedFrontmatter) {
isInFrontmatter = true
return prev
}

if (isInFrontmatter && curr.startsWith('Author: ')) {
return { ...prev, author: curr.replace('Author: ', '').trim() }
}

if (curr === '---' && isInFrontmatter) {
isInFrontmatter = false
hasExitedFrontmatter = true
return prev
}

if (hasExitedFrontmatter) {
return { ...prev, body: prev.body + curr + '\n' }
}

return prev
},
{ author: '', body: '' }
)

return { slug, author, body: body.trim() }
}

export const Loading = () => <div>Loading...</div>

export const Empty = () => <div>Empty</div>

export const Failure = ({ error }: CellFailureProps) => (
<div style={{ color: 'red' }}>Error: {error?.message}</div>
)

type SuccessProps = CellSuccessProps<Awaited<ReturnType<typeof data>>>
export const Success = ({ author, body }: SuccessProps) => {
return (
<div>
<form action={savePost}>
<label htmlFor="author">
Author
<input type="text" name="author" id="author" value={author} />
</label>
<label htmlFor="body">
Body
<textarea name="body" id="body">
{body}
</textarea>
</label>
<button type="submit">Save</button>
</form>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
.blog-layout {
display: flex;
flex-direction: row;
flex-grow: 1;

& > nav {
width: 12rem;
border-right: 1px solid gray;
margin-right: 1rem;

button {
background: none;
border: none;
color: blue;
text-decoration: underline;
cursor: pointer;
padding: 0;
font: inherit;
}

li.new-post {
margin-bottom: 0.8rem;
}
}

& > section {
flex-grow: 1;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// import { useRouteName } from '@redwoodjs/router/dist/useRouteName'
import { Link } from '@redwoodjs/router/Link'
import { namedRoutes as routes } from '@redwoodjs/router/namedRoutes'
import { getLocation } from '@redwoodjs/server-store'

import BlogPostsNavCell from 'src/components/BlogPostsNavCell/BlogPostsNavCell'
import { deletePost } from 'src/lib/actions'

import './BlogLayout.css'

type BlogLayoutProps = {
children?: React.ReactNode
}

const BlogLayout = ({ children }: BlogLayoutProps) => {
// I wish I could do this, but I can't because this is a server component,
// and server components can't use hooks
// const routeName = useRouteName()

const { pathname } = getLocation()
const blogPostPageMatch = pathname.match(/^\/blog\/([-\w]+)$/)
const slug = blogPostPageMatch?.[1]

return (
<div className="blog-layout">
<nav className="blog-posts-nav">
<BlogPostsNavCell />
<hr />
<ul>
<li className="new-post">
<Link to={routes.newBlogPost()}>New Blog Post</Link>
</li>
{slug && slug !== 'new' && (
<>
<li>
<Link to={routes.editBlogPost({ slug })}>Edit Blog Post</Link>
</li>
<li>
<form action={deletePost} method="post">
<input type="hidden" name="slug" value={slug} />
<button type="submit">Delete Blog Post</button>
</form>
</li>
</>
)}
</ul>
</nav>
<section>{children}</section>
</div>
)
}

export default BlogLayout
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
.navigation-layout {
display: flex;
flex-direction: column;
min-height: 100vh;

& > nav {
display: flex;
justify-content: space-between;
Expand Down Expand Up @@ -35,4 +39,17 @@
border-bottom: 2px solid #333;
}
}

& > p {
margin: 0.4rem 0;
}

& > hr {
margin: 0.5rem 0 0 0;
}

& > main {
display: flex;
flex-grow: 1;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ const NavigationLayout = ({ children, rnd }: NavigationLayoutProps) => {
pathname === routes.resetPassword() ||
pathname === routes.profile()

const isBlogRoute =
pathname === routes.blog() || pathname.startsWith(routes.blog() + '/')

return (
<div className="navigation-layout">
<nav>
Expand All @@ -44,6 +47,9 @@ const NavigationLayout = ({ children, rnd }: NavigationLayoutProps) => {
<li>
<Link to={routes.multiCell()}>Multi Cell</Link>
</li>
<li>
<Link to={routes.blog()}>Blog</Link>
</li>
<li>
<NavLink
to={isAuthenticated ? routes.profile() : routes.login()}
Expand All @@ -62,7 +68,7 @@ const NavigationLayout = ({ children, rnd }: NavigationLayoutProps) => {
</li>
</ul>
</nav>
{!isAuthRoute && (
{!isAuthRoute && !isBlogRoute && (
<>
<div id="rnd">{Math.round(rnd * 100)}</div>
<ReadFileServerCell />
Expand Down
Loading
Loading