├── .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 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------