├── .gitignore ├── README.md ├── api ├── comm.sqlite └── index.php ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json └── src ├── App.css ├── App.js ├── App.test.js ├── components ├── Comment.js ├── CommentForm.js └── CommentList.js ├── index.css ├── index.js ├── logo.svg └── registerServiceWorker.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## React Comment 2 | 3 | ![react comments](https://i2.wp.com/www.qcode.in/wp-content/uploads/2018/07/react-comment-app.png?resize=1200%2C811&ssl=1) 4 | A comment app build on ReactJs, checkout the tutorial on [QCode.in](https://www.qcode.in/learn-react-by-creating-a-comment-app) 5 | 6 | ### Installation 7 | 8 | 1. Clone this repo and cd into it 9 | 2. Run `npm install` 10 | 3. run `npm run start` 11 | 4. Cd into `/api` folder and run a PHP dev server to by `php -S localhost:7777` 12 | 5. you should be able to play with the app 13 | 14 | ### Author 15 | 16 | Created by [QCode.in](http://www.qcode.in) 17 | 18 | ## License 19 | 20 | [MIT license](http://opensource.org/licenses/MIT). 21 | 22 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app). 23 | -------------------------------------------------------------------------------- /api/comm.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saqueib/react-comments/659598949cb8e94843a81a01eae10b988fbb8757/api/comm.sqlite -------------------------------------------------------------------------------- /api/index.php: -------------------------------------------------------------------------------- 1 | setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 22 | 23 | // Initialize db table 24 | initialize_db($dbh); 25 | 26 | } catch (PDOException $e) { 27 | json_response(['error' => 'Unable to open db connection '], 500); 28 | } 29 | 30 | // Uncomment to empty the databsae 31 | // empty_comments($dbh); 32 | 33 | /************************************** 34 | * Handle Post comment * 35 | **************************************/ 36 | if ($_SERVER['REQUEST_METHOD'] == 'POST') { 37 | 38 | // Inputs 39 | $name = @$_POST['name']; 40 | $comment = @$_POST['message']; 41 | 42 | // Validate the input 43 | if (strlen($name) < 3) { 44 | json_response(['error' => 'Name is required!'], 422); 45 | } 46 | 47 | if (strlen($comment) < 5) { 48 | json_response(['error' => 'Comments is required!'], 422); 49 | } 50 | 51 | $data = ['name' => $name, 'message' => $comment]; 52 | 53 | save_comment($data, $dbh); 54 | 55 | json_response(transform($data + ['id' => $dbh->lastInsertId(), 'time' => time()]), 201); 56 | } 57 | 58 | /************************************** 59 | * Return list of Comments * 60 | **************************************/ 61 | $result = $dbh->query('SELECT * FROM comments ORDER BY id DESC', PDO::FETCH_ASSOC); 62 | 63 | $comments = $result ? $result->fetchAll() : []; 64 | 65 | // Transform result 66 | $comments = array_map('transform', $comments); 67 | 68 | json_response($comments); 69 | 70 | 71 | /************************************** Helper functions *************************************/ 72 | 73 | /* 74 | * Save a comment in db 75 | * 76 | * @param $data 77 | * @param $dbh 78 | * @return boolean 79 | */ 80 | function save_comment($data, $dbh) 81 | { 82 | // Prepare statement 83 | $statement = $dbh->prepare( 84 | "INSERT INTO comments 85 | (name, message, time) 86 | VALUES 87 | (:name, :message, :time)" 88 | ); 89 | 90 | // Bind and execute 91 | return $statement->execute(array( 92 | "name" => strip_tags($data['name']), 93 | "message" => strip_tags($data['message']), 94 | "time" => time() 95 | )); 96 | } 97 | 98 | /** 99 | * Die a valid json response 100 | * 101 | * @param $data 102 | * @param int $status_code 103 | */ 104 | function json_response($data, $status_code = 200) 105 | { 106 | http_response_code($status_code); 107 | header('Content-Type: application/json'); 108 | global $dbh; 109 | $dbh = null; 110 | die(json_encode($data)); 111 | } 112 | 113 | /** 114 | * Initialize db with table 115 | * 116 | * @param $dbh 117 | */ 118 | function initialize_db($dbh) 119 | { 120 | // Create comments table if not exists 121 | $dbh->exec("CREATE TABLE IF NOT EXISTS comments ( 122 | id INTEGER PRIMARY KEY, 123 | name VARCHAR, 124 | message TEXT, 125 | time INTEGER)"); 126 | } 127 | 128 | /* 129 | * Transform record from db 130 | * 131 | * @param $comm 132 | * @return array 133 | */ 134 | function transform($comm) 135 | { 136 | return [ 137 | 'id' => (int)$comm['id'], 138 | 'name' => $comm['name'], 139 | 'message' => $comm['message'], 140 | 'time' => time_elapsed_string($comm['time']), 141 | ]; 142 | } 143 | 144 | /** 145 | * Empty all comments 146 | * 147 | * @param $dbh 148 | */ 149 | function empty_comments($dbh) 150 | { 151 | $dbh->query('DELETE FROM comments'); 152 | $dbh->query('VACUUM'); 153 | } 154 | 155 | function time_elapsed_string($datetime, $full = false) 156 | { 157 | $now = new DateTime; 158 | $ago = new DateTime('@' . $datetime); 159 | $diff = $now->diff($ago); 160 | 161 | $diff->w = floor($diff->d / 7); 162 | $diff->d -= $diff->w * 7; 163 | 164 | $string = array( 165 | 'y' => 'year', 166 | 'm' => 'month', 167 | 'w' => 'week', 168 | 'd' => 'day', 169 | 'h' => 'hour', 170 | 'i' => 'minute', 171 | 's' => 'second', 172 | ); 173 | foreach ($string as $k => &$v) { 174 | if ($diff->$k) { 175 | $v = $diff->$k . ' ' . $v . ($diff->$k > 1 ? 's' : ''); 176 | } else { 177 | unset($string[$k]); 178 | } 179 | } 180 | 181 | if (!$full) $string = array_slice($string, 0, 1); 182 | return $string ? implode(', ', $string) . ' ago' : 'just now'; 183 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-comment", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "bootstrap": "^4.1.2", 7 | "react": "^16.4.1", 8 | "react-dom": "^16.4.1", 9 | "react-scripts": "1.1.4" 10 | }, 11 | "scripts": { 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test --env=jsdom", 15 | "eject": "react-scripts eject" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saqueib/react-comments/659598949cb8e94843a81a01eae10b988fbb8757/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React Comments by QCode.in 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | padding-top: 1rem; 3 | margin: 2rem auto; 4 | } 5 | 6 | .App-logo { 7 | height: 80px; 8 | } 9 | 10 | .Spin { 11 | animation: App-logo-spin infinite 2s linear; 12 | } 13 | 14 | .App-header { 15 | text-align: center; 16 | background-color: var(--secondary); 17 | padding: 20px; 18 | color: white; 19 | border-radius: 0.3rem; 20 | } 21 | 22 | .App-title { 23 | font-size: 1.5em; 24 | } 25 | 26 | .App-intro { 27 | font-size: large; 28 | } 29 | 30 | @keyframes App-logo-spin { 31 | from { 32 | transform: rotate(0deg); 33 | } 34 | to { 35 | transform: rotate(360deg); 36 | } 37 | } -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import logo from "./logo.svg"; 3 | import "bootstrap/dist/css/bootstrap.css"; 4 | import "./App.css"; 5 | 6 | import CommentList from "./components/CommentList"; 7 | import CommentForm from "./components/CommentForm"; 8 | 9 | class App extends Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | comments: [], 15 | loading: false 16 | }; 17 | 18 | this.addComment = this.addComment.bind(this); 19 | } 20 | 21 | componentDidMount() { 22 | // loading 23 | this.setState({ loading: true }); 24 | 25 | // get all the comments 26 | fetch("http://localhost:7777") 27 | .then(res => res.json()) 28 | .then(res => { 29 | this.setState({ 30 | comments: res, 31 | loading: false 32 | }); 33 | }) 34 | .catch(err => { 35 | this.setState({ loading: false }); 36 | }); 37 | } 38 | 39 | /** 40 | * Add new comment 41 | * @param {Object} comment 42 | */ 43 | addComment(comment) { 44 | this.setState({ 45 | loading: false, 46 | comments: [comment, ...this.state.comments] 47 | }); 48 | } 49 | 50 | render() { 51 | const loadingSpin = this.state.loading ? "App-logo Spin" : "App-logo"; 52 | return ( 53 |
54 |
55 | logo 56 |

