diff --git a/api/prisma/migrations/20201009213512-create-posts/README.md b/api/prisma/migrations/20201009213512-create-posts/README.md
new file mode 100644
index 0000000..f52912a
--- /dev/null
+++ b/api/prisma/migrations/20201009213512-create-posts/README.md
@@ -0,0 +1,45 @@
+# Migration `20201009213512-create-posts`
+
+This migration has been generated by Kurt Hutten at 10/10/2020, 8:35:12 AM.
+You can check out the [state of the schema](./schema.prisma) after the migration.
+
+## Database Steps
+
+```sql
+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
+)
+```
+
+## Changes
+
+```diff
+diff --git schema.prisma schema.prisma
+migration ..20201009213512-create-posts
+--- datamodel.dml
++++ datamodel.dml
+@@ -1,0 +1,18 @@
++datasource DS {
++ // optionally set multiple providers
++ // example: provider = ["sqlite", "postgresql"]
++ provider = "sqlite"
++ url = "***"
++}
++
++generator client {
++ provider = "prisma-client-js"
++ binaryTargets = "native"
++}
++
++model Post {
++ id Int @id @default(autoincrement())
++ title String
++ body String
++ createdAt DateTime @default(now())
++}
+```
+
+
diff --git a/api/prisma/migrations/20201009213512-create-posts/schema.prisma b/api/prisma/migrations/20201009213512-create-posts/schema.prisma
new file mode 100644
index 0000000..624cbdb
--- /dev/null
+++ b/api/prisma/migrations/20201009213512-create-posts/schema.prisma
@@ -0,0 +1,18 @@
+datasource DS {
+ // optionally set multiple providers
+ // example: provider = ["sqlite", "postgresql"]
+ provider = "sqlite"
+ url = "***"
+}
+
+generator client {
+ provider = "prisma-client-js"
+ binaryTargets = "native"
+}
+
+model Post {
+ id Int @id @default(autoincrement())
+ title String
+ body String
+ createdAt DateTime @default(now())
+}
diff --git a/api/prisma/migrations/20201009213512-create-posts/steps.json b/api/prisma/migrations/20201009213512-create-posts/steps.json
new file mode 100644
index 0000000..f2edc88
--- /dev/null
+++ b/api/prisma/migrations/20201009213512-create-posts/steps.json
@@ -0,0 +1,120 @@
+{
+ "version": "0.3.14-fixed",
+ "steps": [
+ {
+ "tag": "CreateSource",
+ "source": "DS"
+ },
+ {
+ "tag": "CreateArgument",
+ "location": {
+ "tag": "Source",
+ "source": "DS"
+ },
+ "argument": "provider",
+ "value": "\"sqlite\""
+ },
+ {
+ "tag": "CreateArgument",
+ "location": {
+ "tag": "Source",
+ "source": "DS"
+ },
+ "argument": "url",
+ "value": "\"***\""
+ },
+ {
+ "tag": "CreateModel",
+ "model": "Post"
+ },
+ {
+ "tag": "CreateField",
+ "model": "Post",
+ "field": "id",
+ "type": "Int",
+ "arity": "Required"
+ },
+ {
+ "tag": "CreateDirective",
+ "location": {
+ "path": {
+ "tag": "Field",
+ "model": "Post",
+ "field": "id"
+ },
+ "directive": "id"
+ }
+ },
+ {
+ "tag": "CreateDirective",
+ "location": {
+ "path": {
+ "tag": "Field",
+ "model": "Post",
+ "field": "id"
+ },
+ "directive": "default"
+ }
+ },
+ {
+ "tag": "CreateArgument",
+ "location": {
+ "tag": "Directive",
+ "path": {
+ "tag": "Field",
+ "model": "Post",
+ "field": "id"
+ },
+ "directive": "default"
+ },
+ "argument": "",
+ "value": "autoincrement()"
+ },
+ {
+ "tag": "CreateField",
+ "model": "Post",
+ "field": "title",
+ "type": "String",
+ "arity": "Required"
+ },
+ {
+ "tag": "CreateField",
+ "model": "Post",
+ "field": "body",
+ "type": "String",
+ "arity": "Required"
+ },
+ {
+ "tag": "CreateField",
+ "model": "Post",
+ "field": "createdAt",
+ "type": "DateTime",
+ "arity": "Required"
+ },
+ {
+ "tag": "CreateDirective",
+ "location": {
+ "path": {
+ "tag": "Field",
+ "model": "Post",
+ "field": "createdAt"
+ },
+ "directive": "default"
+ }
+ },
+ {
+ "tag": "CreateArgument",
+ "location": {
+ "tag": "Directive",
+ "path": {
+ "tag": "Field",
+ "model": "Post",
+ "field": "createdAt"
+ },
+ "directive": "default"
+ },
+ "argument": "",
+ "value": "now()"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/api/prisma/migrations/migrate.lock b/api/prisma/migrations/migrate.lock
new file mode 100644
index 0000000..a23e9cb
--- /dev/null
+++ b/api/prisma/migrations/migrate.lock
@@ -0,0 +1,3 @@
+# Prisma Migrate lockfile v1
+
+20201009213512-create-posts
\ No newline at end of file
diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma
index d2cf1e8..929fed4 100644
--- a/api/prisma/schema.prisma
+++ b/api/prisma/schema.prisma
@@ -10,11 +10,9 @@ generator client {
binaryTargets = "native"
}
-// Define your own datamodels here and run `yarn redwood db save` to create
-// migrations for them.
-// TODO: Please remove the following example:
-model UserExample {
- id Int @id @default(autoincrement())
- email String @unique
- name String?
+model Post {
+ id Int @id @default(autoincrement())
+ title String
+ body String
+ createdAt DateTime @default(now())
}
diff --git a/api/src/graphql/posts.sdl.js b/api/src/graphql/posts.sdl.js
new file mode 100644
index 0000000..ad384ff
--- /dev/null
+++ b/api/src/graphql/posts.sdl.js
@@ -0,0 +1,29 @@
+export const schema = gql`
+ type Post {
+ id: Int!
+ title: String!
+ body: String!
+ createdAt: DateTime!
+ }
+
+ type Query {
+ posts: [Post!]!
+ post(id: Int!): Post
+ }
+
+ input CreatePostInput {
+ title: String!
+ body: String!
+ }
+
+ input UpdatePostInput {
+ title: String
+ body: String
+ }
+
+ type Mutation {
+ createPost(input: CreatePostInput!): Post!
+ updatePost(id: Int!, input: UpdatePostInput!): Post!
+ deletePost(id: Int!): Post!
+ }
+`
diff --git a/api/src/services/posts/posts.js b/api/src/services/posts/posts.js
new file mode 100644
index 0000000..be53d32
--- /dev/null
+++ b/api/src/services/posts/posts.js
@@ -0,0 +1,30 @@
+import { db } from 'src/lib/db'
+
+export const posts = () => {
+ return db.post.findMany()
+}
+
+export const post = ({ id }) => {
+ return db.post.findOne({
+ where: { id },
+ })
+}
+
+export const createPost = ({ input }) => {
+ return db.post.create({
+ data: input,
+ })
+}
+
+export const updatePost = ({ id, input }) => {
+ return db.post.update({
+ data: input,
+ where: { id },
+ })
+}
+
+export const deletePost = ({ id }) => {
+ return db.post.delete({
+ where: { id },
+ })
+}
diff --git a/api/src/services/posts/posts.test.js b/api/src/services/posts/posts.test.js
new file mode 100644
index 0000000..f23483a
--- /dev/null
+++ b/api/src/services/posts/posts.test.js
@@ -0,0 +1,9 @@
+/*
+import { posts } from './posts'
+*/
+
+describe('posts', () => {
+ it('returns true', () => {
+ expect(true).toBe(true)
+ })
+})
diff --git a/web/src/Routes.js b/web/src/Routes.js
index 2c8f02a..3a6bffb 100644
--- a/web/src/Routes.js
+++ b/web/src/Routes.js
@@ -12,6 +12,12 @@ import { Router, Route } from '@redwoodjs/router'
const Routes = () => {
return (
+
+
+
+
+
+
)
diff --git a/web/src/components/BlogPostsCell/BlogPostsCell.js b/web/src/components/BlogPostsCell/BlogPostsCell.js
new file mode 100644
index 0000000..cfd42e4
--- /dev/null
+++ b/web/src/components/BlogPostsCell/BlogPostsCell.js
@@ -0,0 +1,28 @@
+export const QUERY = gql`
+ query BlogPostsQuery {
+ posts {
+ id
+ title
+ body
+ createdAt
+ }
+ }
+`
+
+export const Loading = () =>
Loading...
+
+export const Empty = () => Empty
+
+export const Failure = ({ error }) => Error: {error.message}
+
+export const Success = ({ posts }) => {
+ return posts.map((post) => (
+
+
+ {post.body}
+ Posted on: {post.createdAt.split('T')[0]}
+
+ ))
+}
\ No newline at end of file
diff --git a/web/src/components/BlogPostsCell/BlogPostsCell.mock.js b/web/src/components/BlogPostsCell/BlogPostsCell.mock.js
new file mode 100644
index 0000000..70d4d6a
--- /dev/null
+++ b/web/src/components/BlogPostsCell/BlogPostsCell.mock.js
@@ -0,0 +1,6 @@
+// Define your own mock data here:
+export const standard = (/* vars, { ctx, req } */) => ({
+ blogPosts: {
+ id: 42,
+ },
+})
diff --git a/web/src/components/BlogPostsCell/BlogPostsCell.stories.js b/web/src/components/BlogPostsCell/BlogPostsCell.stories.js
new file mode 100644
index 0000000..841d3b2
--- /dev/null
+++ b/web/src/components/BlogPostsCell/BlogPostsCell.stories.js
@@ -0,0 +1,20 @@
+import { Loading, Empty, Failure, Success } from './BlogPostsCell'
+import { standard } from './BlogPostsCell.mock'
+
+export const loading = () => {
+ return Loading ? : null
+}
+
+export const empty = () => {
+ return Empty ? : null
+}
+
+export const failure = () => {
+ return Failure ? : null
+}
+
+export const success = () => {
+ return Success ? : null
+}
+
+export default { title: 'Cells/BlogPostsCell' }
diff --git a/web/src/components/BlogPostsCell/BlogPostsCell.test.js b/web/src/components/BlogPostsCell/BlogPostsCell.test.js
new file mode 100644
index 0000000..0d1fc66
--- /dev/null
+++ b/web/src/components/BlogPostsCell/BlogPostsCell.test.js
@@ -0,0 +1,26 @@
+import { render, screen } from '@redwoodjs/testing'
+import { Loading, Empty, Failure, Success } from './BlogPostsCell'
+import { standard } from './BlogPostsCell.mock'
+
+describe('BlogPostsCell', () => {
+ test('Loading renders successfully', () => {
+ render()
+ // Use screen.debug() to see output
+ expect(screen.getByText('Loading...')).toBeInTheDocument()
+ })
+
+ test('Empty renders successfully', async () => {
+ render()
+ expect(screen.getByText('Empty')).toBeInTheDocument()
+ })
+
+ test('Failure renders successfully', async () => {
+ render()
+ expect(screen.getByText(/Oh no/i)).toBeInTheDocument()
+ })
+
+ test('Success renders successfully', async () => {
+ render()
+ expect(screen.getByText(/42/i)).toBeInTheDocument()
+ })
+})
diff --git a/web/src/components/EditPostCell/EditPostCell.js b/web/src/components/EditPostCell/EditPostCell.js
new file mode 100644
index 0000000..904e44a
--- /dev/null
+++ b/web/src/components/EditPostCell/EditPostCell.js
@@ -0,0 +1,48 @@
+import { useMutation, useFlash } from '@redwoodjs/web'
+import { navigate, routes } from '@redwoodjs/router'
+import PostForm from 'src/components/PostForm'
+
+export const QUERY = gql`
+ query FIND_POST_BY_ID($id: Int!) {
+ post: post(id: $id) {
+ id
+ title
+ body
+ createdAt
+ }
+ }
+`
+const UPDATE_POST_MUTATION = gql`
+ mutation UpdatePostMutation($id: Int!, $input: UpdatePostInput!) {
+ updatePost(id: $id, input: $input) {
+ id
+ }
+ }
+`
+
+export const Loading = () => Loading...
+
+export const Success = ({ post }) => {
+ const { addMessage } = useFlash()
+ const [updatePost, { loading, error }] = useMutation(UPDATE_POST_MUTATION, {
+ onCompleted: () => {
+ navigate(routes.posts())
+ addMessage('Post updated.', { classes: 'rw-flash-success' })
+ },
+ })
+
+ const onSave = (input, id) => {
+ updatePost({ variables: { id, input } })
+ }
+
+ return (
+
+ )
+}
diff --git a/web/src/components/NewPost/NewPost.js b/web/src/components/NewPost/NewPost.js
new file mode 100644
index 0000000..ff4eec2
--- /dev/null
+++ b/web/src/components/NewPost/NewPost.js
@@ -0,0 +1,38 @@
+import { useMutation, useFlash } from '@redwoodjs/web'
+import { navigate, routes } from '@redwoodjs/router'
+import PostForm from 'src/components/PostForm'
+
+const CREATE_POST_MUTATION = gql`
+ mutation CreatePostMutation($input: CreatePostInput!) {
+ createPost(input: $input) {
+ id
+ }
+ }
+`
+
+const NewPost = () => {
+ const { addMessage } = useFlash()
+ const [createPost, { loading, error }] = useMutation(CREATE_POST_MUTATION, {
+ onCompleted: () => {
+ navigate(routes.posts())
+ addMessage('Post created.', { classes: 'rw-flash-success' })
+ },
+ })
+
+ const onSave = (input) => {
+ createPost({ variables: { input } })
+ }
+
+ return (
+
+ )
+}
+
+export default NewPost
diff --git a/web/src/components/Post/Post.js b/web/src/components/Post/Post.js
new file mode 100644
index 0000000..ff9c448
--- /dev/null
+++ b/web/src/components/Post/Post.js
@@ -0,0 +1,95 @@
+import { useMutation, useFlash } from '@redwoodjs/web'
+import { Link, routes, navigate } from '@redwoodjs/router'
+
+const DELETE_POST_MUTATION = gql`
+ mutation DeletePostMutation($id: Int!) {
+ deletePost(id: $id) {
+ id
+ }
+ }
+`
+
+const jsonDisplay = (obj) => {
+ return (
+
+ {JSON.stringify(obj, null, 2)}
+
+ )
+}
+
+const timeTag = (datetime) => {
+ return (
+
+ )
+}
+
+const checkboxInputTag = (checked) => {
+ return
+}
+
+const Post = ({ post }) => {
+ const { addMessage } = useFlash()
+ const [deletePost] = useMutation(DELETE_POST_MUTATION, {
+ onCompleted: () => {
+ navigate(routes.posts())
+ addMessage('Post deleted.', { classes: 'rw-flash-success' })
+ },
+ })
+
+ const onDeleteClick = (id) => {
+ if (confirm('Are you sure you want to delete post ' + id + '?')) {
+ deletePost({ variables: { id } })
+ }
+ }
+
+ return (
+ <>
+
+
+
+ Post {post.id} Detail
+
+
+
+
+
+ | Id |
+ {post.id} |
+
+
+ | Title |
+ {post.title} |
+
+
+ | Body |
+ {post.body} |
+
+
+ | Created at |
+ {timeTag(post.createdAt)} |
+
+
+
+
+
+ >
+ )
+}
+
+export default Post
diff --git a/web/src/components/PostCell/PostCell.js b/web/src/components/PostCell/PostCell.js
new file mode 100644
index 0000000..65a507b
--- /dev/null
+++ b/web/src/components/PostCell/PostCell.js
@@ -0,0 +1,20 @@
+import Post from 'src/components/Post'
+
+export const QUERY = gql`
+ query FIND_POST_BY_ID($id: Int!) {
+ post: post(id: $id) {
+ id
+ title
+ body
+ createdAt
+ }
+ }
+`
+
+export const Loading = () => Loading...
+
+export const Empty = () => Post not found
+
+export const Success = ({ post }) => {
+ return
+}
diff --git a/web/src/components/PostForm/PostForm.js b/web/src/components/PostForm/PostForm.js
new file mode 100644
index 0000000..90f5ff4
--- /dev/null
+++ b/web/src/components/PostForm/PostForm.js
@@ -0,0 +1,67 @@
+import {
+ Form,
+ FormError,
+ FieldError,
+ Label,
+ TextField,
+ Submit,
+} from '@redwoodjs/forms'
+
+const PostForm = (props) => {
+ const onSubmit = (data) => {
+ props.onSave(data, props?.post?.id)
+ }
+
+ return (
+
+ )
+}
+
+export default PostForm
diff --git a/web/src/components/Posts/Posts.js b/web/src/components/Posts/Posts.js
new file mode 100644
index 0000000..81f4b76
--- /dev/null
+++ b/web/src/components/Posts/Posts.js
@@ -0,0 +1,105 @@
+import { useMutation, useFlash } from '@redwoodjs/web'
+import { Link, routes } from '@redwoodjs/router'
+
+const DELETE_POST_MUTATION = gql`
+ mutation DeletePostMutation($id: Int!) {
+ deletePost(id: $id) {
+ id
+ }
+ }
+`
+
+const MAX_STRING_LENGTH = 150
+
+const truncate = (text) => {
+ let output = text
+ if (text && text.length > MAX_STRING_LENGTH) {
+ output = output.substring(0, MAX_STRING_LENGTH) + '...'
+ }
+ return output
+}
+
+const jsonTruncate = (obj) => {
+ return truncate(JSON.stringify(obj, null, 2))
+}
+
+const timeTag = (datetime) => {
+ return (
+
+ )
+}
+
+const checkboxInputTag = (checked) => {
+ return
+}
+
+const PostsList = ({ posts }) => {
+ const { addMessage } = useFlash()
+ const [deletePost] = useMutation(DELETE_POST_MUTATION, {
+ onCompleted: () => {
+ addMessage('Post deleted.', { classes: 'rw-flash-success' })
+ },
+ })
+
+ const onDeleteClick = (id) => {
+ if (confirm('Are you sure you want to delete post ' + id + '?')) {
+ deletePost({ variables: { id }, refetchQueries: ['POSTS'] })
+ }
+ }
+
+ return (
+
+
+
+
+ | Id |
+ Title |
+ Body |
+ Created at |
+ |
+
+
+
+ {posts.map((post) => (
+
+ | {truncate(post.id)} |
+ {truncate(post.title)} |
+ {truncate(post.body)} |
+ {timeTag(post.createdAt)} |
+
+
+ |
+
+ ))}
+
+
+
+ )
+}
+
+export default PostsList
diff --git a/web/src/components/PostsCell/PostsCell.js b/web/src/components/PostsCell/PostsCell.js
new file mode 100644
index 0000000..ddf49cd
--- /dev/null
+++ b/web/src/components/PostsCell/PostsCell.js
@@ -0,0 +1,31 @@
+import { Link, routes } from '@redwoodjs/router'
+
+import Posts from 'src/components/Posts'
+
+export const QUERY = gql`
+ query POSTS {
+ posts {
+ id
+ title
+ body
+ createdAt
+ }
+ }
+`
+
+export const Loading = () => Loading...
+
+export const Empty = () => {
+ return (
+
+ {'No posts yet. '}
+
+ {'Create one?'}
+
+
+ )
+}
+
+export const Success = ({ posts }) => {
+ return
+}
diff --git a/web/src/index.js b/web/src/index.js
index 63dd948..c7a2415 100644
--- a/web/src/index.js
+++ b/web/src/index.js
@@ -4,6 +4,7 @@ import FatalErrorPage from 'src/pages/FatalErrorPage'
import Routes from 'src/Routes'
+import './scaffold.css'
import './index.css'
ReactDOM.render(
diff --git a/web/src/layouts/BlogLayout/BlogLayout.js b/web/src/layouts/BlogLayout/BlogLayout.js
new file mode 100644
index 0000000..54134d9
--- /dev/null
+++ b/web/src/layouts/BlogLayout/BlogLayout.js
@@ -0,0 +1,24 @@
+import { Link, routes } from '@redwoodjs/router'
+
+const BlogLayout = ({ children }) => {
+ return (
+ <>
+
+ {children}
+ >
+ )
+}
+
+export default BlogLayout
diff --git a/web/src/layouts/BlogLayout/BlogLayout.stories.js b/web/src/layouts/BlogLayout/BlogLayout.stories.js
new file mode 100644
index 0000000..4e249ec
--- /dev/null
+++ b/web/src/layouts/BlogLayout/BlogLayout.stories.js
@@ -0,0 +1,7 @@
+import BlogLayout from './BlogLayout'
+
+export const generated = () => {
+ return
+}
+
+export default { title: 'Layouts/BlogLayout' }
diff --git a/web/src/layouts/BlogLayout/BlogLayout.test.js b/web/src/layouts/BlogLayout/BlogLayout.test.js
new file mode 100644
index 0000000..b87e730
--- /dev/null
+++ b/web/src/layouts/BlogLayout/BlogLayout.test.js
@@ -0,0 +1,11 @@
+import { render } from '@redwoodjs/testing'
+
+import BlogLayout from './BlogLayout'
+
+describe('BlogLayout', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render()
+ }).not.toThrow()
+ })
+})
diff --git a/web/src/layouts/PostsLayout/PostsLayout.js b/web/src/layouts/PostsLayout/PostsLayout.js
new file mode 100644
index 0000000..b08e8fa
--- /dev/null
+++ b/web/src/layouts/PostsLayout/PostsLayout.js
@@ -0,0 +1,23 @@
+import { Link, routes } from '@redwoodjs/router'
+import { Flash } from '@redwoodjs/web'
+
+const PostsLayout = (props) => {
+ return (
+
+
+
+
+
+ Posts
+
+
+
+ +
New Post
+
+
+
{props.children}
+
+ )
+}
+
+export default PostsLayout
diff --git a/web/src/pages/AboutPage/AboutPage.js b/web/src/pages/AboutPage/AboutPage.js
new file mode 100644
index 0000000..71a730c
--- /dev/null
+++ b/web/src/pages/AboutPage/AboutPage.js
@@ -0,0 +1,19 @@
+import { Link, routes } from '@redwoodjs/router'
+import BlogLayout from 'src/layouts/BlogLayout'
+
+const AboutPage = () => {
+ return (
+
+ AboutPage
+
+ Find me in ./web/src/pages/AboutPage/AboutPage.js
+
+
+ My default route is named about, link to me with `
+ About`
+
+
+ )
+}
+
+export default AboutPage
diff --git a/web/src/pages/AboutPage/AboutPage.stories.js b/web/src/pages/AboutPage/AboutPage.stories.js
new file mode 100644
index 0000000..3ca853b
--- /dev/null
+++ b/web/src/pages/AboutPage/AboutPage.stories.js
@@ -0,0 +1,7 @@
+import AboutPage from './AboutPage'
+
+export const generated = () => {
+ return
+}
+
+export default { title: 'Pages/AboutPage' }
diff --git a/web/src/pages/AboutPage/AboutPage.test.js b/web/src/pages/AboutPage/AboutPage.test.js
new file mode 100644
index 0000000..6878394
--- /dev/null
+++ b/web/src/pages/AboutPage/AboutPage.test.js
@@ -0,0 +1,11 @@
+import { render } from '@redwoodjs/testing'
+
+import AboutPage from './AboutPage'
+
+describe('AboutPage', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render()
+ }).not.toThrow()
+ })
+})
diff --git a/web/src/pages/EditPostPage/EditPostPage.js b/web/src/pages/EditPostPage/EditPostPage.js
new file mode 100644
index 0000000..419af6b
--- /dev/null
+++ b/web/src/pages/EditPostPage/EditPostPage.js
@@ -0,0 +1,12 @@
+import PostsLayout from 'src/layouts/PostsLayout'
+import EditPostCell from 'src/components/EditPostCell'
+
+const EditPostPage = ({ id }) => {
+ return (
+
+
+
+ )
+}
+
+export default EditPostPage
diff --git a/web/src/pages/HomePage/HomePage.js b/web/src/pages/HomePage/HomePage.js
new file mode 100644
index 0000000..7d32e12
--- /dev/null
+++ b/web/src/pages/HomePage/HomePage.js
@@ -0,0 +1,13 @@
+import BlogLayout from 'src/layouts/BlogLayout'
+import BlogPostsCell from 'src/components/BlogPostsCell'
+
+const HomePage = () => {
+ return (
+
+
+
+
+ )
+}
+
+export default HomePage
\ No newline at end of file
diff --git a/web/src/pages/HomePage/HomePage.stories.js b/web/src/pages/HomePage/HomePage.stories.js
new file mode 100644
index 0000000..7a14a5c
--- /dev/null
+++ b/web/src/pages/HomePage/HomePage.stories.js
@@ -0,0 +1,7 @@
+import HomePage from './HomePage'
+
+export const generated = () => {
+ return
+}
+
+export default { title: 'Pages/HomePage' }
diff --git a/web/src/pages/HomePage/HomePage.test.js b/web/src/pages/HomePage/HomePage.test.js
new file mode 100644
index 0000000..4880eea
--- /dev/null
+++ b/web/src/pages/HomePage/HomePage.test.js
@@ -0,0 +1,11 @@
+import { render } from '@redwoodjs/testing'
+
+import HomePage from './HomePage'
+
+describe('HomePage', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render()
+ }).not.toThrow()
+ })
+})
diff --git a/web/src/pages/NewPostPage/NewPostPage.js b/web/src/pages/NewPostPage/NewPostPage.js
new file mode 100644
index 0000000..7a41350
--- /dev/null
+++ b/web/src/pages/NewPostPage/NewPostPage.js
@@ -0,0 +1,12 @@
+import PostsLayout from 'src/layouts/PostsLayout'
+import NewPost from 'src/components/NewPost'
+
+const NewPostPage = () => {
+ return (
+
+
+
+ )
+}
+
+export default NewPostPage
diff --git a/web/src/pages/PostPage/PostPage.js b/web/src/pages/PostPage/PostPage.js
new file mode 100644
index 0000000..8b9eec3
--- /dev/null
+++ b/web/src/pages/PostPage/PostPage.js
@@ -0,0 +1,12 @@
+import PostsLayout from 'src/layouts/PostsLayout'
+import PostCell from 'src/components/PostCell'
+
+const PostPage = ({ id }) => {
+ return (
+
+
+
+ )
+}
+
+export default PostPage
diff --git a/web/src/pages/PostsPage/PostsPage.js b/web/src/pages/PostsPage/PostsPage.js
new file mode 100644
index 0000000..7318d9b
--- /dev/null
+++ b/web/src/pages/PostsPage/PostsPage.js
@@ -0,0 +1,12 @@
+import PostsLayout from 'src/layouts/PostsLayout'
+import PostsCell from 'src/components/PostsCell'
+
+const PostsPage = () => {
+ return (
+
+
+
+ )
+}
+
+export default PostsPage
diff --git a/web/src/scaffold.css b/web/src/scaffold.css
new file mode 100644
index 0000000..f2cef6b
--- /dev/null
+++ b/web/src/scaffold.css
@@ -0,0 +1,346 @@
+/*
+ normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css
+*/
+
+.rw-scaffold *,
+.rw-scaffold ::after,
+.rw-scaffold ::before {
+ box-sizing: inherit;
+ border-width: 0;
+ border-style: solid;
+ border-color: #e2e8f0;
+}
+.rw-scaffold main {
+ color: #4a5568;
+ display: block;
+}
+.rw-scaffold h1,
+.rw-scaffold h2 {
+ margin: 0;
+}
+.rw-scaffold a {
+ background-color: transparent;
+}
+.rw-scaffold ul {
+ margin: 0;
+ padding: 0;
+}
+.rw-scaffold input {
+ font-family: inherit;
+ font-size: 100%;
+ overflow: visible;
+}
+.rw-scaffold input:-ms-input-placeholder {
+ color: #a0aec0;
+}
+.rw-scaffold input::-ms-input-placeholder {
+ color: #a0aec0;
+}
+.rw-scaffold input::placeholder {
+ color: #a0aec0;
+}
+.rw-scaffold table {
+ border-collapse: collapse;
+}
+
+/*
+ Style
+*/
+
+.rw-scaffold {
+ background-color: #fff;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
+ 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji',
+ 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
+}
+.rw-header {
+ display: flex;
+ justify-content: space-between;
+ padding: 1rem 2rem 1rem 2rem;
+}
+.rw-main {
+ margin-left: 1rem;
+ margin-right: 1rem;
+ padding-bottom: 1rem;
+}
+.rw-segment {
+ background-color: #fff;
+ border-width: 1px;
+ border-radius: 0.5rem;
+ overflow: hidden;
+}
+.rw-segment-header {
+ background-color: #e2e8f0;
+ color: #4a5568;
+ padding: 0.75rem 1rem;
+}
+.rw-segment-main {
+ background-color: #f7fafc;
+ padding: 1rem;
+}
+.rw-link {
+ color: #4299e1;
+ text-decoration: underline;
+}
+.rw-link:hover {
+ color: #2b6cb0;
+}
+.rw-heading {
+ font-weight: 600;
+}
+.rw-heading.rw-heading-primary {
+ font-size: 1.25rem;
+}
+.rw-heading.rw-heading-secondary {
+ font-size: 0.875rem;
+}
+.rw-heading .rw-link {
+ color: #4a5568;
+ text-decoration: none;
+}
+.rw-heading .rw-link:hover {
+ color: #1a202c;
+ text-decoration: underline;
+}
+.rw-flash-message {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin: 0 1rem;
+ padding: 1rem;
+ background: #e2e8f0;
+ border-radius: 0.25rem;
+}
+.rw-flash-message .rw-flash-message-dismiss {
+ font-size: 1.25rem;
+ font-weight: 600;
+ line-height: 1;
+ margin-right: 0.25rem;
+ transform-origin: center;
+ transform: rotate(45deg);
+ cursor: pointer;
+}
+.rw-flash-message .rw-flash-message-dismiss:hover {
+ opacity: 0.7;
+}
+.rw-flash-message.rw-flash-success {
+ background: #48bb78;
+ color: #fff;
+}
+.rw-flash-message.rw-flash-error {
+ background: #e53e3e;
+ color: #fff;
+}
+.rw-form-wrapper {
+ box-sizing: border-box;
+ font-size: 0.875rem;
+ margin-top: -1rem;
+}
+.rw-form-error-wrapper {
+ padding: 1rem;
+ background-color: #fff5f5;
+ color: #c53030;
+ border-width: 1px;
+ border-color: #feb2b2;
+ border-radius: 0.25rem;
+ margin: 1rem 0;
+}
+.rw-form-error-title {
+ margin-top: 0;
+ margin-bottom: 0;
+ font-weight: 600;
+}
+.rw-form-error-list {
+ margin-top: 0.5rem;
+ list-style-type: disc;
+ list-style-position: inside;
+}
+.rw-button {
+ color: #718096;
+ cursor: pointer;
+ display: flex;
+ justify-content: center;
+ font-size: 0.75rem;
+ font-weight: 600;
+ padding: 0.25rem 1rem;
+ text-transform: uppercase;
+ text-decoration: none;
+ letter-spacing: 0.025em;
+ border-radius: 0.25rem;
+ line-height: 2;
+}
+.rw-button:hover {
+ background-color: #718096;
+ color: #fff;
+}
+.rw-button.rw-button-small {
+ font-size: 0.75rem;
+ border-radius: 0.125rem;
+ padding: 0.25rem 0.5rem;
+ line-height: inherit;
+}
+.rw-button.rw-button-green {
+ background-color: #48bb78;
+ color: #fff;
+}
+.rw-button.rw-button-green:hover {
+ background-color: #38a169;
+ color: #fff;
+}
+.rw-button.rw-button-blue {
+ background-color: #3182ce;
+ color: #fff;
+}
+.rw-button.rw-button-blue:hover {
+ background-color: #2b6cb0;
+}
+.rw-button.rw-button-red {
+ background-color: #e53e3e;
+ color: #fff;
+}
+.rw-button.rw-button-red:hover {
+ background-color: #c53030;
+}
+.rw-button-icon {
+ font-size: 1.25rem;
+ line-height: 1;
+ margin-right: 0.25rem;
+}
+.rw-button-group {
+ display: flex;
+ justify-content: center;
+ margin: 0.75rem 0.5rem;
+}
+.rw-button-group .rw-button {
+ margin: 0 0.25rem;
+}
+.rw-form-wrapper .rw-button-group {
+ margin-top: 2rem;
+ margin-bottom: 0;
+}
+.rw-label {
+ display: block;
+ margin-top: 1.5rem;
+ color: #4a5568;
+ font-weight: 600;
+}
+.rw-label.rw-label-error {
+ color: #c53030;
+}
+.rw-input {
+ display: block;
+ margin-top: 0.5rem;
+ width: 100%;
+ padding: 0.5rem;
+ border-width: 1px;
+ border-color: #e2e8f0;
+ color: #4a5568;
+ border-radius: 0.25rem;
+ outline: none;
+}
+.rw-input[type='checkbox'] {
+ width: initial;
+ margin-left: 0;
+}
+.rw-input:focus {
+ border-color: #a0aec0;
+}
+.rw-input-error {
+ border-color: #c53030;
+ color: #c53030;
+}
+.rw-field-error {
+ display: block;
+ margin-top: 0.25rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ font-size: 0.75rem;
+ color: #c53030;
+}
+.rw-table-wrapper-responsive {
+ overflow-x: scroll;
+}
+.rw-table-wrapper-responsive .rw-table {
+ min-width: 48rem;
+}
+.rw-table {
+ table-layout: auto;
+ width: 100%;
+ font-size: 0.875rem;
+}
+.rw-table th,
+.rw-table td {
+ padding: 0.75rem;
+}
+.rw-table thead tr {
+ background-color: #e2e8f0;
+ color: #4a5568;
+}
+.rw-table th {
+ font-weight: 600;
+ text-align: left;
+}
+.rw-table thead th {
+ text-align: left;
+}
+.rw-table tbody th {
+ text-align: right;
+}
+@media (min-width: 768px) {
+ .rw-table tbody th {
+ width: 20%;
+ }
+}
+.rw-table tbody tr {
+ background-color: #f7fafc;
+ border-top-width: 1px;
+}
+.rw-table tbody tr:nth-child(even) {
+ background-color: #fff;
+}
+.rw-table input {
+ margin-left: 0;
+}
+.rw-table-actions {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ height: 17px;
+ padding-right: 0.25rem;
+}
+.rw-table-actions .rw-button {
+ background-color: transparent;
+}
+.rw-table-actions .rw-button:hover {
+ background-color: #718096;
+ color: #fff;
+}
+.rw-table-actions .rw-button-blue {
+ color: #3182ce;
+}
+.rw-table-actions .rw-button-blue:hover {
+ background-color: #3182ce;
+ color: #fff;
+}
+.rw-table-actions .rw-button-red {
+ color: #e53e3e;
+}
+.rw-table-actions .rw-button-red:hover {
+ background-color: #e53e3e;
+ color: #fff;
+}
+.rw-text-center {
+ text-align: center;
+}
+.rw-slide-up {
+ animation: slideUp 0.5s 1 ease;
+ animation-fill-mode: forwards;
+ overflow-y: hidden;
+}
+@keyframes slideUp {
+ 100% {
+ max-height: 0;
+ padding-top: 0;
+ padding-bottom: 0;
+ }
+}