bunch of stuff
This commit is contained in:
45
api/prisma/migrations/20201009213512-create-posts/README.md
Normal file
45
api/prisma/migrations/20201009213512-create-posts/README.md
Normal file
@@ -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())
|
||||||
|
+}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
120
api/prisma/migrations/20201009213512-create-posts/steps.json
Normal file
120
api/prisma/migrations/20201009213512-create-posts/steps.json
Normal file
@@ -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()"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
3
api/prisma/migrations/migrate.lock
Normal file
3
api/prisma/migrations/migrate.lock
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Prisma Migrate lockfile v1
|
||||||
|
|
||||||
|
20201009213512-create-posts
|
||||||
@@ -10,11 +10,9 @@ generator client {
|
|||||||
binaryTargets = "native"
|
binaryTargets = "native"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define your own datamodels here and run `yarn redwood db save` to create
|
model Post {
|
||||||
// migrations for them.
|
|
||||||
// TODO: Please remove the following example:
|
|
||||||
model UserExample {
|
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
email String @unique
|
title String
|
||||||
name String?
|
body String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
}
|
}
|
||||||
|
|||||||
29
api/src/graphql/posts.sdl.js
Normal file
29
api/src/graphql/posts.sdl.js
Normal file
@@ -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!
|
||||||
|
}
|
||||||
|
`
|
||||||
30
api/src/services/posts/posts.js
Normal file
30
api/src/services/posts/posts.js
Normal file
@@ -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 },
|
||||||
|
})
|
||||||
|
}
|
||||||
9
api/src/services/posts/posts.test.js
Normal file
9
api/src/services/posts/posts.test.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/*
|
||||||
|
import { posts } from './posts'
|
||||||
|
*/
|
||||||
|
|
||||||
|
describe('posts', () => {
|
||||||
|
it('returns true', () => {
|
||||||
|
expect(true).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -12,6 +12,12 @@ import { Router, Route } from '@redwoodjs/router'
|
|||||||
const Routes = () => {
|
const Routes = () => {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
|
<Route path="/posts/new" page={NewPostPage} name="newPost" />
|
||||||
|
<Route path="/posts/{id:Int}/edit" page={EditPostPage} name="editPost" />
|
||||||
|
<Route path="/posts/{id:Int}" page={PostPage} name="post" />
|
||||||
|
<Route path="/posts" page={PostsPage} name="posts" />
|
||||||
|
<Route path="/about" page={AboutPage} name="about" />
|
||||||
|
<Route path="/" page={HomePage} name="home" />
|
||||||
<Route notfound page={NotFoundPage} />
|
<Route notfound page={NotFoundPage} />
|
||||||
</Router>
|
</Router>
|
||||||
)
|
)
|
||||||
|
|||||||
28
web/src/components/BlogPostsCell/BlogPostsCell.js
Normal file
28
web/src/components/BlogPostsCell/BlogPostsCell.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
export const QUERY = gql`
|
||||||
|
query BlogPostsQuery {
|
||||||
|
posts {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
body
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const Loading = () => <div>Loading...</div>
|
||||||
|
|
||||||
|
export const Empty = () => <div>Empty</div>
|
||||||
|
|
||||||
|
export const Failure = ({ error }) => <div>Error: {error.message}</div>
|
||||||
|
|
||||||
|
export const Success = ({ posts }) => {
|
||||||
|
return posts.map((post) => (
|
||||||
|
<article key={post.id}>
|
||||||
|
<header>
|
||||||
|
<h2>{post.title}</h2>
|
||||||
|
</header>
|
||||||
|
<p>{post.body}</p>
|
||||||
|
<div>Posted on: {post.createdAt.split('T')[0]}</div>
|
||||||
|
</article>
|
||||||
|
))
|
||||||
|
}
|
||||||
6
web/src/components/BlogPostsCell/BlogPostsCell.mock.js
Normal file
6
web/src/components/BlogPostsCell/BlogPostsCell.mock.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// Define your own mock data here:
|
||||||
|
export const standard = (/* vars, { ctx, req } */) => ({
|
||||||
|
blogPosts: {
|
||||||
|
id: 42,
|
||||||
|
},
|
||||||
|
})
|
||||||
20
web/src/components/BlogPostsCell/BlogPostsCell.stories.js
Normal file
20
web/src/components/BlogPostsCell/BlogPostsCell.stories.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { Loading, Empty, Failure, Success } from './BlogPostsCell'
|
||||||
|
import { standard } from './BlogPostsCell.mock'
|
||||||
|
|
||||||
|
export const loading = () => {
|
||||||
|
return Loading ? <Loading /> : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const empty = () => {
|
||||||
|
return Empty ? <Empty /> : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const failure = () => {
|
||||||
|
return Failure ? <Failure error={new Error('Oh no')} /> : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const success = () => {
|
||||||
|
return Success ? <Success {...standard()} /> : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { title: 'Cells/BlogPostsCell' }
|
||||||
26
web/src/components/BlogPostsCell/BlogPostsCell.test.js
Normal file
26
web/src/components/BlogPostsCell/BlogPostsCell.test.js
Normal file
@@ -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(<Loading />)
|
||||||
|
// Use screen.debug() to see output
|
||||||
|
expect(screen.getByText('Loading...')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Empty renders successfully', async () => {
|
||||||
|
render(<Empty />)
|
||||||
|
expect(screen.getByText('Empty')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Failure renders successfully', async () => {
|
||||||
|
render(<Failure error={new Error('Oh no')} />)
|
||||||
|
expect(screen.getByText(/Oh no/i)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Success renders successfully', async () => {
|
||||||
|
render(<Success blogPosts={standard().blogPosts} />)
|
||||||
|
expect(screen.getByText(/42/i)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
48
web/src/components/EditPostCell/EditPostCell.js
Normal file
48
web/src/components/EditPostCell/EditPostCell.js
Normal file
@@ -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 = () => <div>Loading...</div>
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="rw-segment">
|
||||||
|
<header className="rw-segment-header">
|
||||||
|
<h2 className="rw-heading rw-heading-secondary">Edit Post {post.id}</h2>
|
||||||
|
</header>
|
||||||
|
<div className="rw-segment-main">
|
||||||
|
<PostForm post={post} onSave={onSave} error={error} loading={loading} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
38
web/src/components/NewPost/NewPost.js
Normal file
38
web/src/components/NewPost/NewPost.js
Normal file
@@ -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 (
|
||||||
|
<div className="rw-segment">
|
||||||
|
<header className="rw-segment-header">
|
||||||
|
<h2 className="rw-heading rw-heading-secondary">New Post</h2>
|
||||||
|
</header>
|
||||||
|
<div className="rw-segment-main">
|
||||||
|
<PostForm onSave={onSave} loading={loading} error={error} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NewPost
|
||||||
95
web/src/components/Post/Post.js
Normal file
95
web/src/components/Post/Post.js
Normal file
@@ -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 (
|
||||||
|
<pre>
|
||||||
|
<code>{JSON.stringify(obj, null, 2)}</code>
|
||||||
|
</pre>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeTag = (datetime) => {
|
||||||
|
return (
|
||||||
|
<time dateTime={datetime} title={datetime}>
|
||||||
|
{new Date(datetime).toUTCString()}
|
||||||
|
</time>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkboxInputTag = (checked) => {
|
||||||
|
return <input type="checkbox" checked={checked} disabled />
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<div className="rw-segment">
|
||||||
|
<header className="rw-segment-header">
|
||||||
|
<h2 className="rw-heading rw-heading-secondary">
|
||||||
|
Post {post.id} Detail
|
||||||
|
</h2>
|
||||||
|
</header>
|
||||||
|
<table className="rw-table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>Id</th>
|
||||||
|
<td>{post.id}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Title</th>
|
||||||
|
<td>{post.title}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Body</th>
|
||||||
|
<td>{post.body}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Created at</th>
|
||||||
|
<td>{timeTag(post.createdAt)}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<nav className="rw-button-group">
|
||||||
|
<Link
|
||||||
|
to={routes.editPost({ id: post.id })}
|
||||||
|
className="rw-button rw-button-blue"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Link>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="rw-button rw-button-red"
|
||||||
|
onClick={() => onDeleteClick(post.id)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Post
|
||||||
20
web/src/components/PostCell/PostCell.js
Normal file
20
web/src/components/PostCell/PostCell.js
Normal file
@@ -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 = () => <div>Loading...</div>
|
||||||
|
|
||||||
|
export const Empty = () => <div>Post not found</div>
|
||||||
|
|
||||||
|
export const Success = ({ post }) => {
|
||||||
|
return <Post post={post} />
|
||||||
|
}
|
||||||
67
web/src/components/PostForm/PostForm.js
Normal file
67
web/src/components/PostForm/PostForm.js
Normal file
@@ -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 (
|
||||||
|
<div className="rw-form-wrapper">
|
||||||
|
<Form onSubmit={onSubmit} error={props.error}>
|
||||||
|
<FormError
|
||||||
|
error={props.error}
|
||||||
|
wrapperClassName="rw-form-error-wrapper"
|
||||||
|
titleClassName="rw-form-error-title"
|
||||||
|
listClassName="rw-form-error-list"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Label
|
||||||
|
name="title"
|
||||||
|
className="rw-label"
|
||||||
|
errorClassName="rw-label rw-label-error"
|
||||||
|
>
|
||||||
|
Title
|
||||||
|
</Label>
|
||||||
|
<TextField
|
||||||
|
name="title"
|
||||||
|
defaultValue={props.post?.title}
|
||||||
|
className="rw-input"
|
||||||
|
errorClassName="rw-input rw-input-error"
|
||||||
|
validation={{ required: true }}
|
||||||
|
/>
|
||||||
|
<FieldError name="title" className="rw-field-error" />
|
||||||
|
|
||||||
|
<Label
|
||||||
|
name="body"
|
||||||
|
className="rw-label"
|
||||||
|
errorClassName="rw-label rw-label-error"
|
||||||
|
>
|
||||||
|
Body
|
||||||
|
</Label>
|
||||||
|
<TextField
|
||||||
|
name="body"
|
||||||
|
defaultValue={props.post?.body}
|
||||||
|
className="rw-input"
|
||||||
|
errorClassName="rw-input rw-input-error"
|
||||||
|
validation={{ required: true }}
|
||||||
|
/>
|
||||||
|
<FieldError name="body" className="rw-field-error" />
|
||||||
|
|
||||||
|
<div className="rw-button-group">
|
||||||
|
<Submit disabled={props.loading} className="rw-button rw-button-blue">
|
||||||
|
Save
|
||||||
|
</Submit>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PostForm
|
||||||
105
web/src/components/Posts/Posts.js
Normal file
105
web/src/components/Posts/Posts.js
Normal file
@@ -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 (
|
||||||
|
<time dateTime={datetime} title={datetime}>
|
||||||
|
{new Date(datetime).toUTCString()}
|
||||||
|
</time>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkboxInputTag = (checked) => {
|
||||||
|
return <input type="checkbox" checked={checked} disabled />
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="rw-segment rw-table-wrapper-responsive">
|
||||||
|
<table className="rw-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Id</th>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Body</th>
|
||||||
|
<th>Created at</th>
|
||||||
|
<th> </th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{posts.map((post) => (
|
||||||
|
<tr key={post.id}>
|
||||||
|
<td>{truncate(post.id)}</td>
|
||||||
|
<td>{truncate(post.title)}</td>
|
||||||
|
<td>{truncate(post.body)}</td>
|
||||||
|
<td>{timeTag(post.createdAt)}</td>
|
||||||
|
<td>
|
||||||
|
<nav className="rw-table-actions">
|
||||||
|
<Link
|
||||||
|
to={routes.post({ id: post.id })}
|
||||||
|
title={'Show post ' + post.id + ' detail'}
|
||||||
|
className="rw-button rw-button-small"
|
||||||
|
>
|
||||||
|
Show
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to={routes.editPost({ id: post.id })}
|
||||||
|
title={'Edit post ' + post.id}
|
||||||
|
className="rw-button rw-button-small rw-button-blue"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Link>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
title={'Delete post ' + post.id}
|
||||||
|
className="rw-button rw-button-small rw-button-red"
|
||||||
|
onClick={() => onDeleteClick(post.id)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PostsList
|
||||||
31
web/src/components/PostsCell/PostsCell.js
Normal file
31
web/src/components/PostsCell/PostsCell.js
Normal file
@@ -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 = () => <div>Loading...</div>
|
||||||
|
|
||||||
|
export const Empty = () => {
|
||||||
|
return (
|
||||||
|
<div className="rw-text-center">
|
||||||
|
{'No posts yet. '}
|
||||||
|
<Link to={routes.newPost()} className="rw-link">
|
||||||
|
{'Create one?'}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Success = ({ posts }) => {
|
||||||
|
return <Posts posts={posts} />
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import FatalErrorPage from 'src/pages/FatalErrorPage'
|
|||||||
|
|
||||||
import Routes from 'src/Routes'
|
import Routes from 'src/Routes'
|
||||||
|
|
||||||
|
import './scaffold.css'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
|
|||||||
24
web/src/layouts/BlogLayout/BlogLayout.js
Normal file
24
web/src/layouts/BlogLayout/BlogLayout.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Link, routes } from '@redwoodjs/router'
|
||||||
|
|
||||||
|
const BlogLayout = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<header>
|
||||||
|
<h1>Redwood Blog</h1>
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<Link to={routes.about()}>About</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link to={routes.home()}>Home</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<main>{children}</main>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlogLayout
|
||||||
7
web/src/layouts/BlogLayout/BlogLayout.stories.js
Normal file
7
web/src/layouts/BlogLayout/BlogLayout.stories.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import BlogLayout from './BlogLayout'
|
||||||
|
|
||||||
|
export const generated = () => {
|
||||||
|
return <BlogLayout />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { title: 'Layouts/BlogLayout' }
|
||||||
11
web/src/layouts/BlogLayout/BlogLayout.test.js
Normal file
11
web/src/layouts/BlogLayout/BlogLayout.test.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { render } from '@redwoodjs/testing'
|
||||||
|
|
||||||
|
import BlogLayout from './BlogLayout'
|
||||||
|
|
||||||
|
describe('BlogLayout', () => {
|
||||||
|
it('renders successfully', () => {
|
||||||
|
expect(() => {
|
||||||
|
render(<BlogLayout />)
|
||||||
|
}).not.toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
23
web/src/layouts/PostsLayout/PostsLayout.js
Normal file
23
web/src/layouts/PostsLayout/PostsLayout.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Link, routes } from '@redwoodjs/router'
|
||||||
|
import { Flash } from '@redwoodjs/web'
|
||||||
|
|
||||||
|
const PostsLayout = (props) => {
|
||||||
|
return (
|
||||||
|
<div className="rw-scaffold">
|
||||||
|
<Flash timeout={1000} />
|
||||||
|
<header className="rw-header">
|
||||||
|
<h1 className="rw-heading rw-heading-primary">
|
||||||
|
<Link to={routes.posts()} className="rw-link">
|
||||||
|
Posts
|
||||||
|
</Link>
|
||||||
|
</h1>
|
||||||
|
<Link to={routes.newPost()} className="rw-button rw-button-green">
|
||||||
|
<div className="rw-button-icon">+</div> New Post
|
||||||
|
</Link>
|
||||||
|
</header>
|
||||||
|
<main className="rw-main">{props.children}</main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PostsLayout
|
||||||
19
web/src/pages/AboutPage/AboutPage.js
Normal file
19
web/src/pages/AboutPage/AboutPage.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Link, routes } from '@redwoodjs/router'
|
||||||
|
import BlogLayout from 'src/layouts/BlogLayout'
|
||||||
|
|
||||||
|
const AboutPage = () => {
|
||||||
|
return (
|
||||||
|
<BlogLayout>
|
||||||
|
<h1>AboutPage</h1>
|
||||||
|
<p>
|
||||||
|
Find me in <tt>./web/src/pages/AboutPage/AboutPage.js</tt>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
My default route is named <tt>about</tt>, link to me with `
|
||||||
|
<Link to={routes.about()}>About</Link>`
|
||||||
|
</p>
|
||||||
|
</BlogLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AboutPage
|
||||||
7
web/src/pages/AboutPage/AboutPage.stories.js
Normal file
7
web/src/pages/AboutPage/AboutPage.stories.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import AboutPage from './AboutPage'
|
||||||
|
|
||||||
|
export const generated = () => {
|
||||||
|
return <AboutPage />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { title: 'Pages/AboutPage' }
|
||||||
11
web/src/pages/AboutPage/AboutPage.test.js
Normal file
11
web/src/pages/AboutPage/AboutPage.test.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { render } from '@redwoodjs/testing'
|
||||||
|
|
||||||
|
import AboutPage from './AboutPage'
|
||||||
|
|
||||||
|
describe('AboutPage', () => {
|
||||||
|
it('renders successfully', () => {
|
||||||
|
expect(() => {
|
||||||
|
render(<AboutPage />)
|
||||||
|
}).not.toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
12
web/src/pages/EditPostPage/EditPostPage.js
Normal file
12
web/src/pages/EditPostPage/EditPostPage.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import PostsLayout from 'src/layouts/PostsLayout'
|
||||||
|
import EditPostCell from 'src/components/EditPostCell'
|
||||||
|
|
||||||
|
const EditPostPage = ({ id }) => {
|
||||||
|
return (
|
||||||
|
<PostsLayout>
|
||||||
|
<EditPostCell id={id} />
|
||||||
|
</PostsLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditPostPage
|
||||||
13
web/src/pages/HomePage/HomePage.js
Normal file
13
web/src/pages/HomePage/HomePage.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import BlogLayout from 'src/layouts/BlogLayout'
|
||||||
|
import BlogPostsCell from 'src/components/BlogPostsCell'
|
||||||
|
|
||||||
|
const HomePage = () => {
|
||||||
|
return (
|
||||||
|
|
||||||
|
<BlogLayout>
|
||||||
|
<BlogPostsCell/>
|
||||||
|
</BlogLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HomePage
|
||||||
7
web/src/pages/HomePage/HomePage.stories.js
Normal file
7
web/src/pages/HomePage/HomePage.stories.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import HomePage from './HomePage'
|
||||||
|
|
||||||
|
export const generated = () => {
|
||||||
|
return <HomePage />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { title: 'Pages/HomePage' }
|
||||||
11
web/src/pages/HomePage/HomePage.test.js
Normal file
11
web/src/pages/HomePage/HomePage.test.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { render } from '@redwoodjs/testing'
|
||||||
|
|
||||||
|
import HomePage from './HomePage'
|
||||||
|
|
||||||
|
describe('HomePage', () => {
|
||||||
|
it('renders successfully', () => {
|
||||||
|
expect(() => {
|
||||||
|
render(<HomePage />)
|
||||||
|
}).not.toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
12
web/src/pages/NewPostPage/NewPostPage.js
Normal file
12
web/src/pages/NewPostPage/NewPostPage.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import PostsLayout from 'src/layouts/PostsLayout'
|
||||||
|
import NewPost from 'src/components/NewPost'
|
||||||
|
|
||||||
|
const NewPostPage = () => {
|
||||||
|
return (
|
||||||
|
<PostsLayout>
|
||||||
|
<NewPost />
|
||||||
|
</PostsLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NewPostPage
|
||||||
12
web/src/pages/PostPage/PostPage.js
Normal file
12
web/src/pages/PostPage/PostPage.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import PostsLayout from 'src/layouts/PostsLayout'
|
||||||
|
import PostCell from 'src/components/PostCell'
|
||||||
|
|
||||||
|
const PostPage = ({ id }) => {
|
||||||
|
return (
|
||||||
|
<PostsLayout>
|
||||||
|
<PostCell id={id} />
|
||||||
|
</PostsLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PostPage
|
||||||
12
web/src/pages/PostsPage/PostsPage.js
Normal file
12
web/src/pages/PostsPage/PostsPage.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import PostsLayout from 'src/layouts/PostsLayout'
|
||||||
|
import PostsCell from 'src/components/PostsCell'
|
||||||
|
|
||||||
|
const PostsPage = () => {
|
||||||
|
return (
|
||||||
|
<PostsLayout>
|
||||||
|
<PostsCell />
|
||||||
|
</PostsLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PostsPage
|
||||||
346
web/src/scaffold.css
Normal file
346
web/src/scaffold.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user