diff --git a/api/prisma/migrations/20201007174432-create-posts/README.md b/api/prisma/migrations/20201007174432-create-posts/README.md
new file mode 100644
index 0000000..a706071
--- /dev/null
+++ b/api/prisma/migrations/20201007174432-create-posts/README.md
@@ -0,0 +1,45 @@
+# Migration `20201007174432-create-posts`
+
+This migration has been generated by Rob Cameron at 10/7/2020, 10:44:32 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 ..20201007174432-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/20201007174432-create-posts/schema.prisma b/api/prisma/migrations/20201007174432-create-posts/schema.prisma
new file mode 100644
index 0000000..624cbdb
--- /dev/null
+++ b/api/prisma/migrations/20201007174432-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/20201007174432-create-posts/steps.json b/api/prisma/migrations/20201007174432-create-posts/steps.json
new file mode 100644
index 0000000..f2edc88
--- /dev/null
+++ b/api/prisma/migrations/20201007174432-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/20201007180930-create-contact/README.md b/api/prisma/migrations/20201007180930-create-contact/README.md
new file mode 100644
index 0000000..44d90a8
--- /dev/null
+++ b/api/prisma/migrations/20201007180930-create-contact/README.md
@@ -0,0 +1,50 @@
+# Migration `20201007180930-create-contact`
+
+This migration has been generated by Rob Cameron at 10/7/2020, 11:09:30 AM.
+You can check out the [state of the schema](./schema.prisma) after the migration.
+
+## Database Steps
+
+```sql
+CREATE TABLE "Contact" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "name" TEXT NOT NULL,
+ "email" TEXT NOT NULL,
+ "message" TEXT NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
+)
+```
+
+## Changes
+
+```diff
+diff --git schema.prisma schema.prisma
+migration 20201007174432-create-posts..20201007180930-create-contact
+--- datamodel.dml
++++ datamodel.dml
+@@ -1,9 +1,9 @@
+ datasource DS {
+ // optionally set multiple providers
+ // example: provider = ["sqlite", "postgresql"]
+ provider = "sqlite"
+- url = "***"
++ url = "***"
+ }
+ generator client {
+ provider = "prisma-client-js"
+@@ -15,4 +15,12 @@
+ title String
+ body String
+ createdAt DateTime @default(now())
+ }
++
++model Contact {
++ id Int @id @default(autoincrement())
++ name String
++ email String
++ message String
++ createdAt DateTime @default(now())
++}
+```
+
+
diff --git a/api/prisma/migrations/20201007180930-create-contact/schema.prisma b/api/prisma/migrations/20201007180930-create-contact/schema.prisma
new file mode 100644
index 0000000..f983e1e
--- /dev/null
+++ b/api/prisma/migrations/20201007180930-create-contact/schema.prisma
@@ -0,0 +1,26 @@
+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())
+}
+
+model Contact {
+ id Int @id @default(autoincrement())
+ name String
+ email String
+ message String
+ createdAt DateTime @default(now())
+}
diff --git a/api/prisma/migrations/20201007180930-create-contact/steps.json b/api/prisma/migrations/20201007180930-create-contact/steps.json
new file mode 100644
index 0000000..0ca4b72
--- /dev/null
+++ b/api/prisma/migrations/20201007180930-create-contact/steps.json
@@ -0,0 +1,105 @@
+{
+ "version": "0.3.14-fixed",
+ "steps": [
+ {
+ "tag": "CreateModel",
+ "model": "Contact"
+ },
+ {
+ "tag": "CreateField",
+ "model": "Contact",
+ "field": "id",
+ "type": "Int",
+ "arity": "Required"
+ },
+ {
+ "tag": "CreateDirective",
+ "location": {
+ "path": {
+ "tag": "Field",
+ "model": "Contact",
+ "field": "id"
+ },
+ "directive": "id"
+ }
+ },
+ {
+ "tag": "CreateDirective",
+ "location": {
+ "path": {
+ "tag": "Field",
+ "model": "Contact",
+ "field": "id"
+ },
+ "directive": "default"
+ }
+ },
+ {
+ "tag": "CreateArgument",
+ "location": {
+ "tag": "Directive",
+ "path": {
+ "tag": "Field",
+ "model": "Contact",
+ "field": "id"
+ },
+ "directive": "default"
+ },
+ "argument": "",
+ "value": "autoincrement()"
+ },
+ {
+ "tag": "CreateField",
+ "model": "Contact",
+ "field": "name",
+ "type": "String",
+ "arity": "Required"
+ },
+ {
+ "tag": "CreateField",
+ "model": "Contact",
+ "field": "email",
+ "type": "String",
+ "arity": "Required"
+ },
+ {
+ "tag": "CreateField",
+ "model": "Contact",
+ "field": "message",
+ "type": "String",
+ "arity": "Required"
+ },
+ {
+ "tag": "CreateField",
+ "model": "Contact",
+ "field": "createdAt",
+ "type": "DateTime",
+ "arity": "Required"
+ },
+ {
+ "tag": "CreateDirective",
+ "location": {
+ "path": {
+ "tag": "Field",
+ "model": "Contact",
+ "field": "createdAt"
+ },
+ "directive": "default"
+ }
+ },
+ {
+ "tag": "CreateArgument",
+ "location": {
+ "tag": "Directive",
+ "path": {
+ "tag": "Field",
+ "model": "Contact",
+ "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..7d72c0b
--- /dev/null
+++ b/api/prisma/migrations/migrate.lock
@@ -0,0 +1,4 @@
+# Prisma Migrate lockfile v1
+
+20201007174432-create-posts
+20201007180930-create-contact
\ No newline at end of file
diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma
index d2cf1e8..1288fdf 100644
--- a/api/prisma/schema.prisma
+++ b/api/prisma/schema.prisma
@@ -10,11 +10,17 @@ 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())
+}
+
+model Contact {
+ id Int @id @default(autoincrement())
+ name String
+ email String
+ message String
+ createdAt DateTime @default(now())
}
diff --git a/api/src/graphql/contacts.sdl.js b/api/src/graphql/contacts.sdl.js
new file mode 100644
index 0000000..c4153b6
--- /dev/null
+++ b/api/src/graphql/contacts.sdl.js
@@ -0,0 +1,29 @@
+export const schema = gql`
+ type Contact {
+ id: Int!
+ name: String!
+ email: String!
+ message: String!
+ createdAt: DateTime!
+ }
+
+ type Query {
+ contacts: [Contact!]!
+ }
+
+ input CreateContactInput {
+ name: String!
+ email: String!
+ message: String!
+ }
+
+ input UpdateContactInput {
+ name: String
+ email: String
+ message: String
+ }
+
+ type Mutation {
+ createContact(input: CreateContactInput!): Contact
+ }
+`
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/contacts/contacts.js b/api/src/services/contacts/contacts.js
new file mode 100644
index 0000000..b36cb0f
--- /dev/null
+++ b/api/src/services/contacts/contacts.js
@@ -0,0 +1,21 @@
+import { db } from 'src/lib/db'
+import { UserInputError } from '@redwoodjs/api'
+
+const validate = (input) => {
+ if (input.email && !input.email.match(/[^@]+@[^.]+\..+/)) {
+ throw new UserInputError("Can't create new contact",{
+ messages: {
+ email: ['is not formatted like an email address'],
+ },
+ })
+ }
+}
+
+export const contacts = () => {
+ return db.contact.findMany()
+}
+
+export const createContact = ({ input }) => {
+ validate(input)
+ return db.contact.create({ data: input })
+}
diff --git a/api/src/services/contacts/contacts.test.js b/api/src/services/contacts/contacts.test.js
new file mode 100644
index 0000000..fbbbc1e
--- /dev/null
+++ b/api/src/services/contacts/contacts.test.js
@@ -0,0 +1,9 @@
+/*
+import { contacts } from './contacts'
+*/
+
+describe('contacts', () => {
+ it('returns true', () => {
+ expect(true).toBe(true)
+ })
+})
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..a05d885 100644
--- a/web/src/Routes.js
+++ b/web/src/Routes.js
@@ -12,6 +12,14 @@ import { Router, Route } from '@redwoodjs/router'
const Routes = () => {
return (
+
+
+
+
+
+
+
+
)
diff --git a/web/src/components/BlogPost/BlogPost.js b/web/src/components/BlogPost/BlogPost.js
new file mode 100644
index 0000000..fe0979b
--- /dev/null
+++ b/web/src/components/BlogPost/BlogPost.js
@@ -0,0 +1,16 @@
+import { Link,routes } from '@redwoodjs/router'
+
+const BlogPost = ({ post }) => {
+ return (
+
+
+ {post.body}
+
+ )
+}
+
+export default BlogPost
diff --git a/web/src/components/BlogPost/BlogPost.stories.js b/web/src/components/BlogPost/BlogPost.stories.js
new file mode 100644
index 0000000..03e9f5f
--- /dev/null
+++ b/web/src/components/BlogPost/BlogPost.stories.js
@@ -0,0 +1,7 @@
+import BlogPost from './BlogPost'
+
+export const generated = () => {
+ return
+}
+
+export default { title: 'Components/BlogPost' }
diff --git a/web/src/components/BlogPost/BlogPost.test.js b/web/src/components/BlogPost/BlogPost.test.js
new file mode 100644
index 0000000..87f976f
--- /dev/null
+++ b/web/src/components/BlogPost/BlogPost.test.js
@@ -0,0 +1,11 @@
+import { render } from '@redwoodjs/testing'
+
+import BlogPost from './BlogPost'
+
+describe('BlogPost', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render()
+ }).not.toThrow()
+ })
+})
diff --git a/web/src/components/BlogPostCell/BlogPostCell.js b/web/src/components/BlogPostCell/BlogPostCell.js
new file mode 100644
index 0000000..fd7987f
--- /dev/null
+++ b/web/src/components/BlogPostCell/BlogPostCell.js
@@ -0,0 +1,22 @@
+import BlogPost from 'src/components/BlogPost'
+
+export const QUERY = gql`
+ query BlogPostQuery($id: Int!) {
+ post(id: $id) {
+ id
+ title
+ body
+ createdAt
+ }
+ }
+`
+
+export const Loading = () =>
Loading...
+
+export const Empty = () => Empty
+
+export const Failure = ({ error }) => Error: {error.message}
+
+export const Success = ({ post }) => {
+ return
+}
diff --git a/web/src/components/BlogPostCell/BlogPostCell.mock.js b/web/src/components/BlogPostCell/BlogPostCell.mock.js
new file mode 100644
index 0000000..c689622
--- /dev/null
+++ b/web/src/components/BlogPostCell/BlogPostCell.mock.js
@@ -0,0 +1,6 @@
+// Define your own mock data here:
+export const standard = (/* vars, { ctx, req } */) => ({
+ blogPost: {
+ id: 42,
+ },
+})
diff --git a/web/src/components/BlogPostCell/BlogPostCell.stories.js b/web/src/components/BlogPostCell/BlogPostCell.stories.js
new file mode 100644
index 0000000..597ce5f
--- /dev/null
+++ b/web/src/components/BlogPostCell/BlogPostCell.stories.js
@@ -0,0 +1,20 @@
+import { Loading, Empty, Failure, Success } from './BlogPostCell'
+import { standard } from './BlogPostCell.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/BlogPostCell' }
diff --git a/web/src/components/BlogPostCell/BlogPostCell.test.js b/web/src/components/BlogPostCell/BlogPostCell.test.js
new file mode 100644
index 0000000..61adcd6
--- /dev/null
+++ b/web/src/components/BlogPostCell/BlogPostCell.test.js
@@ -0,0 +1,26 @@
+import { render, screen } from '@redwoodjs/testing'
+import { Loading, Empty, Failure, Success } from './BlogPostCell'
+import { standard } from './BlogPostCell.mock'
+
+describe('BlogPostCell', () => {
+ 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/BlogPostsCell/BlogPostsCell.js b/web/src/components/BlogPostsCell/BlogPostsCell.js
new file mode 100644
index 0000000..fd44b4b
--- /dev/null
+++ b/web/src/components/BlogPostsCell/BlogPostsCell.js
@@ -0,0 +1,23 @@
+import { Link,routes } from '@redwoodjs/router'
+import BlogPost from 'src/components/BlogPost'
+
+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) => )
+}
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.css b/web/src/index.css
index e69de29..d3118a1 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -0,0 +1,16 @@
+button, input, label, textarea {
+ display: block;
+ outline: none;
+}
+
+label {
+ margin-top: 1rem;
+}
+
+.error {
+ color: red;
+}
+
+input.error, textarea.error {
+ border: 1px solid red;
+}
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..8f62ab4
--- /dev/null
+++ b/web/src/layouts/BlogLayout/BlogLayout.js
@@ -0,0 +1,26 @@
+import { Link,routes } from '@redwoodjs/router'
+
+const BlogLayout = ({ children }) => {
+ return (
+ <>
+
+
+ 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..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..59c059a
--- /dev/null
+++ b/web/src/pages/AboutPage/AboutPage.js
@@ -0,0 +1,15 @@
+import { Link,routes } from '@redwoodjs/router'
+import BlogLayout from 'src/layouts/BlogLayout'
+
+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..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/BlogPostPage/BlogPostPage.js b/web/src/pages/BlogPostPage/BlogPostPage.js
new file mode 100644
index 0000000..d297e20
--- /dev/null
+++ b/web/src/pages/BlogPostPage/BlogPostPage.js
@@ -0,0 +1,12 @@
+import BlogLayout from 'src/layouts/BlogLayout'
+import BlogPostCell from 'src/components/BlogPostCell'
+
+const BlogPostPage = ({ id }) => {
+ return (
+
+
+
+ )
+}
+
+export default BlogPostPage
diff --git a/web/src/pages/BlogPostPage/BlogPostPage.stories.js b/web/src/pages/BlogPostPage/BlogPostPage.stories.js
new file mode 100644
index 0000000..5e9fa74
--- /dev/null
+++ b/web/src/pages/BlogPostPage/BlogPostPage.stories.js
@@ -0,0 +1,7 @@
+import BlogPostPage from './BlogPostPage'
+
+export const generated = () => {
+ return
+}
+
+export default { title: 'Pages/BlogPostPage' }
diff --git a/web/src/pages/BlogPostPage/BlogPostPage.test.js b/web/src/pages/BlogPostPage/BlogPostPage.test.js
new file mode 100644
index 0000000..cb14ea6
--- /dev/null
+++ b/web/src/pages/BlogPostPage/BlogPostPage.test.js
@@ -0,0 +1,11 @@
+import { render } from '@redwoodjs/testing'
+
+import BlogPostPage from './BlogPostPage'
+
+describe('BlogPostPage', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render()
+ }).not.toThrow()
+ })
+})
diff --git a/web/src/pages/ContactPage/ContactPage.js b/web/src/pages/ContactPage/ContactPage.js
new file mode 100644
index 0000000..e239fef
--- /dev/null
+++ b/web/src/pages/ContactPage/ContactPage.js
@@ -0,0 +1,95 @@
+import {
+ Form,
+ TextField,
+ TextAreaField,
+ Submit,
+ FieldError,
+ Label,
+ FormError,
+} from '@redwoodjs/forms'
+import { Flash,useFlash,useMutation } from '@redwoodjs/web'
+import { useForm } from 'react-hook-form'
+import BlogLayout from 'src/layouts/BlogLayout'
+
+const CREATE_CONTACT = gql`
+ mutation CreateContactMutation($input: CreateContactInput!) {
+ createContact(input: $input) {
+ id
+ }
+ }
+`
+
+const ContactPage = () => {
+ const formMethods = useForm()
+ const { addMessage } = useFlash()
+
+ const [create,{ loading,error }] = useMutation(CREATE_CONTACT,{
+ onCompleted: () => {
+ addMessage('Thank you for your submission!',{
+ style: { backgroundColor: 'green',color: 'white',padding: '1rem' },
+ })
+ formMethods.reset()
+ },
+ })
+
+ const onSubmit = (data) => {
+ create({ variables: { input: data } })
+ console.log(data)
+ }
+
+ return (
+
+
+
+
+ )
+}
+
+export default ContactPage
diff --git a/web/src/pages/ContactPage/ContactPage.stories.js b/web/src/pages/ContactPage/ContactPage.stories.js
new file mode 100644
index 0000000..0894c0b
--- /dev/null
+++ b/web/src/pages/ContactPage/ContactPage.stories.js
@@ -0,0 +1,7 @@
+import ContactPage from './ContactPage'
+
+export const generated = () => {
+ return
+}
+
+export default { title: 'Pages/ContactPage' }
diff --git a/web/src/pages/ContactPage/ContactPage.test.js b/web/src/pages/ContactPage/ContactPage.test.js
new file mode 100644
index 0000000..d31f442
--- /dev/null
+++ b/web/src/pages/ContactPage/ContactPage.test.js
@@ -0,0 +1,11 @@
+import { render } from '@redwoodjs/testing'
+
+import ContactPage from './ContactPage'
+
+describe('ContactPage', () => {
+ 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..50fa6f2
--- /dev/null
+++ b/web/src/pages/HomePage/HomePage.js
@@ -0,0 +1,12 @@
+import BlogLayout from 'src/layouts/BlogLayout'
+import BlogPostsCell from 'src/components/BlogPostsCell'
+
+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..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;
+ }
+}