Add a Posts Schema just to test authentication

This commit is contained in:
Konstantin Hintermayer 2024-10-04 09:50:58 +02:00
parent 437e49b842
commit 1a12ed6c9c
23 changed files with 984 additions and 1 deletions

View File

@ -0,0 +1,8 @@
-- CreateTable
CREATE TABLE "Post" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"title" TEXT NOT NULL,
"body" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);

View File

@ -52,3 +52,11 @@ model Identity {
@@unique([provider, uid]) @@unique([provider, uid])
@@index(userId) @@index(userId)
} }
model Post {
id Int @id @default(autoincrement())
title String
body String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

View File

@ -0,0 +1,30 @@
export const schema = gql`
type Post {
id: Int!
title: String!
body: String!
createdAt: DateTime!
updatedAt: DateTime!
}
type Query {
posts: [Post!]! @requireAuth
post(id: Int!): Post @requireAuth
}
input CreatePostInput {
title: String!
body: String!
}
input UpdatePostInput {
title: String
body: String
}
type Mutation {
createPost(input: CreatePostInput!): Post! @requireAuth
updatePost(id: Int!, input: UpdatePostInput!): Post! @requireAuth
deletePost(id: Int!): Post! @requireAuth
}
`

View File

@ -0,0 +1,23 @@
import type { Prisma, Post } from '@prisma/client'
import type { ScenarioData } from '@redwoodjs/testing/api'
export const standard = defineScenario<Prisma.PostCreateArgs>({
post: {
one: {
data: {
title: 'String',
body: 'String',
updatedAt: '2024-10-04T07:38:59.006Z',
},
},
two: {
data: {
title: 'String',
body: 'String',
updatedAt: '2024-10-04T07:38:59.006Z',
},
},
},
})
export type StandardScenario = ScenarioData<Post, 'post'>

View File

@ -0,0 +1,55 @@
import type { Post } from '@prisma/client'
import { posts, post, createPost, updatePost, deletePost } from './posts'
import type { StandardScenario } from './posts.scenarios'
// Generated boilerplate tests do not account for all circumstances
// and can fail without adjustments, e.g. Float.
// Please refer to the RedwoodJS Testing Docs:
// https://redwoodjs.com/docs/testing#testing-services
// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
describe('posts', () => {
scenario('returns all posts', async (scenario: StandardScenario) => {
const result = await posts()
expect(result.length).toEqual(Object.keys(scenario.post).length)
})
scenario('returns a single post', async (scenario: StandardScenario) => {
const result = await post({ id: scenario.post.one.id })
expect(result).toEqual(scenario.post.one)
})
scenario('creates a post', async () => {
const result = await createPost({
input: {
title: 'String',
body: 'String',
updatedAt: '2024-10-04T07:38:58.985Z',
},
})
expect(result.title).toEqual('String')
expect(result.body).toEqual('String')
expect(result.updatedAt).toEqual(new Date('2024-10-04T07:38:58.985Z'))
})
scenario('updates a post', async (scenario: StandardScenario) => {
const original = (await post({ id: scenario.post.one.id })) as Post
const result = await updatePost({
id: original.id,
input: { title: 'String2' },
})
expect(result.title).toEqual('String2')
})
scenario('deletes a post', async (scenario: StandardScenario) => {
const original = (await deletePost({ id: scenario.post.one.id })) as Post
const result = await post({ id: original.id })
expect(result).toEqual(null)
})
})

View File

@ -0,0 +1,32 @@
import type { QueryResolvers, MutationResolvers } from 'types/graphql'
import { db } from 'src/lib/db'
export const posts: QueryResolvers['posts'] = () => {
return db.post.findMany()
}
export const post: QueryResolvers['post'] = ({ id }) => {
return db.post.findUnique({
where: { id },
})
}
export const createPost: MutationResolvers['createPost'] = ({ input }) => {
return db.post.create({
data: input,
})
}
export const updatePost: MutationResolvers['updatePost'] = ({ id, input }) => {
return db.post.update({
data: input,
where: { id },
})
}
export const deletePost: MutationResolvers['deletePost'] = ({ id }) => {
return db.post.delete({
where: { id },
})
}

View File

@ -16,6 +16,7 @@
"@redwoodjs/router": "8.3.0", "@redwoodjs/router": "8.3.0",
"@redwoodjs/web": "8.3.0", "@redwoodjs/web": "8.3.0",
"@redwoodjs/web-server": "8.3.0", "@redwoodjs/web-server": "8.3.0",
"humanize-string": "2.1.0",
"react": "18.3.1", "react": "18.3.1",
"react-dom": "18.3.1" "react-dom": "18.3.1"
}, },

