├── .travis.yml ├── app ├── spec ├── spec_helper.cr └── kemal-react-chat_spec.cr ├── .gitignore ├── src ├── assets │ ├── images │ │ └── bg.png │ ├── styles │ │ └── main.css │ └── javascripts │ │ ├── dist │ │ └── chat.min.js │ │ └── chat.jsx ├── views │ └── index.ecr ├── app.cr ├── app │ └── message.cr └── db │ └── init_db.cr ├── shard.yml ├── shard.lock ├── gulpfile.js ├── package.json ├── LICENSE └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: crystal 2 | -------------------------------------------------------------------------------- /app: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Angarsk8/chat-app-demo/HEAD/app -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/kemal-react-chat" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/ 2 | /libs/ 3 | /node_modules/ 4 | /.crystal/ 5 | /.shards/ 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Angarsk8/chat-app-demo/HEAD/src/assets/images/bg.png -------------------------------------------------------------------------------- /spec/kemal-react-chat_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Kemal::React::Chat do 4 | # TODO: Write tests 5 | 6 | it "works" do 7 | false.should eq(true) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: kemal-react-chat 2 | version: 0.1.0 3 | 4 | dependencies: 5 | kemal: 6 | github: sdogruyol/kemal 7 | pg: 8 | github: will/crystal-pg 9 | 10 | authors: 11 | - Fatih Kadir Akın 12 | - Andrés García 13 | 14 | license: MIT 15 | -------------------------------------------------------------------------------- /shard.lock: -------------------------------------------------------------------------------- 1 | version: 1.0 2 | shards: 3 | kemal: 4 | github: sdogruyol/kemal 5 | version: 0.14.1 6 | 7 | kilt: 8 | github: jeromegn/kilt 9 | version: 0.3.3 10 | 11 | pg: 12 | github: will/crystal-pg 13 | version: 0.8.0 14 | 15 | radix: 16 | github: luislavena/radix 17 | version: 0.3.0 18 | 19 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp') 2 | const react = require('gulp-react') 3 | const concat = require('gulp-concat') 4 | const uglify = require('gulp-uglify') 5 | const rename = require('gulp-rename') 6 | const babel = require('gulp-babel') 7 | 8 | const jsFiles = './src/assets/javascripts/*.jsx' 9 | const jsDest = './src/assets/javascripts/dist' 10 | 11 | gulp.task('build-js', () => { 12 | return gulp.src(jsFiles) 13 | .pipe(concat('chat.jsx')) 14 | .pipe(react()) 15 | .pipe(babel({ 16 | presets: ['es2015'] 17 | })) 18 | .pipe(rename('chat.min.js')) 19 | .pipe(uglify()) 20 | .pipe(gulp.dest(jsDest)) 21 | }) 22 | 23 | gulp.task('default', ['build-js']) 24 | -------------------------------------------------------------------------------- /src/assets/styles/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Helvetica Neue", sans-serif; 3 | font-size: 20px; 4 | font-weight: normal; 5 | background-color: #fcfcfc; 6 | background-image: url(/images/bg.png); 7 | } 8 | 9 | ul { 10 | margin: 0; 11 | padding: 0; 12 | display: block; 13 | margin-bottom: 60px; 14 | } 15 | 16 | ul li { 17 | display: block; 18 | width: 100%; 19 | padding: 0 10px; 20 | } 21 | 22 | ul li span:first-of-type { 23 | font-weight: bold; 24 | } 25 | 26 | input { 27 | width: calc(100% - 20px); 28 | font-size: 20px; 29 | line-height: 35px; 30 | background: #E4E4E4; 31 | border: 0; 32 | margin-top: 10px; 33 | padding: 0 10px; 34 | outline: 0; 35 | position: fixed; 36 | bottom: 10px; 37 | z-index: 1; 38 | box-shadow: 0px 3px 7px -3px #666; 39 | } 40 | 41 | button { 42 | display: none; 43 | } 44 | -------------------------------------------------------------------------------- /src/views/index.ecr: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Example with Kemal and PostgresSQL 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kemal-react-pg-chat", 3 | "version": "1.0.0", 4 | "description": "chat developed using crystal, kemal, react and postgres", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Angarsk8/kemal-react-pg-chat.git" 12 | }, 13 | "author": "Andrés García ", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/Angarsk8/kemal-react-pg-chat/issues" 17 | }, 18 | "homepage": "https://github.com/Angarsk8/kemal-react-pg-chat#readme", 19 | "devDependencies": { 20 | "babel-cli": "^6.11.4", 21 | "babel-preset-es2015": "^6.9.0", 22 | "gulp": "^3.9.1", 23 | "gulp-babel": "^6.1.2", 24 | "gulp-concat": "^2.6.0", 25 | "gulp-react": "^3.1.0", 26 | "gulp-rename": "^1.2.2", 27 | "gulp-uglify": "^1.5.4" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Andrés García 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/app.cr: -------------------------------------------------------------------------------- 1 | require "kemal" 2 | require "pg" 3 | require "./app/message" 4 | 5 | conn = PG.connect "postgres://user:password@localhost:5432/db_name" 6 | 7 | sockets = [] of HTTP::WebSocket 8 | 9 | public_folder "src/assets" 10 | 11 | get "/" do 12 | render "src/views/index.ecr" 13 | end 14 | 15 | ws "/" do |socket| 16 | sockets.push socket 17 | 18 | # Dispatch list of messages to the current socket 19 | socket.send Message.all(conn).to_json 20 | 21 | # Handle incoming message and dispatch it to all connected clients 22 | socket.on_message do |message| 23 | # Insert message into the Database 24 | Message.from_json(message).insert(conn) 25 | 26 | # Dispatch list of messages to all connected clients 27 | sockets.each do |a_socket| 28 | begin 29 | a_socket.send Message.all(conn).to_json 30 | rescue 31 | sockets.delete(a_socket) 32 | puts "Closing Socket: #{socket}" 33 | end 34 | end 35 | end 36 | 37 | # Handle disconnection and clean sockets 38 | socket.on_close do |_| 39 | sockets.delete(socket) 40 | puts "Closing Socket: #{socket}" 41 | end 42 | end 43 | 44 | Kemal.run 45 | -------------------------------------------------------------------------------- /src/app/message.cr: -------------------------------------------------------------------------------- 1 | class Message 2 | JSON.mapping( 3 | id: {type: Int64, nilable: true}, 4 | created_at: {type: String, nilable: true}, 5 | updated_at: {type: String, nilable: true}, 6 | username: String, 7 | message: String 8 | ) 9 | 10 | def self.all(conn) 11 | self.transform_messages(conn.exec(%{ 12 | SELECT 13 | id, username, message, 14 | to_char(created_at, 'HH24:MI AM') as created_at, 15 | to_char(updated_at, 'HH24:MI AM') as updated_at 16 | FROM messages 17 | ORDER BY id; 18 | })) 19 | end 20 | 21 | def insert(conn) 22 | conn.exec(%{ 23 | INSERT INTO messages (username, message, created_at, updated_at) 24 | VALUES ($1, $2, current_timestamp, current_timestamp) 25 | }, [self.username, self.message]) 26 | end 27 | 28 | def self.transform_messages(messages_table) 29 | messages_table.to_hash.map do |message| 30 | { 31 | id: message["id"].as(Int), 32 | username: message["username"].as(String), 33 | message: message["message"].as(String), 34 | created_at: message["created_at"].as(String), 35 | updated_at: message["updated_at"].as(String), 36 | } 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /src/db/init_db.cr: -------------------------------------------------------------------------------- 1 | require "pg" 2 | 3 | DB_NAME = "db_name" 4 | PG_PATH = "postgres://user:password@localhost:5432" 5 | 6 | # CREATES CONNECTION WITH DEFAULT POSTGRES DB 7 | conn = PG.connect("#{PG_PATH}/postgres") 8 | 9 | database_exists? = conn.exec(%{ 10 | SELECT CAST(1 AS integer) 11 | FROM pg_database 12 | WHERE datname=$1 13 | }, [DB_NAME]).to_hash.empty? ? false : true 14 | 15 | if !database_exists? 16 | # CREATES THE DB_NAME DATABASE WITH UTF8 ENCODING AND CLOSE THE CONNECTION 17 | puts "Creating database: #{DB_NAME}..." 18 | conn.exec("CREATE DATABASE #{DB_NAME} ENCODING 'UTF8';") 19 | conn.close 20 | 21 | # CREATES CONNECTION WITH THE NEWLY CREATED DATABASE 22 | puts "Connecting database: #{DB_NAME}..." 23 | conn = PG.connect("#{PG_PATH}/#{DB_NAME}") 24 | 25 | # CREATES THE MESSAGES TABLE IN THE NEWLY CREATED DATABASE 26 | puts "Creating messages table in #{DB_NAME}..." 27 | conn.exec(%{ 28 | CREATE TABLE messages ( 29 | id SERIAL PRIMARY KEY, 30 | username TEXT NOT NULL, 31 | message TEXT NOT NULL, 32 | created_at TIMESTAMP WITH TIME ZONE NOT NULL, 33 | updated_at TIMESTAMP WITH TIME ZONE NOT NULL 34 | ); 35 | }) 36 | puts "Process finished succesfully" 37 | else 38 | puts "The database #{DB_NAME} already exists!!" 39 | end 40 | 41 | puts "Closing connection..." 42 | conn.close 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React + ES2015 + Kemal + PostgreSQL Chat Example 2 | 3 | ![Imgur](http://i.imgur.com/1hJIcKo.gif) 4 | 5 | Chat app using [React + ES2015](https://facebook.github.io/react/) + [Kemal](http://kemalcr.com) + [PostgreSQL](https://www.postgresql.org/) 6 | 7 | This demonstrates how easy it is to build Realtime Web applications with Kemal. 8 | 9 | * This project is a fork of https://github.com/f/kemal-react-chat, but implements ES2015 and integrates with a persistent data storage with PostgreSQL* 10 | 11 | ## Requirements 12 | * Crystal 0.18.7 13 | * PostgreSQL (I have the v9.5.2) 14 | * Node (I have the v5.11.1) 15 | * NPM (I have the v3.8.6) 16 | 17 | **Node and NPM are both optional if you are just going to run the app, but necessary for development since they are needed to run the gulp tasks* 18 | 19 | ## Installation 20 | Before you can run this program you have to install the packages that it uses. You do that with `$ shards install `. 21 | You also need to change the path to the PostgreSQL database in `src/db/init_db.cr` and `src/app.cr` 22 | 23 | ```crystal 24 | # src/db/init_db.cr 25 | require "pg" 26 | 27 | # Configure these two variables 28 | DB_NAME = "db_name" 29 | PG_PATH = "postgres://user:password@localhost:5432" 30 | 31 | ... 32 | ``` 33 | 34 | ```crystal 35 | # src/notes.cr 36 | require "kemal" 37 | require "pg" 38 | require "./app/message" 39 | 40 | # Configure the path of the database based on what you did in the src/db/init_db.cr file 41 | conn = PG.connect "postgres://user:password@localhost:5432/db_name 42 | 43 | ... 44 | ``` 45 | 46 | Once you have installed the dependencies and configured the database path, you need to create the actual database and table for the application. You do that by running `$ crystal src/db/init_db.cr`. 47 | 48 | ## Run Project 49 | You can run this program in two ways: 50 | 51 | 1. Compile/build the project using the command line with `$ crystal build src/app.cr --release` and run the executable `$ ./app` 52 | 2. Run the program with `$ crystal src/app.cr` (no compilation required) 53 | 54 | Once you have run the program, you can open a browser window at [localthost:3000](http://localhost:3000) and see the actual app. 55 | 56 | ## Live Demo 57 | 58 | You can see and use a live demo of the app here: [kemal-react-pg-chat.herokuapp.com](https://kemal-react-pg-chat.herokuapp.com/). -------------------------------------------------------------------------------- /src/assets/javascripts/dist/chat.min.js: -------------------------------------------------------------------------------- 1 | "use strict";function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function _inherits(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}var _createClass=function(){function e(e,t){for(var n=0;n Math.floor(Math.random()*(max-min+1)+min) 19 | const server = new WebSocket(`ws://${location.hostname}:${location.port}`) 20 | const user = localStorage.getItem('user') || `${prompt("What is your name, sir?").replace(/\:|\@/g, "")}@${randomColor({luminosity: 'dark'})}@${random(1000, 2000)}` 21 | 22 | this.sendable = true 23 | localStorage.setItem('user', user) 24 | 25 | server.onmessage = event => { 26 | const messages = JSON.parse(event.data) 27 | 28 | self.setState({messages: messages}) 29 | self.refs.message.focus() 30 | 31 | window.scrollTo(0, document.body.scrollHeight) 32 | new Beep(random(18000, 22050)).play(messages[messages.length - 1].username.split('@')[2], 0.05, [Beep.utils.amplify(8000)]); 33 | } 34 | 35 | server.onopen = () => { 36 | const payload = 37 | JSON.stringify({username: user, message: "joined the room."}) 38 | 39 | server.send(payload) 40 | } 41 | 42 | server.onclose = () => { 43 | const payload = 44 | JSON.stringify({username: user, message: "left the room."}) 45 | 46 | server.send(payload) 47 | } 48 | 49 | this.server = server 50 | this.user = user 51 | this.refs.message.focus() 52 | } 53 | 54 | sendMessage () { 55 | if (!this.sendable) { 56 | return false 57 | } 58 | const self = this 59 | setTimeout(function () { 60 | self.sendable = true; 61 | }, 100) 62 | 63 | const payload = 64 | JSON.stringify({username: this.user, message: this.refs.message.value}) 65 | 66 | this.server.send(payload) 67 | this.refs.message.value = '' 68 | this.sendable = false 69 | } 70 | 71 | sendMessageWithEnter (e) { 72 | if (e.keyCode == 13) { 73 | this.sendMessage(); 74 | } 75 | } 76 | 77 | render () { 78 | const messages = this.state.messages.map( message => { 79 | const user = message.username.split("@") 80 | const color = user[1] 81 | const name = user[0] 82 | const $message = 83 |
  • 84 | {name}[{message.created_at}]: 85 | {message.message} 86 |
  • 87 | 88 | return $message 89 | }) 90 | 91 | const $chat = 92 |
    93 |
      {messages}
    94 | 96 | 97 |
    98 | 99 | return $chat 100 | } 101 | } 102 | 103 | ReactDOM.render(, document.getElementById('chat')) 104 | --------------------------------------------------------------------------------