├── .babelrc ├── .gitignore ├── README.md ├── package.json ├── public ├── global.css └── index.html ├── server ├── index.js └── sockets.js ├── src ├── App.html ├── ChatInput.html ├── LoginForm.html ├── Message.html ├── MessageList.html └── main.js ├── sveltegif.gif └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | plugins: [ 3 | 'transform-class-properties', 4 | 'transform-object-rest-spread', 5 | 'syntax-async-functions', 6 | 'transform-runtime', 7 | ], 8 | presets: [ 9 | 'es2015-node5', 10 | ], 11 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | public/bundle.* 4 | package-lock.json 5 | yarn.lock -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Svelte Chat 2 | 3 | An example app built with [Svelte.js](https://svelte.technology/), using Socket.io to provide the chat functionality. 4 | 5 | To try it out, clone/download this repo, run `yarn`, and then run both `yarn dev` and `yarn start:api`. 6 | 7 | Note that you must have both [yarn](https://yarnpkg.com/en/) and MongoDB installed to run. 8 | 9 | ![Svelte GIF](sveltegif.gif) 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-app", 3 | "version": "1.0.0", 4 | "devDependencies": { 5 | "babel-cli": "^6.26.0", 6 | "babel-plugin-transform-async-functions": "^6.22.0", 7 | "babel-plugin-transform-class-properties": "^6.24.1", 8 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 9 | "babel-plugin-transform-runtime": "^6.23.0", 10 | "babel-preset-es2015-node5": "^1.2.0", 11 | "cross-env": "^5.1.5", 12 | "css-loader": "^1.0.0", 13 | "mini-css-extract-plugin": "^0.4.0", 14 | "nodemon": "^1.18.9", 15 | "serve": "^10.0.2", 16 | "style-loader": "^0.23.1", 17 | "svelte": "^2.0.0", 18 | "svelte-loader": "2.11.0", 19 | "webpack": "^4.8.3", 20 | "webpack-cli": "^3.1.2", 21 | "webpack-dev-server": "^3.1.9" 22 | }, 23 | "scripts": { 24 | "build": "cross-env NODE_ENV=production webpack", 25 | "dev": "webpack-dev-server --content-base public", 26 | "start:api": "nodemon --exec babel-node server/index.js" 27 | }, 28 | "dependencies": { 29 | "body-parser": "^1.18.3", 30 | "cors": "^2.8.5", 31 | "express": "^4.16.4", 32 | "http": "^0.0.0", 33 | "mime": "^2.4.0", 34 | "mongodb": "^3.1.13", 35 | "socket.io": "^2.2.0", 36 | "socket.io-client": "^2.2.0", 37 | "svelte-transitions": "^1.2.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /public/global.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | position: relative; 4 | width: 100%; 5 | height: 100%; 6 | } 7 | 8 | * { 9 | box-sizing: border-box; 10 | } 11 | 12 | body { 13 | color: #333; 14 | margin: 0; 15 | padding: 8px; 16 | box-sizing: border-box; 17 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 18 | Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; 19 | height: 100vh; 20 | background: linear-gradient(to bottom, #fd8271, #a62824); 21 | font-family: 'Khula', sans-serif; 22 | color: #2f2f2f; 23 | overflow: hidden; 24 | } 25 | 26 | #root { 27 | height: 100%; 28 | display: flex; 29 | justify-content: center; 30 | align-items: center; 31 | flex-direction: row; 32 | } 33 | 34 | a { 35 | color: rgb(0, 100, 200); 36 | text-decoration: none; 37 | } 38 | 39 | a:hover { 40 | text-decoration: underline; 41 | } 42 | 43 | a:visited { 44 | color: rgb(0, 80, 160); 45 | } 46 | 47 | label { 48 | display: block; 49 | } 50 | 51 | input, 52 | button, 53 | select, 54 | textarea { 55 | font-family: inherit; 56 | font-size: inherit; 57 | padding: 0.4em; 58 | margin: 0 0 0.5em 0; 59 | box-sizing: border-box; 60 | border: 1px solid #ccc; 61 | border-radius: 2px; 62 | } 63 | 64 | h1, 65 | p { 66 | margin: 0; 67 | } 68 | 69 | input:disabled { 70 | color: #ccc; 71 | } 72 | 73 | input[type='range'] { 74 | height: 0; 75 | } 76 | 77 | button { 78 | background-color: #f4f4f4; 79 | outline: none; 80 | } 81 | 82 | button:active { 83 | background-color: #ddd; 84 | } 85 | 86 | button:focus { 87 | border-color: #666; 88 | } 89 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Svelte app 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { MongoClient } from 'mongodb'; 3 | import bodyParser from 'body-parser'; 4 | import cors from 'cors'; 5 | import { createServer } from 'http'; 6 | import setUpSockets from './sockets'; 7 | 8 | const app = express(); 9 | app.use(bodyParser.json()); 10 | app.use(cors()); 11 | const server = createServer(app); 12 | 13 | const start = async () => { 14 | const client = await MongoClient.connect('mongodb://localhost/realtime'); 15 | const db = client.db('realtime'); 16 | setUpSockets(server, db); 17 | 18 | server.listen(3000, () => { 19 | console.log('Listening at port', server.address().port); 20 | }); 21 | }; 22 | 23 | start(); 24 | -------------------------------------------------------------------------------- /server/sockets.js: -------------------------------------------------------------------------------- 1 | import socket from 'socket.io'; 2 | 3 | const emitMessages = async (db, emitter) => { 4 | const messages = await getMessages(db); 5 | emitter.emit('messages', messages); 6 | }; 7 | 8 | const getMessages = db => { 9 | return db 10 | .collection('messages') 11 | .find() 12 | .toArray(); 13 | }; 14 | 15 | const handleNewMessage = (db, client, io) => { 16 | client.on('newMessage', async message => { 17 | const { text, author } = message; 18 | await db.collection('messages').insertOne({ text, author }); 19 | emitMessages(db, io); 20 | }); 21 | }; 22 | 23 | const setUpConnection = (io, db) => { 24 | io.on('connection', async client => { 25 | emitMessages(db, client); 26 | handleNewMessage(db, client, io); 27 | }); 28 | }; 29 | 30 | export default (server, db) => { 31 | const io = socket(server); 32 | setUpConnection(io, db); 33 | }; 34 | -------------------------------------------------------------------------------- /src/App.html: -------------------------------------------------------------------------------- 1 |
2 | {#if username} 3 |
4 | 5 | 6 |
7 | {:else} 8 | 9 | {/if} 10 |
11 | 12 | 30 | 31 | 40 | -------------------------------------------------------------------------------- /src/ChatInput.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 8 |
9 | 10 | 51 | -------------------------------------------------------------------------------- /src/LoginForm.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 |
7 |
8 | 9 | -------------------------------------------------------------------------------- /src/Message.html: -------------------------------------------------------------------------------- 1 |
2 |

{message.text}

3 | {#if showAuthor(message, nextMessage)} 4 |

{message.author}

5 | {/if} 6 |
7 | 8 | 36 | 37 | 48 | -------------------------------------------------------------------------------- /src/MessageList.html: -------------------------------------------------------------------------------- 1 |
2 | {#each messages as message, i} 3 | 4 | {:else} 5 |

No messages

6 | {/each} 7 |
8 | 9 | 16 | 17 | 24 | 25 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import io from 'socket.io-client'; 2 | 3 | import App from './App.html'; 4 | const socket = io('http://localhost:3000'); 5 | 6 | const app = new App({ 7 | target: document.getElementById('root'), 8 | data: { 9 | messages: [], 10 | newMessage: '', 11 | username: '' 12 | } 13 | }); 14 | 15 | app.on('submitForm', () => { 16 | const { newMessage, username } = app.get(); 17 | socket.emit('newMessage', { 18 | text: newMessage, 19 | author: username 20 | }); 21 | app.set({ newMessage: '' }); 22 | }); 23 | 24 | app.on('setUsername', () => { 25 | app.set({ username: document.getElementById('enter-username').value }); 26 | }); 27 | 28 | socket.on('messages', messages => { 29 | app.set({ 30 | messages 31 | }); 32 | }); 33 | 34 | window.app = app; 35 | 36 | export default app; 37 | -------------------------------------------------------------------------------- /sveltegif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdomes/svelte-chat/87c1de706935d21d9d35b629e5edc98c5e854cea/sveltegif.gif -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 3 | 4 | const mode = process.env.NODE_ENV || 'development'; 5 | const prod = mode === 'production'; 6 | 7 | module.exports = { 8 | entry: { 9 | bundle: ['./src/main.js'] 10 | }, 11 | resolve: { 12 | extensions: ['.js', '.html'] 13 | }, 14 | output: { 15 | path: __dirname + '/public', 16 | filename: '[name].js', 17 | chunkFilename: '[name].[id].js' 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.html$/, 23 | exclude: /node_modules/, 24 | use: { 25 | loader: 'svelte-loader', 26 | options: { 27 | skipIntroByDefault: true, 28 | nestedTransitions: true, 29 | emitCss: true, 30 | hotReload: true 31 | } 32 | } 33 | }, 34 | { 35 | test: /\.css$/, 36 | use: [ 37 | /** 38 | * MiniCssExtractPlugin doesn't support HMR. 39 | * For developing, use 'style-loader' instead. 40 | * */ 41 | prod ? MiniCssExtractPlugin.loader : 'style-loader', 42 | 'css-loader' 43 | ] 44 | } 45 | ] 46 | }, 47 | mode, 48 | plugins: [ 49 | new MiniCssExtractPlugin({ 50 | filename: '[name].css' 51 | }) 52 | ], 53 | devtool: prod ? false: 'source-map' 54 | }; 55 | --------------------------------------------------------------------------------