├── views ├── head.pug ├── message.pug └── body.pug ├── package.json ├── .gitignore ├── .editorconfig ├── LICENSE ├── README.md ├── public └── css │ └── main.css └── index.js /views/head.pug: -------------------------------------------------------------------------------- 1 | html 2 | head 3 | title #{title} 4 | link(href='' + styleUrl rel='stylesheet') 5 | body 6 | iframe(name='frame') 7 | -------------------------------------------------------------------------------- /views/message.pug: -------------------------------------------------------------------------------- 1 | div.message 2 | div.message__avatar 3 | img(src='https://api.adorable.io/avatars/50/' + user.nickname + '.png' alt='' + user.nickname) 4 | div.message__content 5 | div.message__author 6 | span.message__author-nickname #{user.nickname} 7 | span.message__date #{message.dt} 8 | p.message__text #{message.message} 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-only-chat", 3 | "version": "1.0.0", 4 | "description": "Async web chat without JavaScript in browser", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "iwonz", 11 | "license": "MIT", 12 | "dependencies": { 13 | "body-parser": "^1.19.0", 14 | "express": "^4.16.4", 15 | "human-date": "^1.4.0", 16 | "node-random-name": "^1.0.1", 17 | "nodemon": "^1.19.0", 18 | "pug": "^2.0.3", 19 | "uuid": "^3.3.2", 20 | "ip": "^1.1.5" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /views/body.pug: -------------------------------------------------------------------------------- 1 | style(type='text/css'). 2 | .chat:not([last-modify='#{lastModify}']) { display: none; } 3 | .chat[last-modify='#{lastModify}'] { display: flex; } 4 | div.chat(last-modify='' + lastModify) 5 | != messages 6 | div.chat__footer 7 | form(action='' + writeUrl method='POST' target='frame') 8 | input(type='hidden' name='user_id' value='' + user.id) 9 | div.message-box 10 | div.message-box__avatar 11 | img(src='https://api.adorable.io/avatars/50/' + user.nickname + '.png' alt='' + user.nickname) 12 | div.message-box__text 13 | textarea(name='message' placeholder='Write something...' required) 14 | button Send 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # profiling files 12 | chrome-profiler-events.json 13 | speed-measure-plugin.json 14 | 15 | # IDEs and editors 16 | /.idea 17 | .project 18 | .classpath 19 | .c9/ 20 | *.launch 21 | .settings/ 22 | *.sublime-workspace 23 | 24 | # IDE - VSCode 25 | .vscode/* 26 | !.vscode/settings.json 27 | !.vscode/tasks.json 28 | !.vscode/launch.json 29 | !.vscode/extensions.json 30 | 31 | # misc 32 | /.sass-cache 33 | /connect.lock 34 | /coverage 35 | /libpeerconnection.log 36 | npm-debug.log 37 | yarn-error.log 38 | testem.log 39 | /typings 40 | 41 | # System Files 42 | .DS_Store 43 | Thumbs.db 44 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | 12 | # Matches multiple files with brace expansion notation 13 | # Set default charset 14 | [*.{js,py}] 15 | charset = utf-8 16 | 17 | # 4 space indentation 18 | [*.py] 19 | indent_style = space 20 | indent_size = 4 21 | 22 | # Tab indentation (no size specified) 23 | [Makefile] 24 | indent_style = tab 25 | 26 | # Indentation override for all JS under lib directory 27 | [lib/**.js] 28 | indent_style = space 29 | indent_size = 2 30 | 31 | # Matches the exact files either package.json or .travis.yml 32 | [{package.json,.travis.yml}] 33 | indent_style = space 34 | indent_size = 2 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ivan Zimin 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What? 2 | Performance? No! 3 | Best code? No! 4 | Just for fun! 5 | 6 | Inspired by [@kkuchta](https://twitter.com/kkuchta) https://github.com/kkuchta/css-only-chat 7 | 8 | ![demo](http://iwonz.ru/projects/html-only-chat/demo.gif) 9 | 10 | # How can you test it? 11 | 12 | ## Online 13 | Go to https://html-only-chat.glitch.me 14 | 15 | ## Offline 16 | 1. Make sure what you have installed Node.js or [download it](https://nodejs.org/en/download/) 17 | 18 | 2. Clone repo by console command `git clone https://github.com/iwonz/html-only-chat.git` or [download it](https://github.com/iwonz/html-only-chat/archive/master.zip) 19 | 20 | 3. Enter the directory by `cd html-only-chat` 21 | 22 | 4. Install dependencies `npm i` 23 | 24 | 6. Run it `npm run start` 25 | 26 | 7. Wait for starting and follow to [http://127.0.0.1:3000](http://127.0.0.1:3000) (by default) or check your console to see actual link 27 | 28 | # Some interesting questions 29 | 1. How to scroll down the list all the time with messages after redraw? (overflow-anchor I couldn't start) 30 | 31 | 2. Why don't work CSS selectors like :last-child :nth-child :last-of-type? (Maybe problem in Transfer-Encoding: chunked) 32 | 33 | Have any idea? [Tweet me](https://twitter.com/iwonzimin) or [Email me](mailto:hello@iwonz.ru). Thx! 34 | -------------------------------------------------------------------------------- /public/css/main.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | border: none; 6 | outline: none; 7 | } 8 | 9 | body, 10 | html { 11 | width: 100%; 12 | height: 100%; 13 | } 14 | 15 | body { 16 | font-size: 14px; 17 | line-height: 1.4em; 18 | overflow: hidden; 19 | background-color: #EDEEF0; 20 | font-family: Tahoma, Arial, sans-serif; 21 | } 22 | 23 | iframe { display: none; } 24 | 25 | .chat { 26 | width: 600px; 27 | height: 100%; 28 | display: none; 29 | flex-direction: column; 30 | margin: 0 auto; 31 | background: #fff; 32 | overflow: hidden; 33 | } 34 | 35 | .chat__footer { 36 | background-color: #FAFBFC; 37 | flex-shrink: 0; 38 | border-top: 1px solid #E3E4E8; 39 | } 40 | 41 | .message-box { 42 | width: 100%; 43 | display: flex; 44 | padding: 15px; 45 | align-items: center; 46 | } 47 | 48 | .message-box__avatar img { 49 | border: 1px solid #E3E4E8; 50 | border-radius: 50%; 51 | } 52 | 53 | .message-box__text { 54 | padding-left: 15px; 55 | display: flex; 56 | flex-grow: 1; 57 | } 58 | 59 | .message-box__text textarea { 60 | border: 1px solid #E3E4E8; 61 | resize: none; 62 | height: 50px; 63 | font-size: 14px; 64 | line-height: 1.6em; 65 | padding: 13px 15px; 66 | flex-grow: 1; 67 | border-radius: 50px; 68 | margin-right: 15px; 69 | } 70 | 71 | .message-box__text button { 72 | flex-shrink: 0; 73 | flex-basis: 100px; 74 | border-radius: 50px; 75 | cursor: pointer; 76 | border: 1px solid #E3E4E8; 77 | background: #fff; 78 | } 79 | 80 | .messages { 81 | display: flex; 82 | flex-grow: 1; 83 | flex-direction: column; 84 | overflow: scroll; 85 | } 86 | 87 | .message { 88 | display: flex; 89 | padding: 15px; 90 | } 91 | 92 | .message_mine { 93 | text-align: right; 94 | } 95 | 96 | .message__avatar img { 97 | border: 1px solid #E3E4E8; 98 | border-radius: 50%; 99 | } 100 | 101 | .message__content { 102 | padding-left: 15px; 103 | min-width: 0; 104 | } 105 | 106 | .message__author-nickname { 107 | font-weight: bold; 108 | } 109 | 110 | .message__date { 111 | font-size: 12px; 112 | color: #777e8c; 113 | padding-left: 5px; 114 | } 115 | 116 | .message__text { 117 | word-wrap: break-word; 118 | line-height: 1.6em; 119 | } 120 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const uuid = require('uuid/v4'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const random_name = require('node-random-name'); 5 | const express = require('express'); 6 | const bodyParser = require('body-parser'); 7 | const hdate = require('human-date'); 8 | const pug = require('pug'); 9 | const ip = require('ip'); 10 | 11 | const app = express(); 12 | 13 | app.set('view engine', 'pug'); 14 | app.set('views', path.join(__dirname, 'views')); 15 | app.use(express.static(path.join(__dirname, 'public'))); 16 | app.use(bodyParser.urlencoded({ extended: true })); 17 | 18 | const config = { 19 | IP: ip.address(), 20 | PORT: 3000, 21 | get fullAddress() { 22 | return `http://${config.IP}:${config.PORT}`; 23 | }, 24 | get writeUrl() { 25 | return `${config.fullAddress}/write`; 26 | }, 27 | get styleUrl() { 28 | return `${config.fullAddress}/css/main.css`; 29 | } 30 | }; 31 | 32 | const users = new Map(); 33 | const messages = new Map(); 34 | 35 | const getContentHTML = (userId) => { 36 | return pug.compileFile('views/body.pug')({ 37 | lastModify: +new Date(), 38 | writeUrl: config.writeUrl, 39 | user: users.get(userId), 40 | messages: getMessagesHTML() 41 | }); 42 | }; 43 | 44 | const getMessagesHTML = () => { 45 | let messagesHTML = ` 46 |
47 | `; 48 | 49 | messages.forEach((message) => { 50 | message.dt = hdate.relativeTime(new Date(message.dt)); 51 | 52 | messagesHTML += pug.compileFile('views/message.pug')({ 53 | user: users.get(message.userId), 54 | message 55 | }); 56 | }); 57 | 58 | messagesHTML += ` 59 |
60 | `; 61 | 62 | return messagesHTML; 63 | }; 64 | 65 | const redraw = () => { 66 | users.forEach((user) => user.res.write(getContentHTML(user.id))); 67 | }; 68 | 69 | app.get('/', (req, res) => { 70 | res.setHeader('Content-Type', 'text/html; charset=utf-8'); 71 | res.setHeader('Transfer-Encoding', 'chunked'); 72 | res.setHeader('Content-Encoding', 'chunked'); 73 | res.setHeader('Connection', 'keep-alive'); 74 | 75 | const userId = uuid(); 76 | 77 | users.set(userId, { 78 | id: userId, 79 | nickname: random_name(), 80 | messages: [], 81 | res 82 | }); 83 | 84 | const headHTML = pug.compileFile('views/head.pug')({ 85 | title: 'Async web chat without JS in browser', 86 | styleUrl: config.styleUrl 87 | }); 88 | 89 | res.write(headHTML); 90 | res.write(getContentHTML(userId)); 91 | }); 92 | 93 | app.post('/write', (req, res) => { 94 | if (!req.param('message').length) { return res.end(); } 95 | 96 | const userId = req.param('user_id'); 97 | const user = users.get(userId); 98 | 99 | if (!user) { return res.end(); } 100 | 101 | const messageId = uuid(); 102 | 103 | messages.set(messageId, { 104 | id: messageId, 105 | userId: req.param('user_id'), 106 | dt: +new Date(), 107 | message: req.param('message') 108 | }); 109 | 110 | users.set(userId, { 111 | ...user, 112 | messages: [...user.messages, messageId] 113 | }); 114 | 115 | redraw(); 116 | 117 | res.end(); 118 | }); 119 | 120 | app.listen(config.PORT); 121 | 122 | console.log(`Server listening on ${config.fullAddress} or http://127.0.0.1:${config.PORT}`); 123 | --------------------------------------------------------------------------------