mirror of
https://github.com/astuto/astuto.git
synced 2025-12-16 11:47:56 +01:00
Add new post form
This commit is contained in:
3
app/assets/stylesheets/posts.scss
Normal file
3
app/assets/stylesheets/posts.scss
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Place all the styles related to the posts controller here.
|
||||||
|
// They will automatically be included in application.css.
|
||||||
|
// You can use Sass (SCSS) here: http://sass-lang.com/
|
||||||
20
app/controllers/posts_controller.rb
Normal file
20
app/controllers/posts_controller.rb
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
class PostsController < ApplicationController
|
||||||
|
before_action :authenticate_user!
|
||||||
|
|
||||||
|
def create
|
||||||
|
post = Post.new(post_params)
|
||||||
|
post.user_id = current_user.id
|
||||||
|
|
||||||
|
if post.save
|
||||||
|
render json: { status: 'success' }
|
||||||
|
else
|
||||||
|
render json: { status: 'error', message: post.errors.full_messages }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def post_params
|
||||||
|
params.require(:post).permit(:title, :description, :board_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
2
app/helpers/posts_helper.rb
Normal file
2
app/helpers/posts_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
module PostsHelper
|
||||||
|
end
|
||||||
29
app/javascript/components/Board/Board.tsx
Normal file
29
app/javascript/components/Board/Board.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import NewPost from './NewPost';
|
||||||
|
import PostList from './PostList';
|
||||||
|
|
||||||
|
import IBoard from '../../interfaces/IBoard';
|
||||||
|
|
||||||
|
import '../../stylesheets/components/Board/Board.scss';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
board: IBoard;
|
||||||
|
isLoggedIn: boolean;
|
||||||
|
authenticityToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Board extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { board, isLoggedIn, authenticityToken } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="boardContainer">
|
||||||
|
<NewPost board={board} isLoggedIn={isLoggedIn} authenticityToken={authenticityToken} />
|
||||||
|
<PostList />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Board;
|
||||||
170
app/javascript/components/Board/NewPost.tsx
Normal file
170
app/javascript/components/Board/NewPost.tsx
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import NewPostForm from './NewPostForm';
|
||||||
|
import Spinner from '../Shared/Spinner';
|
||||||
|
|
||||||
|
import IBoard from '../../interfaces/IBoard';
|
||||||
|
|
||||||
|
import '../../stylesheets/components/Board/NewPost.scss';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
board: IBoard;
|
||||||
|
isLoggedIn: boolean;
|
||||||
|
authenticityToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
showForm: boolean;
|
||||||
|
error: string;
|
||||||
|
success: string;
|
||||||
|
isLoading: boolean;
|
||||||
|
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class NewPost extends React.Component<Props, State> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
showForm: false,
|
||||||
|
error: '',
|
||||||
|
success: '',
|
||||||
|
isLoading: false,
|
||||||
|
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
this.toggleForm = this.toggleForm.bind(this);
|
||||||
|
this.onTitleChange = this.onTitleChange.bind(this);
|
||||||
|
this.onDescriptionChange = this.onDescriptionChange.bind(this);
|
||||||
|
this.submitForm = this.submitForm.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleForm() {
|
||||||
|
this.setState({
|
||||||
|
showForm: !this.state.showForm,
|
||||||
|
error: '',
|
||||||
|
success: '',
|
||||||
|
isLoading: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onTitleChange(title) {
|
||||||
|
this.setState({
|
||||||
|
title,
|
||||||
|
error: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onDescriptionChange(description) {
|
||||||
|
this.setState({
|
||||||
|
description,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async submitForm(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
error: '',
|
||||||
|
success: '',
|
||||||
|
isLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const boardId = this.props.board.id;
|
||||||
|
const { authenticityToken } = this.props;
|
||||||
|
const { title, description } = this.state;
|
||||||
|
|
||||||
|
if (title === '') {
|
||||||
|
this.setState({
|
||||||
|
error: 'You forgot to enter a title!',
|
||||||
|
isLoading: false,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let res = await fetch('http://localhost:3000/posts', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': authenticityToken,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
post: {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
board_id: boardId,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
this.setState({isLoading: false});
|
||||||
|
|
||||||
|
let data = await res.json();
|
||||||
|
|
||||||
|
if (data.status === 'success') this.setState({success: 'Your post has been published!'});
|
||||||
|
else this.setState({error: data.message});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
this.setState({
|
||||||
|
error: 'An unknown error occurred, try again.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { board, isLoggedIn } = this.props;
|
||||||
|
const {
|
||||||
|
showForm,
|
||||||
|
error,
|
||||||
|
success,
|
||||||
|
isLoading,
|
||||||
|
|
||||||
|
title,
|
||||||
|
description
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="newBoardContainer">
|
||||||
|
<span className="boardName">{board.name}</span>
|
||||||
|
<span className="boardDescription">{board.description}</span>
|
||||||
|
{/* <span>{this.props.authenticityToken}</span> */}
|
||||||
|
{
|
||||||
|
isLoggedIn ?
|
||||||
|
<button
|
||||||
|
onClick={this.toggleForm}
|
||||||
|
className="submitBtn btn btn-dark">
|
||||||
|
{ showForm ? 'Cancel' : 'Submit feedback' }
|
||||||
|
</button>
|
||||||
|
:
|
||||||
|
<a href="http://localhost:3000/users/sign_in" className="btn btn-dark">
|
||||||
|
Log in / Sign up
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
showForm ?
|
||||||
|
<NewPostForm
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
handleTitleChange={this.onTitleChange}
|
||||||
|
handleDescriptionChange={this.onDescriptionChange}
|
||||||
|
handleSubmit={this.submitForm}
|
||||||
|
/>
|
||||||
|
:
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{ isLoading ? <Spinner /> : null }
|
||||||
|
{ error ? <span className="error">{error}</span> : null }
|
||||||
|
{ success ? <span className="success">{success}</span> : null }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NewPost;
|
||||||
55
app/javascript/components/Board/NewPostForm.tsx
Normal file
55
app/javascript/components/Board/NewPostForm.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import '../../stylesheets/components/Board/NewPostForm.scss';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
handleTitleChange(title: string): void;
|
||||||
|
handleDescriptionChange(description: string): void;
|
||||||
|
handleSubmit(e: object): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NewPostForm = ({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
handleTitleChange,
|
||||||
|
handleDescriptionChange,
|
||||||
|
handleSubmit,
|
||||||
|
}: Props) => (
|
||||||
|
<div className="newPostForm">
|
||||||
|
<form>
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="postTitle">Title</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={title}
|
||||||
|
onChange={e => handleTitleChange(e.target.value)}
|
||||||
|
|
||||||
|
id="postTitle"
|
||||||
|
className="form-control"
|
||||||
|
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="postDescription">Description (optional)</label>
|
||||||
|
<textarea
|
||||||
|
value={description}
|
||||||
|
onChange={e => handleDescriptionChange(e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
|
||||||
|
className="form-control"
|
||||||
|
id="postDescription"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={e => handleSubmit(e)}
|
||||||
|
className="submitBtn btn btn-dark d-block mx-auto">
|
||||||
|
Submit feedback
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default NewPostForm;
|
||||||
11
app/javascript/components/Board/PostList.tsx
Normal file
11
app/javascript/components/Board/PostList.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import '../../stylesheets/components/Board/PostList.scss';
|
||||||
|
|
||||||
|
const PostList = () => (
|
||||||
|
<div className="postListContainer">
|
||||||
|
Posts will be show here.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default PostList;
|
||||||
9
app/javascript/components/Shared/Spinner.tsx
Normal file
9
app/javascript/components/Shared/Spinner.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
const Spinner = () => (
|
||||||
|
<div className="spinner-border" role="status">
|
||||||
|
<span className="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Spinner;
|
||||||
7
app/javascript/stylesheets/components/Board/Board.scss
Normal file
7
app/javascript/stylesheets/components/Board/Board.scss
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.boardContainer {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
47
app/javascript/stylesheets/components/Board/NewPost.scss
Normal file
47
app/javascript/stylesheets/components/Board/NewPost.scss
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
.newBoardContainer {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
|
||||||
|
width: 250px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
padding: 8px;
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boardName {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boardDescription {
|
||||||
|
color: grey;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 200;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitBtn {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
color: green;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.submitBtn {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.postListContainer {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
padding: 8px;
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
@@ -1,2 +1,10 @@
|
|||||||
<h1><%= @board.name %></h1>
|
<%=
|
||||||
<p><%= @board.description %></p>
|
react_component(
|
||||||
|
'Board/Board',
|
||||||
|
{
|
||||||
|
board: @board,
|
||||||
|
isLoggedIn: user_signed_in?,
|
||||||
|
authenticityToken: form_authenticity_token,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
%>
|
||||||
@@ -13,4 +13,6 @@ Rails.application.routes.draw do
|
|||||||
devise_for :users
|
devise_for :users
|
||||||
|
|
||||||
resources :boards, only: [:show]
|
resources :boards, only: [:show]
|
||||||
|
|
||||||
|
post '/posts', to: 'posts#create'
|
||||||
end
|
end
|
||||||
|
|||||||
5
spec/controllers/posts_controller_spec.rb
Normal file
5
spec/controllers/posts_controller_spec.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe PostsController, type: :controller do
|
||||||
|
|
||||||
|
end
|
||||||
15
spec/helpers/posts_helper_spec.rb
Normal file
15
spec/helpers/posts_helper_spec.rb
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
# Specs in this file have access to a helper object that includes
|
||||||
|
# the PostsHelper. For example:
|
||||||
|
#
|
||||||
|
# describe PostsHelper do
|
||||||
|
# describe "string concat" do
|
||||||
|
# it "concats two strings with spaces" do
|
||||||
|
# expect(helper.concat_strings("this","that")).to eq("this that")
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
RSpec.describe PostsHelper, type: :helper do
|
||||||
|
pending "add some examples to (or delete) #{__FILE__}"
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user