View File

@ -7,13 +7,21 @@
// 'src/pages/HomePage/HomePage.js' -> HomePage // 'src/pages/HomePage/HomePage.js' -> HomePage
// 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage // 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage
import { Router, Route } from '@redwoodjs/router' import { Set, Router, Route, PrivateSet } from '@redwoodjs/router'
import ScaffoldLayout from 'src/layouts/ScaffoldLayout'
import { useAuth } from './auth' import { useAuth } from './auth'
const Routes = () => { const Routes = () => {
return ( return (
<Router useAuth={useAuth}> <Router useAuth={useAuth}>
<Set wrap={ScaffoldLayout} title="Posts" titleTo="posts" buttonLabel="New Post" buttonTo="newPost">
<Route path="/admin/posts/new" page={PostNewPostPage} name="newPost" />
<Route path="/admin/posts/{id:Int}/edit" page={PostEditPostPage} name="editPost" />
<Route path="/admin/posts/{id:Int}" page={PostPostPage} name="post" />
<Route path="/admin/posts" page={PostPostsPage} name="posts" />
</Set>
<Route path="/login" page={LoginPage} name="login" /> <Route path="/login" page={LoginPage} name="login" />
<Route path="/signup" page={SignupPage} name="signup" /> <Route path="/signup" page={SignupPage} name="signup" />
<Route path="/forgot-password" page={ForgotPasswordPage} name="forgotPassword" /> <Route path="/forgot-password" page={ForgotPasswordPage} name="forgotPassword" />

View File

@ -0,0 +1,78 @@
import type {
EditPostById,
UpdatePostInput,
UpdatePostMutationVariables,
} from 'types/graphql'
import { navigate, routes } from '@redwoodjs/router'
import type {
CellSuccessProps,
CellFailureProps,
TypedDocumentNode,
} from '@redwoodjs/web'
import { useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import PostForm from 'src/components/Post/PostForm'
export const QUERY: TypedDocumentNode<EditPostById> = gql`
query EditPostById($id: Int!) {
post: post(id: $id) {
id
title
body
createdAt
updatedAt
}
}
`
const UPDATE_POST_MUTATION: TypedDocumentNode<
EditPostById,
UpdatePostMutationVariables
> = gql`
mutation UpdatePostMutation($id: Int!, $input: UpdatePostInput!) {
updatePost(id: $id, input: $input) {
id
title
body
createdAt
updatedAt
}
}
`
export const Loading = () => <div>Loading...</div>
export const Failure = ({ error }: CellFailureProps) => (
<div className="rw-cell-error">{error?.message}</div>
)
export const Success = ({ post }: CellSuccessProps<EditPostById>) => {
const [updatePost, { loading, error }] = useMutation(UPDATE_POST_MUTATION, {
onCompleted: () => {
toast.success('Post updated')
navigate(routes.posts())
},
onError: (error) => {
toast.error(error.message)
},
})
const onSave = (input: UpdatePostInput, id: EditPostById['post']['id']) => {
updatePost({ variables: { id, input } })
}
return (
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">
Edit Post {post?.id}
</h2>
</header>
<div className="rw-segment-main">
<PostForm post={post} onSave={onSave} error={error} loading={loading} />
</div>
</div>
)
}

View File

@ -0,0 +1,52 @@
import type {
CreatePostMutation,
CreatePostInput,
CreatePostMutationVariables,
} from 'types/graphql'
import { navigate, routes } from '@redwoodjs/router'
import { useMutation } from '@redwoodjs/web'
import type { TypedDocumentNode } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import PostForm from 'src/components/Post/PostForm'
const CREATE_POST_MUTATION: TypedDocumentNode<
CreatePostMutation,
CreatePostMutationVariables
> = gql`
mutation CreatePostMutation($input: CreatePostInput!) {
createPost(input: $input) {
id
}
}
`
const NewPost = () => {
const [createPost, { loading, error }] = useMutation(CREATE_POST_MUTATION, {
onCompleted: () => {
toast.success('Post created')
navigate(routes.posts())
},
onError: (error) => {
toast.error(error.message)
},
})
const onSave = (input: CreatePostInput) => {
createPost({ variables: { input } })
}
return (
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">New Post</h2>
</header>
<div className="rw-segment-main">
<PostForm onSave={onSave} loading={loading} error={error} />
</div>
</div>
)
}
export default NewPost

View File

@ -0,0 +1,98 @@
import type {
DeletePostMutation,
DeletePostMutationVariables,
FindPostById,
} from 'types/graphql'
import { Link, routes, navigate } from '@redwoodjs/router'
import { useMutation } from '@redwoodjs/web'
import type { TypedDocumentNode } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import { timeTag } from 'src/lib/formatters'
const DELETE_POST_MUTATION: TypedDocumentNode<
DeletePostMutation,
DeletePostMutationVariables
> = gql`
mutation DeletePostMutation($id: Int!) {
deletePost(id: $id) {
id
}
}
`
interface Props {
post: NonNullable<FindPostById['post']>
}
const Post = ({ post }: Props) => {
const [deletePost] = useMutation(DELETE_POST_MUTATION, {
onCompleted: () => {
toast.success('Post deleted')
navigate(routes.posts())
},
onError: (error) => {
toast.error(error.message)
},
})
const onDeleteClick = (id: DeletePostMutationVariables['id']) => {
if (confirm('Are you sure you want to delete post ' + id + '?')) {
deletePost({ variables: { id } })
}
}
return (
<>
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">
Post {post.id} Detail
</h2>
</header>
<table className="rw-table">
<tbody>
<tr>
<th>Id</th>
<td>{post.id}</td>
</tr>
<tr>
<th>Title</th>
<td>{post.title}</td>
</tr>
<tr>
<th>Body</th>
<td>{post.body}</td>
</tr>
<tr>
<th>Created at</th>
<td>{timeTag(post.createdAt)}</td>
</tr>
<tr>
<th>Updated at</th>
<td>{timeTag(post.updatedAt)}</td>
</tr>
</tbody>
</table>
</div>
<nav className="rw-button-group">
<Link
to={routes.editPost({ id: post.id })}
className="rw-button rw-button-blue"
>
Edit
</Link>
<button
type="button"
className="rw-button rw-button-red"
onClick={() => onDeleteClick(post.id)}
>
Delete
</button>
</nav>
</>
)
}
export default Post

View File

@ -0,0 +1,36 @@
import type { FindPostById, FindPostByIdVariables } from 'types/graphql'
import type {
CellSuccessProps,
CellFailureProps,
TypedDocumentNode,
} from '@redwoodjs/web'
import Post from 'src/components/Post/Post'
export const QUERY: TypedDocumentNode<FindPostById, FindPostByIdVariables> =
gql`
query FindPostById($id: Int!) {
post: post(id: $id) {
id
title
body
createdAt
updatedAt
}
}
`
export const Loading = () => <div>Loading...</div>
export const Empty = () => <div>Post not found</div>
export const Failure = ({ error }: CellFailureProps<FindPostByIdVariables>) => (
<div className="rw-cell-error">{error?.message}</div>
)
export const Success = ({
post,
}: CellSuccessProps<FindPostById, FindPostByIdVariables>) => {
return <Post post={post} />
}

View File

@ -0,0 +1,83 @@
import type { EditPostById, UpdatePostInput } from 'types/graphql'
import type { RWGqlError } from '@redwoodjs/forms'
import {
Form,
FormError,
FieldError,
Label,
TextField,
Submit,
} from '@redwoodjs/forms'
type FormPost = NonNullable<EditPostById['post']>
interface PostFormProps {
post?: EditPostById['post']
onSave: (data: UpdatePostInput, id?: FormPost['id']) => void
error: RWGqlError
loading: boolean
}
const PostForm = (props: PostFormProps) => {
const onSubmit = (data: FormPost) => {
props.onSave(data, props?.post?.id)
}
return (
<div className="rw-form-wrapper">
<Form<FormPost> onSubmit={onSubmit} error={props.error}>
<FormError
error={props.error}
wrapperClassName="rw-form-error-wrapper"
titleClassName="rw-form-error-title"
listClassName="rw-form-error-list"
/>
<Label
name="title"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Title
</Label>
<TextField
name="title"
defaultValue={props.post?.title}
className="rw-input"
errorClassName="rw-input rw-input-error"
validation={{ required: true }}
/>
<FieldError name="title" className="rw-field-error" />
<Label
name="body"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Body
</Label>
<TextField
name="body"
defaultValue={props.post?.body}
className="rw-input"
errorClassName="rw-input rw-input-error"
validation={{ required: true }}
/>
<FieldError name="body" className="rw-field-error" />
<div className="rw-button-group">
<Submit disabled={props.loading} className="rw-button rw-button-blue">
Save
</Submit>
</div>
</Form>
</div>
)
}
export default PostForm

View File

@ -0,0 +1,102 @@
import type {
DeletePostMutation,
DeletePostMutationVariables,
FindPosts,
} from 'types/graphql'
import { Link, routes } from '@redwoodjs/router'
import { useMutation } from '@redwoodjs/web'
import type { TypedDocumentNode } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import { QUERY } from 'src/components/Post/PostsCell'
import { timeTag, truncate } from 'src/lib/formatters'
const DELETE_POST_MUTATION: TypedDocumentNode<
DeletePostMutation,
DeletePostMutationVariables
> = gql`
mutation DeletePostMutation($id: Int!) {
deletePost(id: $id) {
id
}
}
`
const PostsList = ({ posts }: FindPosts) => {
const [deletePost] = useMutation(DELETE_POST_MUTATION, {
onCompleted: () => {
toast.success('Post deleted')
},
onError: (error) => {
toast.error(error.message)
},
// This refetches the query on the list page. Read more about other ways to
// update the cache over here:
// https://www.apollographql.com/docs/react/data/mutations/#making-all-other-cache-updates
refetchQueries: [{ query: QUERY }],
awaitRefetchQueries: true,
})
const onDeleteClick = (id: DeletePostMutationVariables['id']) => {
if (confirm('Are you sure you want to delete post ' + id + '?')) {
deletePost({ variables: { id } })
}
}
return (
<div className="rw-segment rw-table-wrapper-responsive">
<table className="rw-table">
<thead>
<tr>
<th>Id</th>
<th>Title</th>
<th>Body</th>
<th>Created at</th>
<th>Updated at</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
{posts.map((post) => (
<tr key={post.id}>
<td>{truncate(post.id)}</td>
<td>{truncate(post.title)}</td>
<td>{truncate(post.body)}</td>
<td>{timeTag(post.createdAt)}</td>
<td>{timeTag(post.updatedAt)}</td>
<td>
<nav className="rw-table-actions">
<Link
to={routes.post({ id: post.id })}
title={'Show post ' + post.id + ' detail'}
className="rw-button rw-button-small"
>
Show
</Link>
<Link
to={routes.editPost({ id: post.id })}
title={'Edit post ' + post.id}
className="rw-button rw-button-small rw-button-blue"
>
Edit
</Link>
<button
type="button"
title={'Delete post ' + post.id}
className="rw-button rw-button-small rw-button-red"
onClick={() => onDeleteClick(post.id)}
>
Delete
</button>
</nav>
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
export default PostsList

View File

@ -0,0 +1,45 @@
import type { FindPosts, FindPostsVariables } from 'types/graphql'
import { Link, routes } from '@redwoodjs/router'
import type {
CellSuccessProps,
CellFailureProps,
TypedDocumentNode,
} from '@redwoodjs/web'
import Posts from 'src/components/Post/Posts'
export const QUERY: TypedDocumentNode<FindPosts, FindPostsVariables> = gql`
query FindPosts {
posts {
id
title
body
createdAt
updatedAt
}
}
`
export const Loading = () => <div>Loading...</div>
export const Empty = () => {
return (
<div className="rw-text-center">
No posts yet.{' '}
<Link to={routes.newPost()} className="rw-link">
Create one?
</Link>
</div>
)
}
export const Failure = ({ error }: CellFailureProps<FindPosts>) => (
<div className="rw-cell-error">{error?.message}</div>
)
export const Success = ({
posts,
}: CellSuccessProps<FindPosts, FindPostsVariables>) => {
return <Posts posts={posts} />
}

View File

@ -0,0 +1,37 @@
import { Link, routes } from '@redwoodjs/router'
import { Toaster } from '@redwoodjs/web/toast'
type LayoutProps = {
title: string
titleTo: keyof typeof routes
buttonLabel: string
buttonTo: keyof typeof routes
children: React.ReactNode
}
const ScaffoldLayout = ({
title,
titleTo,
buttonLabel,
buttonTo,
children,
}: LayoutProps) => {
return (
<div className="rw-scaffold">
<Toaster toastOptions={{ className: 'rw-toast', duration: 6000 }} />
<header className="rw-header">
<h1 className="rw-heading rw-heading-primary">
<Link to={routes[titleTo]()} className="rw-link">
{title}
</Link>
</h1>
<Link to={routes[buttonTo]()} className="rw-button rw-button-green">
<div className="rw-button-icon">+</div> {buttonLabel}
</Link>
</header>
<main className="rw-main">{children}</main>
</div>
)
}
export default ScaffoldLayout

View File

@ -0,0 +1,192 @@
import { render, waitFor, screen } from '@redwoodjs/testing/web'
import {
formatEnum,
jsonTruncate,
truncate,
timeTag,
jsonDisplay,
checkboxInputTag,
} from './formatters'
describe('formatEnum', () => {
it('handles nullish values', () => {
expect(formatEnum(null)).toEqual('')
expect(formatEnum('')).toEqual('')
expect(formatEnum(undefined)).toEqual('')
})
it('formats a list of values', () => {
expect(
formatEnum(['RED', 'ORANGE', 'YELLOW', 'GREEN', 'BLUE', 'VIOLET'])
).toEqual('Red, Orange, Yellow, Green, Blue, Violet')
})
it('formats a single value', () => {
expect(formatEnum('DARK_BLUE')).toEqual('Dark blue')
})
it('returns an empty string for values of the wrong type (for JS projects)', () => {
// @ts-expect-error - Testing JS scenario
expect(formatEnum(5)).toEqual('')
})
})
describe('truncate', () => {
it('truncates really long strings', () => {
expect(truncate('na '.repeat(1000) + 'batman').length).toBeLessThan(1000)
expect(truncate('na '.repeat(1000) + 'batman')).not.toMatch(/batman/)
})
it('does not modify short strings', () => {
expect(truncate('Short strinG')).toEqual('Short strinG')
})
it('adds ... to the end of truncated strings', () => {
expect(truncate('repeat'.repeat(1000))).toMatch(/\w\.\.\.$/)
})
it('accepts numbers', () => {
expect(truncate(123)).toEqual('123')
expect(truncate(0)).toEqual('0')
expect(truncate(0o000)).toEqual('0')
})
it('handles arguments of invalid type', () => {
// @ts-expect-error - Testing JS scenario
expect(truncate(false)).toEqual('false')
expect(truncate(undefined)).toEqual('')
expect(truncate(null)).toEqual('')
})
})
describe('jsonTruncate', () => {
it('truncates large json structures', () => {
expect(
jsonTruncate({
foo: 'foo',
bar: 'bar',
baz: 'baz',
kittens: 'kittens meow',
bazinga: 'Sheldon',
nested: {
foobar: 'I have no imagination',
two: 'Second nested item',
},
five: 5,
bool: false,
})
).toMatch(/.+\n.+\w\.\.\.$/s)
})
})
describe('timeTag', () => {
it('renders a date', async () => {
render(<div>{timeTag(new Date('1970-08-20').toUTCString())}</div>)
await waitFor(() => screen.getByText(/1970.*00:00:00/))
})
it('can take an empty input string', async () => {
expect(timeTag('')).toEqual('')
})
})
describe('jsonDisplay', () => {
it('produces the correct output', () => {
expect(
jsonDisplay({
title: 'TOML Example (but in JSON)',
database: {
data: [['delta', 'phi'], [3.14]],
enabled: true,
ports: [8000, 8001, 8002],
temp_targets: {
case: 72.0,
cpu: 79.5,
},
},
owner: {
dob: '1979-05-27T07:32:00-08:00',
name: 'Tom Preston-Werner',
},
servers: {
alpha: {
ip: '10.0.0.1',
role: 'frontend',
},
beta: {
ip: '10.0.0.2',
role: 'backend',
},
},
})
).toMatchInlineSnapshot(`
<pre>
<code>
{
"title": "TOML Example (but in JSON)",
"database": {
"data": [
[
"delta",
"phi"
],
[
3.14
]
],
"enabled": true,
"ports": [
8000,
8001,
8002
],
"temp_targets": {
"case": 72,
"cpu": 79.5
}
},
"owner": {
"dob": "1979-05-27T07:32:00-08:00",
"name": "Tom Preston-Werner"
},
"servers": {
"alpha": {
"ip": "10.0.0.1",
"role": "frontend"
},
"beta": {
"ip": "10.0.0.2",
"role": "backend"
}
}
}
</code>
</pre>
`)
})
})
describe('checkboxInputTag', () => {
it('can be checked', () => {
render(checkboxInputTag(true))
expect(screen.getByRole('checkbox')).toBeChecked()
})
it('can be unchecked', () => {
render(checkboxInputTag(false))
expect(screen.getByRole('checkbox')).not.toBeChecked()
})
it('is disabled when checked', () => {
render(checkboxInputTag(true))
expect(screen.getByRole('checkbox')).toBeDisabled()
})
it('is disabled when unchecked', () => {
render(checkboxInputTag(false))
expect(screen.getByRole('checkbox')).toBeDisabled()
})
})

View File

@ -0,0 +1,58 @@
import React from 'react'
import humanize from 'humanize-string'
const MAX_STRING_LENGTH = 150
export const formatEnum = (values: string | string[] | null | undefined) => {
let output = ''
if (Array.isArray(values)) {
const humanizedValues = values.map((value) => humanize(value))
output = humanizedValues.join(', ')
} else if (typeof values === 'string') {
output = humanize(values)
}
return output
}
export const jsonDisplay = (obj: unknown) => {
return (
<pre>
<code>{JSON.stringify(obj, null, 2)}</code>
</pre>
)
}
export const truncate = (value: string | number) => {
let output = value?.toString() ?? ''
if (output.length > MAX_STRING_LENGTH) {
output = output.substring(0, MAX_STRING_LENGTH) + '...'
}
return output
}
export const jsonTruncate = (obj: unknown) => {
return truncate(JSON.stringify(obj, null, 2))
}
export const timeTag = (dateTime?: string) => {
let output: string | JSX.Element = ''
if (dateTime) {
output = (
<time dateTime={dateTime} title={dateTime}>
{new Date(dateTime).toUTCString()}
</time>
)
}
return output
}
export const checkboxInputTag = (checked: boolean) => {
return <input type="checkbox" checked={checked} disabled />
}

View File

@ -0,0 +1,11 @@
import EditPostCell from 'src/components/Post/EditPostCell'
type PostPageProps = {
id: number
}
const EditPostPage = ({ id }: PostPageProps) => {
return <EditPostCell id={id} />
}
export default EditPostPage

View File

@ -0,0 +1,7 @@
import NewPost from 'src/components/Post/NewPost'
const NewPostPage = () => {
return <NewPost />
}
export default NewPostPage

View File

@ -0,0 +1,11 @@
import PostCell from 'src/components/Post/PostCell'
type PostPageProps = {
id: number
}
const PostPage = ({ id }: PostPageProps) => {
return <PostCell id={id} />
}
export default PostPage

View File

@ -0,0 +1,7 @@
import PostsCell from 'src/components/Post/PostsCell'
const PostsPage = () => {
return <PostsCell />
}
export default PostsPage

View File

@ -17108,6 +17108,7 @@ __metadata:
"@redwoodjs/web-server": "npm:8.3.0" "@redwoodjs/web-server": "npm:8.3.0"
"@types/react": "npm:^18.2.55" "@types/react": "npm:^18.2.55"
"@types/react-dom": "npm:^18.2.19" "@types/react-dom": "npm:^18.2.19"
humanize-string: "npm:2.1.0"
react: "npm:18.3.1" react: "npm:18.3.1"
react-dom: "npm:18.3.1" react-dom: "npm:18.3.1"
languageName: unknown languageName: unknown