├── .babelrc ├── .gitignore ├── Dockerfile ├── README.md ├── components └── theme.ts ├── next-env.d.ts ├── next.config.js ├── nodemon.json ├── package.json ├── pages ├── _app.tsx ├── _document.tsx └── index.tsx ├── public ├── favicon.ico └── vercel.svg ├── server └── index.ts ├── styles └── reset.scss ├── tsconfig.json ├── tsconfig.server.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": [["styled-components", { "ssr": true }]] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # image指定 2 | FROM node:14-alpine 3 | 4 | # 作業ディレクトリ作成 5 | RUN mkdir -p /app 6 | COPY . /app 7 | WORKDIR /app 8 | 9 | # linux Update, set Timezone, install bash 10 | RUN apk --update add tzdata && \ 11 | cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \ 12 | apk del tzdata && \ 13 | apk add --no-cache bash 14 | 15 | RUN yarn install 16 | 17 | ENV HOST 0.0.0.0 18 | EXPOSE 3000 19 | 20 | CMD ["yarn", "dev"] 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nextjs + SocketIO + Chat App 2 | 3 | - [Next.js](https://nextjs.org/) 4 | - [SocketIO](https://socket.io/) 5 | - [Material UI](https://material-ui.com/) 6 | - [Express](https://expressjs.com/) 7 | - [TypeScript](https://www.typescriptlang.org/) 8 | - [styled-component](https://styled-components.com/) 9 | 10 | ## Development 11 | 12 | ### Docker image build 13 | ``` 14 | $ docker build -t nextjs-chat:latest . 15 | ``` 16 | 17 | ### Docker container start 18 | ``` 19 | $ docker run -p 3000:3000 nextjs-chat:latest 20 | ``` 21 | 22 | http://localhost:3000 23 | -------------------------------------------------------------------------------- /components/theme.ts: -------------------------------------------------------------------------------- 1 | import { createMuiTheme } from '@material-ui/core/styles' 2 | 3 | const theme = createMuiTheme({ 4 | palette: {}, 5 | overrides: {}, 6 | }) 7 | 8 | export default theme 9 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | sassOptions: { 5 | includePaths: [path.join(__dirname, 'styles')], 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["server"], 3 | "exec": "ts-node --project tsconfig.server.json server/index.ts", 4 | "ext": "js ts" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "nodemon", 7 | "build": "next build && tsc --project tsconfig.server.json", 8 | "start": "NODE_ENV=production node dist/index.js" 9 | }, 10 | "dependencies": { 11 | "@material-ui/core": "^4.11.0", 12 | "@material-ui/icons": "^4.9.1", 13 | "dayjs": "^1.8.35", 14 | "express": "^4.17.1", 15 | "next": "9.5.2", 16 | "react": "16.13.1", 17 | "react-dom": "16.13.1", 18 | "sass": "^1.26.10", 19 | "socket.io": "^2.3.0", 20 | "socket.io-client": "^2.3.0", 21 | "styled-components": "^5.1.1" 22 | }, 23 | "devDependencies": { 24 | "@types/express": "^4.17.7", 25 | "@types/node": "^14.6.1", 26 | "@types/react": "^16.9.48", 27 | "@types/react-dom": "^16.9.8", 28 | "@types/socket.io": "^2.1.11", 29 | "@types/socket.io-client": "^1.4.33", 30 | "babel-plugin-styled-components": "^1.11.1", 31 | "nodemon": "^2.0.4", 32 | "ts-node": "^9.0.0", 33 | "typescript": "^4.0.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { AppProps } from 'next/app' 3 | import { ThemeProvider as StyledComponentsThemeProvider } from 'styled-components' 4 | import { ThemeProvider as MaterialUIThemeProvider } from '@material-ui/core/styles' 5 | import { StylesProvider } from '@material-ui/styles' 6 | import CssBaseline from '@material-ui/core/CssBaseline' 7 | import theme from 'components/theme' 8 | 9 | const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => { 10 | useEffect(() => { 11 | const jssStyles = document.querySelector('#jss-server-side') 12 | if (jssStyles) { 13 | jssStyles.parentElement.removeChild(jssStyles) 14 | } 15 | }, []) 16 | 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | } 28 | 29 | export default MyApp 30 | 31 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { 2 | DocumentContext, 3 | Html, 4 | Head, 5 | Main, 6 | NextScript, 7 | } from 'next/document' 8 | import { ServerStyleSheet as StyledComponentSheets } from 'styled-components' 9 | import { ServerStyleSheets as MaterialUiServerStyleSheets } from '@material-ui/styles' 10 | 11 | export default class MyDocument extends Document { 12 | static async getInitialProps(ctx: DocumentContext) { 13 | const styledComponentSheets = new StyledComponentSheets() 14 | const materialUiServerStyleSheets = new MaterialUiServerStyleSheets() 15 | const originalRenderPage = ctx.renderPage 16 | 17 | try { 18 | ctx.renderPage = () => 19 | originalRenderPage({ 20 | enhanceApp: (App) => (props) => 21 | styledComponentSheets.collectStyles( 22 | materialUiServerStyleSheets.collect() 23 | ), 24 | }) 25 | 26 | const initialProps = await Document.getInitialProps(ctx) 27 | return { 28 | ...initialProps, 29 | styles: ( 30 | <> 31 | {initialProps.styles} 32 | {styledComponentSheets.getStyleElement()} 33 | {materialUiServerStyleSheets.getStyleElement()} 34 | 35 | ), 36 | } 37 | } finally { 38 | styledComponentSheets.seal() 39 | } 40 | } 41 | 42 | render() { 43 | return ( 44 | 45 | 46 | 47 |
48 | 49 | 50 | 51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import styled from 'styled-components' 3 | import io from 'socket.io-client' 4 | import dayjs from 'dayjs' 5 | import { Container, Button, InputBase, Box, Avatar, Paper, Typography } from '@material-ui/core' 6 | import { Send } from '@material-ui/icons' 7 | 8 | type ContainerProps = {} 9 | 10 | type ChatType = { 11 | userName: string 12 | message: string 13 | datetime: string 14 | } 15 | 16 | const Home = (props: ContainerProps) => { 17 | const [socket, _] = useState(() => io()) 18 | const [isConnected, setIsConnected] = useState(false) 19 | const [newChat, setNewChat] = useState({ 20 | userName: '', 21 | message: '', 22 | datetime: '', 23 | }) 24 | const [chats, setChats] = useState([ 25 | { 26 | userName: 'TEST BOT', 27 | message: 'Hello World', 28 | datetime: '2020-09-01 12:00:00', 29 | } 30 | ]) 31 | const [userName, setUserName] = useState('') 32 | const [message, setMessage] = useState('') 33 | 34 | useEffect(() => { 35 | socket.on('connect', () => { 36 | console.log('socket connected!!') 37 | setIsConnected(true) 38 | }) 39 | socket.on('disconnect', () => { 40 | console.log('socket disconnected!!') 41 | setIsConnected(false) 42 | }) 43 | socket.on('update-data', (newData: ChatType) => { 44 | console.log('Get Updated Data', newData) 45 | setNewChat(newData) 46 | }) 47 | 48 | return () => { 49 | socket.close() 50 | } 51 | }, []) 52 | 53 | useEffect(() => { 54 | if (newChat.message) { 55 | setChats([ ...chats, newChat]) 56 | } 57 | }, [newChat]) 58 | 59 | const handleSubmit = async () => { 60 | const datetime = dayjs().format('YYYY-MM-DD HH:mm:ss') 61 | await fetch(location.href + 'chat', { 62 | method: 'POST', 63 | mode: 'cors', 64 | headers: { 65 | 'Content-Type': 'application/json' 66 | }, 67 | body: JSON.stringify({ 68 | userName, 69 | message, 70 | datetime, 71 | }) 72 | }) 73 | setMessage('') 74 | } 75 | 76 | return ( 77 | 87 | ) 88 | } 89 | 90 | type Props = ContainerProps & { 91 | className?: string 92 | isConnected: boolean 93 | chats: ChatType[] 94 | userName: string 95 | message: string 96 | setUserName: (value: string) => void 97 | setMessage: (value: string) => void 98 | handleSubmit: () => void 99 | } 100 | 101 | const Component = (props: Props) => ( 102 | 103 | 104 | 105 | {props.chats.map((chat, index) => ( 106 | 107 | 108 | 109 | {chat.userName.slice(0, 1).toUpperCase() || 'T'} 110 | 111 | 112 | 113 | {chat.userName || 'TEST BOT'} 114 | {dayjs(chat.datetime).format('HH:mm')} 115 | 116 | {chat.message} 117 | 118 | 119 | 120 | ))} 121 | 122 | 123 | 124 | 125 | props.setUserName(e.target.value)} 129 | fullWidth 130 | /> 131 | props.setMessage(e.target.value)} 136 | fullWidth 137 | multiline 138 | rows={3} 139 | /> 140 | 141 | 142 | 152 | 153 | 154 | 155 | 156 | ) 157 | 158 | const StyledComponent = styled(Component)` 159 | .name { 160 | font-weight: 700; 161 | padding-right: 5px; 162 | } 163 | ` 164 | 165 | export default Home 166 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serip39/nextjs-socketio-example/e9703d6c79e124f3aeb8757db081314e34b91d22/public/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /server/index.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from 'express' 2 | import bodyParser from 'body-parser' 3 | import next from 'next' 4 | import socketio from "socket.io" 5 | 6 | const dev = process.env.NODE_ENV !== 'production' 7 | const app = next({ dev }) 8 | const handle = app.getRequestHandler() 9 | const port = process.env.PORT || 3000 10 | 11 | app 12 | .prepare() 13 | .then(() => { 14 | const server = express() 15 | 16 | server.use(bodyParser()) 17 | 18 | server.post('/chat', (req: Request, res: Response) => { 19 | console.log('body', req.body) 20 | postIO(req.body) 21 | res.status(200).json({ message: 'success' }) 22 | }) 23 | 24 | server.all('*', async (req: Request, res: Response) => { 25 | return handle(req, res) 26 | }) 27 | 28 | const httpServer = server.listen(port, (err?: any) => { 29 | if (err) throw err 30 | console.log(`> Ready on localhost:${port} - env ${process.env.NODE_ENV}`) 31 | }) 32 | 33 | const io = socketio.listen(httpServer) 34 | 35 | io.on('connection', (socket: socketio.Socket) => { 36 | console.log('id: ' + socket.id + ' is connected') 37 | }) 38 | 39 | const postIO = (data) => { 40 | io.emit('update-data', data) 41 | } 42 | }) 43 | .catch((ex) => { 44 | console.error(ex.stack) 45 | process.exit(1) 46 | }) -------------------------------------------------------------------------------- /styles/reset.scss: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { 2 | margin: 0; 3 | padding: 0; 4 | border: 0; 5 | font-size: 100%; 6 | font: inherit; 7 | vertical-align: baseline; 8 | } 9 | article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { 10 | display: block; 11 | } 12 | body { 13 | line-height: 1; 14 | } 15 | ol, ul { 16 | list-style: none; 17 | } 18 | blockquote, q { 19 | quotes: none; 20 | } 21 | blockquote { 22 | &::before, &::after { 23 | content: ""; 24 | content: none; 25 | } 26 | } 27 | q { 28 | &::before, &::after { 29 | content: ""; 30 | content: none; 31 | } 32 | } 33 | table { 34 | border-collapse: collapse; 35 | border-spacing: 0; 36 | } 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "baseUrl": "./" 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"], 19 | "exclude": ["node_modules", ".next", "out", "dist"] 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "dist", 6 | "target": "es2017", 7 | "isolatedModules": false, 8 | "noEmit": false 9 | }, 10 | "include": ["server/**/*.ts"] 11 | } 12 | --------------------------------------------------------------------------------