bunch of stuff

This commit is contained in:
Kurt Hutten
2020-10-10 11:26:25 +11:00
parent 5f8cccf336
commit 029d6f4efc
36 changed files with 1277 additions and 7 deletions

View 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())
+}
```

View File

@@ -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())
}

View 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()"
}
]
}

View File

@@ -0,0 +1,3 @@
# Prisma Migrate lockfile v1
20201009213512-create-posts

View File

@@ -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())
} }

View 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!
}
`

View 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 },
})
}

View File

@@ -0,0 +1,9 @@
/*
import { posts } from './posts'
*/
describe('posts', () => {
it('returns true', () => {
expect(true).toBe(true)
})
})

View File

@@ -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>
) )

View 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>
))
}

View File

@@ -0,0 +1,6 @@
// Define your own mock data here:
export const standard = (/* vars, { ctx, req } */) => ({
blogPosts: {
id: 42,
},
})

View 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' }

View 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()
})
})

View 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>
)
}

View 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

View 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

View 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} />
}

View 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

View 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>&nbsp;</th>
</tr>
</thead>
<tbody>
{posts.map((post) => (
<tr key={post.id}>
<td>{truncate(post.id)}</td>
<td>{truncate(post.title)}</td>
<td>{truncate(post.body)}</td>
<td>{timeTag(post.createdAt)}</td>
<td>
<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

View 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} />
}

View File

@@ -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(

View 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

View File

@@ -0,0 +1,7 @@
import BlogLayout from './BlogLayout'
export const generated = () => {
return <BlogLayout />
}
export default { title: 'Layouts/BlogLayout' }

View File

@@ -0,0 +1,11 @@
import { render } from '@redwoodjs/testing'
import BlogLayout from './BlogLayout'
describe('BlogLayout', () => {
it('renders successfully', () => {
expect(() => {
render(<BlogLayout />)
}).not.toThrow()
})
})

View 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

View 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

View File

@@ -0,0 +1,7 @@
import AboutPage from './AboutPage'
export const generated = () => {
return <AboutPage />
}
export default { title: 'Pages/AboutPage' }

View File

@@ -0,0 +1,11 @@
import { render } from '@redwoodjs/testing'
import AboutPage from './AboutPage'
describe('AboutPage', () => {
it('renders successfully', () => {
expect(() => {
render(<AboutPage />)
}).not.toThrow()
})
})

View 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

View 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

View File

@@ -0,0 +1,7 @@
import HomePage from './HomePage'
export const generated = () => {
return <HomePage />
}
export default { title: 'Pages/HomePage' }

View File

@@ -0,0 +1,11 @@
import { render } from '@redwoodjs/testing'
import HomePage from './HomePage'
describe('HomePage', () => {
it('renders successfully', () => {
expect(() => {
render(<HomePage />)
}).not.toThrow()
})
})

View 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

View 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

View 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
View 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;
}
}