57 | React Comments 58 | 59 | 💬 60 | 61 |

62 |

63 | Checkout the tutorial on{" "} 64 | 65 | QCode.in 66 | 67 |

68 |
69 | 70 |
71 |
72 |
Say something about React
73 | 74 |
75 |
76 | 80 |
81 |
82 |
83 | ); 84 | } 85 | } 86 | 87 | export default App; 88 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/Comment.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Comment(props) { 4 | const { name, message, time } = props.comment; 5 | 6 | return ( 7 |
8 | {name} 15 | 16 |
17 | {time} 18 |
{name}
19 | {message} 20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/components/CommentForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | export default class CommentForm extends Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { 7 | loading: false, 8 | error: "", 9 | 10 | comment: { 11 | name: "", 12 | message: "" 13 | } 14 | }; 15 | 16 | // bind context to methods 17 | this.handleFieldChange = this.handleFieldChange.bind(this); 18 | this.onSubmit = this.onSubmit.bind(this); 19 | } 20 | 21 | /** 22 | * Handle form input field changes & update the state 23 | */ 24 | handleFieldChange = event => { 25 | const { value, name } = event.target; 26 | 27 | this.setState({ 28 | ...this.state, 29 | comment: { 30 | ...this.state.comment, 31 | [name]: value 32 | } 33 | }); 34 | }; 35 | 36 | /** 37 | * Form submit handler 38 | */ 39 | onSubmit(e) { 40 | // prevent default form submission 41 | e.preventDefault(); 42 | 43 | if (!this.isFormValid()) { 44 | this.setState({ error: "All fields are required." }); 45 | return; 46 | } 47 | 48 | // loading status and clear error 49 | this.setState({ error: "", loading: true }); 50 | 51 | // persist the comments on server 52 | let { comment } = this.state; 53 | fetch("http://localhost:7777", { 54 | method: "post", 55 | body: JSON.stringify(comment) 56 | }) 57 | .then(res => res.json()) 58 | .then(res => { 59 | if (res.error) { 60 | this.setState({ loading: false, error: res.error }); 61 | } else { 62 | // add time return from api and push comment to parent state 63 | comment.time = res.time; 64 | this.props.addComment(comment); 65 | 66 | // clear the message box 67 | this.setState({ 68 | loading: false, 69 | comment: { ...comment, message: "" } 70 | }); 71 | } 72 | }) 73 | .catch(err => { 74 | this.setState({ 75 | error: "Something went wrong while submitting form.", 76 | loading: false 77 | }); 78 | }); 79 | } 80 | 81 | /** 82 | * Simple validation 83 | */ 84 | isFormValid() { 85 | return this.state.comment.name !== "" && this.state.comment.message !== ""; 86 | } 87 | 88 | renderError() { 89 | return this.state.error ? ( 90 |
{this.state.error}
91 | ) : null; 92 | } 93 | 94 | render() { 95 | return ( 96 | 97 |
98 |
99 | 107 |
108 | 109 |
110 |