├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── shard.lock ├── shard.yml ├── spec ├── kemal-react-chat_spec.cr └── spec_helper.cr └── src ├── app.cr ├── assets ├── images │ └── bg.png ├── javascripts │ └── main.js └── styles │ └── main.css └── views └── index.ecr /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/ 2 | /libs/ 3 | /.crystal/ 4 | /.shards/ 5 | 6 | 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: crystal 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Fatih Kadir Akın 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React + Kemal Chat Example 2 | 3 | ![Imgur](http://i.imgur.com/1hJIcKo.gif) 4 | 5 | Chat app using [React](https://facebook.github.io/react/) + [Kemal](http://kemalcr.com). 6 | 7 | This demonstrates how easy it is to build Realtime Web applications with Kemal. 8 | 9 | ## Install & Run 10 | 11 | [Kemal](http://kemalcr.com) is written in [Crystal Programming Language](http://crystal-lang.org/) you need to have it installed on your machine. 12 | 13 | ``` 14 | git clone https://github.com/f/kemal-react-chat 15 | cd kemal-react-chat && shards install 16 | crystal src/app.cr 17 | ``` 18 | 19 | Go to `http://localhost:3000` to see it in action. 20 | -------------------------------------------------------------------------------- /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 | radix: 12 | github: luislavena/radix 13 | version: 0.3.0 14 | 15 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: kemal-react-chat 2 | version: 0.1.0 3 | 4 | dependencies: 5 | kemal: 6 | github: sdogruyol/kemal 7 | 8 | authors: 9 | - Fatih Kadir Akın 10 | 11 | license: MIT 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/kemal-react-chat" 3 | -------------------------------------------------------------------------------- /src/app.cr: -------------------------------------------------------------------------------- 1 | require "kemal" 2 | 3 | messages = [] of String 4 | sockets = [] of HTTP::WebSocket 5 | 6 | public_folder "src/assets" 7 | 8 | get "/" do 9 | render "src/views/index.ecr" 10 | end 11 | 12 | ws "/" do |socket| 13 | sockets.push socket 14 | 15 | # Handle incoming message and dispatch it to all connected clients 16 | socket.on_message do |message| 17 | messages.push message 18 | sockets.each do |a_socket| 19 | a_socket.send messages.to_json 20 | end 21 | end 22 | 23 | # Handle disconnection and clean sockets 24 | socket.on_close do |_| 25 | sockets.delete(socket) 26 | puts "Closing Socket: #{socket}" 27 | end 28 | end 29 | 30 | Kemal.run 31 | -------------------------------------------------------------------------------- /src/assets/images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f/kemal-react-chat/ce829d8da1187d464a7471560004604019b7102c/src/assets/images/bg.png -------------------------------------------------------------------------------- /src/assets/javascripts/main.js: -------------------------------------------------------------------------------- 1 | function random(min, max) { 2 | return Math.floor(Math.random()*(max-min+1)+min); 3 | } 4 | 5 | function setCurrentTime() { 6 | var currentTime = new Date() 7 | var hours = currentTime.getHours() 8 | var minutes = currentTime.getMinutes() 9 | 10 | if (minutes < 10) { 11 | minutes = "0" + minutes 12 | } 13 | 14 | var timePeriod = hours > 11 ? "PM" : "AM" 15 | 16 | return hours+":"+minutes + " " + timePeriod 17 | } 18 | 19 | var Chat = React.createClass({ 20 | 21 | getInitialState: function () { 22 | return { 23 | message: '', 24 | messages: [] 25 | }; 26 | }, 27 | 28 | componentDidMount: function () { 29 | var self = this; 30 | this.sendable = true; 31 | var server = new WebSocket("ws://" + location.hostname + ":" + location.port); 32 | var user = localStorage.getItem('user') || prompt("What is your name, sir?").replace(/\:|\@/g, "") + "@" + randomColor({luminosity: 'dark'}) + "@" + random(1000, 2000); 33 | localStorage.setItem('user', user); 34 | server.onmessage = function (event) { 35 | var messages = JSON.parse(event.data); 36 | self.setState({messages: messages}); 37 | window.scrollTo(0, document.body.scrollHeight); 38 | self.refs.message.focus(); 39 | new Beep(random(18000, 22050)).play(messages[messages.length - 1].split(":")[0].split('@')[2], 0.05, [Beep.utils.amplify(8000)]); 40 | }; 41 | 42 | server.onopen = function () { 43 | server.send(user + ": joined the room."); 44 | }; 45 | 46 | server.onclose = function () { 47 | server.send(user + ": left the room."); 48 | }; 49 | 50 | this.server = server; 51 | this.user = user; 52 | this.refs.message.focus(); 53 | }, 54 | 55 | sendMessage: function () { 56 | if (!this.sendable) { 57 | return false; 58 | } 59 | var self = this; 60 | setTimeout(function () { 61 | self.sendable = true; 62 | }, 100); 63 | this.server.send(this.user + ":" + this.refs.message.value); 64 | this.refs.message.value = ''; 65 | this.sendable = false; 66 | }, 67 | 68 | sendMessageWithEnter: function (e) { 69 | if (e.keyCode == 13) { 70 | this.sendMessage(); 71 | } 72 | }, 73 | 74 | render: function () { 75 | var messages = this.state.messages.map(function (message) { 76 | var parts = message.split(":"); 77 | var user = parts[0].split("@"); 78 | var color = user[1]; 79 | var name = user[0]; 80 | return React.createElement("li", null, 81 | React.createElement('span', {style: {color: color}}, name+"["+setCurrentTime()+"]: "), 82 | React.createElement('span', null, parts.slice(1).join(":").trim()) 83 | ); 84 | }); 85 | 86 | return React.createElement("div", null, 87 | React.createElement("ul", null, messages), 88 | React.createElement("input", { autofocus: true, placeholder: "write your message!", type: "text", ref: "message", onKeyUp: this.sendMessageWithEnter }), 89 | React.createElement("button", { type: "button", onClick: this.sendMessage }, "Send") 90 | ); 91 | } 92 | 93 | }); 94 | 95 | ReactDOM.render(React.createElement(Chat, null), document.getElementById('chat')); 96 | -------------------------------------------------------------------------------- /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 | React Example with Kemal 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | --------------------------------------------------------------------------------