From 0c47db9001e6df02694b41980bb22884ba597bea Mon Sep 17 00:00:00 2001 From: Colin Diem Date: Thu, 14 Apr 2022 21:57:50 -0400 Subject: [PATCH] chapter 2 --- .../20220414233534_create_post/migration.sql | 7 + api/db/migrations/migration_lock.toml | 3 + api/db/schema.prisma | 12 +- api/src/graphql/posts.sdl.js | 29 ++ api/src/services/posts/posts.js | 30 ++ api/src/services/posts/posts.scenarios.js | 6 + api/src/services/posts/posts.test.js | 47 +++ web/src/App.js | 1 + web/src/Routes.js | 16 +- .../components/ArticlesCell/ArticlesCell.js | 34 ++ .../ArticlesCell/ArticlesCell.mock.js | 4 + .../ArticlesCell/ArticlesCell.stories.js | 20 + .../ArticlesCell/ArticlesCell.test.js | 41 ++ .../Post/EditPostCell/EditPostCell.js | 59 +++ web/src/components/Post/NewPost/NewPost.js | 41 ++ web/src/components/Post/Post/Post.js | 113 ++++++ web/src/components/Post/PostCell/PostCell.js | 24 ++ web/src/components/Post/PostForm/PostForm.js | 71 ++++ web/src/components/Post/Posts/Posts.js | 130 +++++++ .../components/Post/PostsCell/PostsCell.js | 35 ++ web/src/index.css | 50 +++ web/src/layouts/BlogLayout/BlogLayout.js | 23 ++ .../layouts/BlogLayout/BlogLayout.stories.js | 7 + web/src/layouts/BlogLayout/BlogLayout.test.js | 14 + web/src/layouts/PostsLayout/PostsLayout.js | 23 ++ web/src/pages/AboutPage/AboutPage.js | 14 + web/src/pages/AboutPage/AboutPage.stories.js | 7 + web/src/pages/AboutPage/AboutPage.test.js | 14 + web/src/pages/HomePage/HomePage.js | 13 + web/src/pages/HomePage/HomePage.stories.js | 7 + web/src/pages/HomePage/HomePage.test.js | 14 + .../pages/Post/EditPostPage/EditPostPage.js | 7 + web/src/pages/Post/NewPostPage/NewPostPage.js | 7 + web/src/pages/Post/PostPage/PostPage.js | 7 + web/src/pages/Post/PostsPage/PostsPage.js | 7 + web/src/scaffold.css | 366 ++++++++++++++++++ 36 files changed, 1294 insertions(+), 9 deletions(-) create mode 100644 api/db/migrations/20220414233534_create_post/migration.sql create mode 100644 api/db/migrations/migration_lock.toml create mode 100644 api/src/graphql/posts.sdl.js create mode 100644 api/src/services/posts/posts.js create mode 100644 api/src/services/posts/posts.scenarios.js create mode 100644 api/src/services/posts/posts.test.js create mode 100644 web/src/components/ArticlesCell/ArticlesCell.js create mode 100644 web/src/components/ArticlesCell/ArticlesCell.mock.js create mode 100644 web/src/components/ArticlesCell/ArticlesCell.stories.js create mode 100644 web/src/components/ArticlesCell/ArticlesCell.test.js create mode 100644 web/src/components/Post/EditPostCell/EditPostCell.js create mode 100644 web/src/components/Post/NewPost/NewPost.js create mode 100644 web/src/components/Post/Post/Post.js create mode 100644 web/src/components/Post/PostCell/PostCell.js create mode 100644 web/src/components/Post/PostForm/PostForm.js create mode 100644 web/src/components/Post/Posts/Posts.js create mode 100644 web/src/components/Post/PostsCell/PostsCell.js create mode 100644 web/src/layouts/BlogLayout/BlogLayout.js create mode 100644 web/src/layouts/BlogLayout/BlogLayout.stories.js create mode 100644 web/src/layouts/BlogLayout/BlogLayout.test.js create mode 100644 web/src/layouts/PostsLayout/PostsLayout.js create mode 100644 web/src/pages/AboutPage/AboutPage.js create mode 100644 web/src/pages/AboutPage/AboutPage.stories.js create mode 100644 web/src/pages/AboutPage/AboutPage.test.js create mode 100644 web/src/pages/HomePage/HomePage.js create mode 100644 web/src/pages/HomePage/HomePage.stories.js create mode 100644 web/src/pages/HomePage/HomePage.test.js create mode 100644 web/src/pages/Post/EditPostPage/EditPostPage.js create mode 100644 web/src/pages/Post/NewPostPage/NewPostPage.js create mode 100644 web/src/pages/Post/PostPage/PostPage.js create mode 100644 web/src/pages/Post/PostsPage/PostsPage.js create mode 100644 web/src/scaffold.css diff --git a/api/db/migrations/20220414233534_create_post/migration.sql b/api/db/migrations/20220414233534_create_post/migration.sql new file mode 100644 index 0000000..26e7ce5 --- /dev/null +++ b/api/db/migrations/20220414233534_create_post/migration.sql @@ -0,0 +1,7 @@ +-- 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 +); diff --git a/api/db/migrations/migration_lock.toml b/api/db/migrations/migration_lock.toml new file mode 100644 index 0000000..e5e5c47 --- /dev/null +++ b/api/db/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "sqlite" \ No newline at end of file diff --git a/api/db/schema.prisma b/api/db/schema.prisma index 3dea71a..3e7e700 100644 --- a/api/db/schema.prisma +++ b/api/db/schema.prisma @@ -8,11 +8,9 @@ generator client { binaryTargets = "native" } -// Define your own datamodels here and run `yarn redwood prisma migrate dev` -// to create migrations for them and apply to your dev DB. -// 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..52a44a7 --- /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!]! @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 + } +` diff --git a/api/src/services/posts/posts.js b/api/src/services/posts/posts.js new file mode 100644 index 0000000..3d680c9 --- /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.findUnique({ + 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.scenarios.js b/api/src/services/posts/posts.scenarios.js new file mode 100644 index 0000000..8126153 --- /dev/null +++ b/api/src/services/posts/posts.scenarios.js @@ -0,0 +1,6 @@ +export const standard = defineScenario({ + post: { + one: { data: { title: 'String', body: 'String' } }, + two: { data: { title: 'String', body: 'String' } }, + }, +}) diff --git a/api/src/services/posts/posts.test.js b/api/src/services/posts/posts.test.js new file mode 100644 index 0000000..12eb27e --- /dev/null +++ b/api/src/services/posts/posts.test.js @@ -0,0 +1,47 @@ +import { posts, post, createPost, updatePost, deletePost } from './posts' + +// Generated boilerplate tests do not account for all circumstances +// and can fail without adjustments, e.g. Float and DateTime types. +// 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) => { + const result = await posts() + + expect(result.length).toEqual(Object.keys(scenario.post).length) + }) + + scenario('returns a single post', async (scenario) => { + 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' }, + }) + + expect(result.title).toEqual('String') + expect(result.body).toEqual('String') + }) + + scenario('updates a post', async (scenario) => { + const original = await post({ id: scenario.post.one.id }) + const result = await updatePost({ + id: original.id, + input: { title: 'String2' }, + }) + + expect(result.title).toEqual('String2') + }) + + scenario('deletes a post', async (scenario) => { + const original = await deletePost({ id: scenario.post.one.id }) + const result = await post({ id: original.id }) + + expect(result).toEqual(null) + }) +}) diff --git a/web/src/App.js b/web/src/App.js index 97fb5e0..5e7beac 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -4,6 +4,7 @@ import { RedwoodApolloProvider } from '@redwoodjs/web/apollo' import FatalErrorPage from 'src/pages/FatalErrorPage' import Routes from 'src/Routes' +import './scaffold.css' import './index.css' const App = () => ( diff --git a/web/src/Routes.js b/web/src/Routes.js index 2c8f02a..d72ab0c 100644 --- a/web/src/Routes.js +++ b/web/src/Routes.js @@ -7,12 +7,24 @@ // 'src/pages/HomePage/HomePage.js' -> HomePage // 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage -import { Router, Route } from '@redwoodjs/router' +import { Router, Route, Set } from '@redwoodjs/router' +import PostsLayout from 'src/layouts/PostsLayout' +import BlogLayout from './layouts/BlogLayout/BlogLayout' const Routes = () => { return ( - + + + + + + + + + + + ) } diff --git a/web/src/components/ArticlesCell/ArticlesCell.js b/web/src/components/ArticlesCell/ArticlesCell.js new file mode 100644 index 0000000..1437f5d --- /dev/null +++ b/web/src/components/ArticlesCell/ArticlesCell.js @@ -0,0 +1,34 @@ +export const QUERY = gql` + query ArticlesQuery { + articles: posts { + id + title + body + createdAt + } + } +` + +export const Loading = () =>
Loading...
+ +export const Empty = () =>
Empty
+ +export const Failure = ({ error }) => ( +
Error: {error.message}
+) + +export const Success = ({ articles }) => { + return ( + + ) +} diff --git a/web/src/components/ArticlesCell/ArticlesCell.mock.js b/web/src/components/ArticlesCell/ArticlesCell.mock.js new file mode 100644 index 0000000..b2b3f1b --- /dev/null +++ b/web/src/components/ArticlesCell/ArticlesCell.mock.js @@ -0,0 +1,4 @@ +// Define your own mock data here: +export const standard = () => ({ + articles: [{ id: 42 }, { id: 43 }, { id: 44 }], +}) diff --git a/web/src/components/ArticlesCell/ArticlesCell.stories.js b/web/src/components/ArticlesCell/ArticlesCell.stories.js new file mode 100644 index 0000000..a3134d3 --- /dev/null +++ b/web/src/components/ArticlesCell/ArticlesCell.stories.js @@ -0,0 +1,20 @@ +import { Loading, Empty, Failure, Success } from './ArticlesCell' +import { standard } from './ArticlesCell.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/ArticlesCell' } diff --git a/web/src/components/ArticlesCell/ArticlesCell.test.js b/web/src/components/ArticlesCell/ArticlesCell.test.js new file mode 100644 index 0000000..5514f36 --- /dev/null +++ b/web/src/components/ArticlesCell/ArticlesCell.test.js @@ -0,0 +1,41 @@ +import { render } from '@redwoodjs/testing/web' +import { Loading, Empty, Failure, Success } from './ArticlesCell' +import { standard } from './ArticlesCell.mock' + +// Generated boilerplate tests do not account for all circumstances +// and can fail without adjustments, e.g. Float and DateTime types. +// Please refer to the RedwoodJS Testing Docs: +// https://redwoodjs.com/docs/testing#testing-cells +// https://redwoodjs.com/docs/testing#jest-expect-type-considerations + +describe('ArticlesCell', () => { + it('renders Loading successfully', () => { + expect(() => { + render() + }).not.toThrow() + }) + + it('renders Empty successfully', async () => { + expect(() => { + render() + }).not.toThrow() + }) + + it('renders Failure successfully', async () => { + expect(() => { + render() + }).not.toThrow() + }) + + // When you're ready to test the actual output of your component render + // you could test that, for example, certain text is present: + // + // 1. import { screen } from '@redwoodjs/testing/web' + // 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument() + + it('renders Success successfully', async () => { + expect(() => { + render() + }).not.toThrow() + }) +}) diff --git a/web/src/components/Post/EditPostCell/EditPostCell.js b/web/src/components/Post/EditPostCell/EditPostCell.js new file mode 100644 index 0000000..663b593 --- /dev/null +++ b/web/src/components/Post/EditPostCell/EditPostCell.js @@ -0,0 +1,59 @@ +import { useMutation } from '@redwoodjs/web' +import { toast } from '@redwoodjs/web/toast' +import { navigate, routes } from '@redwoodjs/router' + +import PostForm from 'src/components/Post/PostForm' + +export const QUERY = gql` + query EditPostById($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 + title + body + createdAt + } + } +` + +export const Loading = () =>
Loading...
+ +export const Failure = ({ error }) => ( +
{error.message}
+) + +export const Success = ({ post }) => { + 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, id) => { + updatePost({ variables: { id, input } }) + } + + return ( +
+
+

Edit Post {post.id}

+
+
+ +
+
+ ) +} diff --git a/web/src/components/Post/NewPost/NewPost.js b/web/src/components/Post/NewPost/NewPost.js new file mode 100644 index 0000000..c31b349 --- /dev/null +++ b/web/src/components/Post/NewPost/NewPost.js @@ -0,0 +1,41 @@ +import { useMutation } from '@redwoodjs/web' +import { toast } from '@redwoodjs/web/toast' +import { navigate, routes } from '@redwoodjs/router' +import PostForm from 'src/components/Post/PostForm' + +const CREATE_POST_MUTATION = 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) => { + createPost({ variables: { input } }) + } + + return ( +
+
+

New Post

+
+
+ +
+
+ ) +} + +export default NewPost diff --git a/web/src/components/Post/Post/Post.js b/web/src/components/Post/Post/Post.js new file mode 100644 index 0000000..b1bf1fc --- /dev/null +++ b/web/src/components/Post/Post/Post.js @@ -0,0 +1,113 @@ +import humanize from 'humanize-string' + +import { useMutation } from '@redwoodjs/web' +import { toast } from '@redwoodjs/web/toast' +import { Link, routes, navigate } from '@redwoodjs/router' + +const DELETE_POST_MUTATION = gql` + mutation DeletePostMutation($id: Int!) { + deletePost(id: $id) { + id + } + } +` + +const formatEnum = (values) => { + if (values) { + if (Array.isArray(values)) { + const humanizedValues = values.map((value) => humanize(value)) + return humanizedValues.join(', ') + } else { + return humanize(values) + } + } +} + +const jsonDisplay = (obj) => { + return ( +
+      {JSON.stringify(obj, null, 2)}
+    
+ ) +} + +const timeTag = (datetime) => { + return ( + datetime && ( + + ) + ) +} + +const checkboxInputTag = (checked) => { + return +} + +const Post = ({ post }) => { + const [deletePost] = useMutation(DELETE_POST_MUTATION, { + onCompleted: () => { + toast.success('Post deleted') + navigate(routes.posts()) + }, + onError: (error) => { + toast.error(error.message) + }, + }) + + 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/Post/PostCell/PostCell.js b/web/src/components/Post/PostCell/PostCell.js new file mode 100644 index 0000000..39674aa --- /dev/null +++ b/web/src/components/Post/PostCell/PostCell.js @@ -0,0 +1,24 @@ +import Post from 'src/components/Post/Post' + +export const QUERY = gql` + query FindPostById($id: Int!) { + post: post(id: $id) { + id + title + body + createdAt + } + } +` + +export const Loading = () =>
Loading...
+ +export const Empty = () =>
Post not found
+ +export const Failure = ({ error }) => ( +
{error.message}
+) + +export const Success = ({ post }) => { + return +} diff --git a/web/src/components/Post/PostForm/PostForm.js b/web/src/components/Post/PostForm/PostForm.js new file mode 100644 index 0000000..250a307 --- /dev/null +++ b/web/src/components/Post/PostForm/PostForm.js @@ -0,0 +1,71 @@ +import { + Form, + FormError, + FieldError, + Label, + TextField, + Submit, +} from '@redwoodjs/forms' + +const PostForm = (props) => { + const onSubmit = (data) => { + props.onSave(data, props?.post?.id) + } + + return ( +
+
+ + + + + + + + + + + + + + +
+ + Save + +
+ +
+ ) +} + +export default PostForm diff --git a/web/src/components/Post/Posts/Posts.js b/web/src/components/Post/Posts/Posts.js new file mode 100644 index 0000000..c0e2be4 --- /dev/null +++ b/web/src/components/Post/Posts/Posts.js @@ -0,0 +1,130 @@ +import humanize from 'humanize-string' + +import { useMutation } from '@redwoodjs/web' +import { toast } from '@redwoodjs/web/toast' +import { Link, routes } from '@redwoodjs/router' + +import { QUERY } from 'src/components/Post/PostsCell' + +const DELETE_POST_MUTATION = gql` + mutation DeletePostMutation($id: Int!) { + deletePost(id: $id) { + id + } + } +` + +const MAX_STRING_LENGTH = 150 + +const formatEnum = (values) => { + if (values) { + if (Array.isArray(values)) { + const humanizedValues = values.map((value) => humanize(value)) + return humanizedValues.join(', ') + } else { + return humanize(values) + } + } +} + +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 ( + datetime && ( + + ) + ) +} + +const checkboxInputTag = (checked) => { + return +} + +const PostsList = ({ posts }) => { + 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) => { + if (confirm('Are you sure you want to delete post ' + id + '?')) { + deletePost({ variables: { id } }) + } + } + + return ( +
+ + + + + + + + + + + + {posts.map((post) => ( + + + + + + + + ))} + +
IdTitleBodyCreated at 
{truncate(post.id)}{truncate(post.title)}{truncate(post.body)}{timeTag(post.createdAt)} + +
+
+ ) +} + +export default PostsList diff --git a/web/src/components/Post/PostsCell/PostsCell.js b/web/src/components/Post/PostsCell/PostsCell.js new file mode 100644 index 0000000..ff350d3 --- /dev/null +++ b/web/src/components/Post/PostsCell/PostsCell.js @@ -0,0 +1,35 @@ +import { Link, routes } from '@redwoodjs/router' + +import Posts from 'src/components/Post/Posts' + +export const QUERY = gql` + query FindPosts { + posts { + id + title + body + createdAt + } + } +` + +export const Loading = () =>
Loading...
+ +export const Empty = () => { + return ( +
+ {'No posts yet. '} + + {'Create one?'} + +
+ ) +} + +export const Failure = ({ error }) => ( +
{error.message}
+) + +export const Success = ({ posts }) => { + return +} diff --git a/web/src/index.css b/web/src/index.css index e69de29..cebc98a 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -0,0 +1,50 @@ +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; +} +ul { + list-style-type: none; + margin: 1rem 0; + padding: 0; +} +li { + display: inline-block; + margin: 0 1rem 0 0 ; +} +h1 > a { + text-decoration: none; + color: black; +} +button, input, label, textarea { + display: block; + outline: none; +} +label { + margin-top: 1rem; +} +.error { + color: red; +} +input.error, textarea.error { + border: 1px solid red; +} +.form-error { + color: red; + background-color: lavenderblush; + padding: 1rem; + display: inline-block; +} +.form-error ul { + list-style-type: disc; + margin: 1rem; + padding: 1rem; +} +.form-error li { + display: list-item; +} +.flex-between { + display: flex; + justify-content: space-between; +} +.flex-between button { + display: inline; +} \ No newline at end of file diff --git a/web/src/layouts/BlogLayout/BlogLayout.js b/web/src/layouts/BlogLayout/BlogLayout.js new file mode 100644 index 0000000..103a755 --- /dev/null +++ b/web/src/layouts/BlogLayout/BlogLayout.js @@ -0,0 +1,23 @@ +import { Link, routes } from '@redwoodjs/router'; + +const BlogLayout = ({ children }) => { + return <> +
+

+ Colin's Redwood Blog +

+ +
+
{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..f1ebed5 --- /dev/null +++ b/web/src/layouts/BlogLayout/BlogLayout.test.js @@ -0,0 +1,14 @@ +import { render } from '@redwoodjs/testing/web' + +import BlogLayout from './BlogLayout' + +// Improve this test with help from the Redwood Testing Doc: +// https://redwoodjs.com/docs/testing#testing-pages-layouts + +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..bad7cb7 --- /dev/null +++ b/web/src/layouts/PostsLayout/PostsLayout.js @@ -0,0 +1,23 @@ +import { Link, routes } from '@redwoodjs/router' +import { Toaster } from '@redwoodjs/web/toast' + +const PostsLayout = ({ children }) => { + return ( +
+ +
+

+ + Posts + +

+ +
+
New Post + +
+
{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..50a6e5d --- /dev/null +++ b/web/src/pages/AboutPage/AboutPage.js @@ -0,0 +1,14 @@ +import { MetaTags } from '@redwoodjs/web' + +const AboutPage = () => { + return ( + <> + +

+ This site was created to demonstrate my mastery of Redwood: Look on my works, ye mighty, and despair! +

+ + ) +} + +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..571b85e --- /dev/null +++ b/web/src/pages/AboutPage/AboutPage.test.js @@ -0,0 +1,14 @@ +import { render } from '@redwoodjs/testing/web' + +import AboutPage from './AboutPage' + +// Improve this test with help from the Redwood Testing Doc: +// https://redwoodjs.com/docs/testing#testing-pages-layouts + +describe('AboutPage', () => { + it('renders successfully', () => { + expect(() => { + render() + }).not.toThrow() + }) +}) diff --git a/web/src/pages/HomePage/HomePage.js b/web/src/pages/HomePage/HomePage.js new file mode 100644 index 0000000..418ec64 --- /dev/null +++ b/web/src/pages/HomePage/HomePage.js @@ -0,0 +1,13 @@ +import { MetaTags } from '@redwoodjs/web' +import ArticlesCell from 'src/components/ArticlesCell' + +const HomePage = () => { + return ( + <> + + + + ) +} + +export default HomePage 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..c684c7a --- /dev/null +++ b/web/src/pages/HomePage/HomePage.test.js @@ -0,0 +1,14 @@ +import { render } from '@redwoodjs/testing/web' + +import HomePage from './HomePage' + +// Improve this test with help from the Redwood Testing Doc: +// https://redwoodjs.com/docs/testing#testing-pages-layouts + +describe('HomePage', () => { + it('renders successfully', () => { + expect(() => { + render() + }).not.toThrow() + }) +}) diff --git a/web/src/pages/Post/EditPostPage/EditPostPage.js b/web/src/pages/Post/EditPostPage/EditPostPage.js new file mode 100644 index 0000000..5147fe8 --- /dev/null +++ b/web/src/pages/Post/EditPostPage/EditPostPage.js @@ -0,0 +1,7 @@ +import EditPostCell from 'src/components/Post/EditPostCell' + +const EditPostPage = ({ id }) => { + return +} + +export default EditPostPage diff --git a/web/src/pages/Post/NewPostPage/NewPostPage.js b/web/src/pages/Post/NewPostPage/NewPostPage.js new file mode 100644 index 0000000..0b3c453 --- /dev/null +++ b/web/src/pages/Post/NewPostPage/NewPostPage.js @@ -0,0 +1,7 @@ +import NewPost from 'src/components/Post/NewPost' + +const NewPostPage = () => { + return +} + +export default NewPostPage diff --git a/web/src/pages/Post/PostPage/PostPage.js b/web/src/pages/Post/PostPage/PostPage.js new file mode 100644 index 0000000..70070fc --- /dev/null +++ b/web/src/pages/Post/PostPage/PostPage.js @@ -0,0 +1,7 @@ +import PostCell from 'src/components/Post/PostCell' + +const PostPage = ({ id }) => { + return +} + +export default PostPage diff --git a/web/src/pages/Post/PostsPage/PostsPage.js b/web/src/pages/Post/PostsPage/PostsPage.js new file mode 100644 index 0000000..f5b3668 --- /dev/null +++ b/web/src/pages/Post/PostsPage/PostsPage.js @@ -0,0 +1,7 @@ +import PostsCell from 'src/components/Post/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..30928ab --- /dev/null +++ b/web/src/scaffold.css @@ -0,0 +1,366 @@ +/* + normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css +*/ + +.rw-scaffold *, +.rw-scaffold ::after, +.rw-scaffold ::before { + box-sizing: inherit; +} +.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, +.rw-toast { + 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 { + border-radius: 0.5rem; + overflow: hidden; + width: 100%; +} +.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-forgot-link { + font-size: 0.75rem; + color: #a0aec0; + text-align: right; + margin-top: 0.1rem; +} +.rw-forgot-link:hover { + font-size: 0.75rem; + color: #4299e1; +} +.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-cell-error { + font-size: 90%; + font-weight: 600; +} +.rw-form-wrapper { + box-sizing: border-box; + font-size: 0.875rem; + margin-top: -1rem; +} +.rw-cell-error, +.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 { + border: none; + 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; + border: 0; +} +.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-style: solid; + border-color: #e2e8f0; + color: #4a5568; + border-radius: 0.25rem; + outline: none; +} +.rw-check-radio-item-none { + color: #4a5568; +} +.rw-check-radio-items { + display: flex; + justify-items: center; +} +.rw-input[type='checkbox'] { + display: inline; + width: 1rem; + margin-left: 0; + margin-right: 0.5rem; + margin-top: 0.25rem; +} +.rw-input[type='radio'] { + display: inline; + width: 1rem; + margin-left: 0; + margin-right: 0.5rem; + margin-top: 0.25rem; +} +.rw-input:focus { + border-color: #a0aec0; +} +.rw-input-error { + border-color: #c53030; + color: #c53030; +} + +.rw-input-error:focus { + outline: none; + border-color: #c53030; + box-shadow: 0 0 5px #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 td { + background-color: #ffffff; + color: #1a202c; +} +.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-login-container { + display: flex; + align-items: center; + justify-content: center; + width: 24rem; + margin: 4rem auto; + flex-wrap: wrap; +} +.rw-login-container .rw-form-wrapper { + width: 100%; +} +.rw-login-link { + margin-top: 1rem; + color: #4a5568; + font-size: 90%; + text-align: center; + flex-basis: 100%; +}