├── containers ├── index.ts └── Sample.tsx ├── .dockerignore ├── .env.sample ├── static └── etc │ ├── js │ └── hello.js │ ├── css │ └── hello.css │ └── hello.html ├── client ├── api │ ├── index.ts │ ├── sampleApi.ts │ └── service.ts └── polyfills.js ├── components ├── sample │ ├── index.ts │ ├── Sample.tsx │ ├── __tests__ │ │ └── Login.test.tsx │ └── Login.tsx ├── SelectLanguage.tsx └── Layout.tsx ├── actions ├── index.ts └── sample.ts ├── styles ├── index.ts ├── theme.ts ├── mixin.ts └── global.ts ├── svgs ├── index.ts └── Sample.tsx ├── .prettierrc ├── jest.setup.js ├── Dockerfile ├── lang ├── en │ ├── index.js │ ├── sample.json │ └── meta.json └── ko │ ├── index.js │ ├── sample.json │ └── meta.json ├── nodemon.json ├── server ├── api │ ├── index.ts │ ├── sample │ │ ├── index.ts │ │ └── controller.ts │ └── service.ts └── index.ts ├── tsconfig.test.json ├── tsconfig.server.json ├── sagas ├── index.ts └── sample.ts ├── reducers ├── index.ts └── sample.ts ├── lib └── withIntl.ts ├── ecosystem.config.js ├── jest.config.js ├── constants └── index.ts ├── .prettierignore ├── .gitignore ├── tsconfig.json ├── .babelrc ├── pages ├── _error.tsx ├── param.tsx ├── index.tsx ├── other.tsx ├── path │ └── depth.tsx ├── _document.tsx └── _app.tsx ├── store └── index.ts ├── next.config.js ├── contexts └── ThemeProvider.tsx ├── package.json └── readme.MD /containers/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .next 2 | node_modules 3 | Dockerfile -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | URL= 2 | CDN_URL= 3 | PORT= 4 | API_URL= -------------------------------------------------------------------------------- /static/etc/js/hello.js: -------------------------------------------------------------------------------- 1 | console.log('hello world!!') -------------------------------------------------------------------------------- /client/api/index.ts: -------------------------------------------------------------------------------- 1 | export {default as sampleApi} from './sampleApi'; -------------------------------------------------------------------------------- /components/sample/index.ts: -------------------------------------------------------------------------------- 1 | export {default as Sample} from './Sample'; -------------------------------------------------------------------------------- /actions/index.ts: -------------------------------------------------------------------------------- 1 | import * as sample from './sample' 2 | 3 | export default { 4 | sample, 5 | } -------------------------------------------------------------------------------- /styles/index.ts: -------------------------------------------------------------------------------- 1 | import * as theme from './theme'; 2 | 3 | export default { 4 | theme, 5 | } -------------------------------------------------------------------------------- /svgs/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @TODO: svg 사용 방식 예제 작업 필요. 3 | */ 4 | export {default as Sample} from './Sample'; -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": false, 5 | "singleQuote": true 6 | } -------------------------------------------------------------------------------- /static/etc/css/hello.css: -------------------------------------------------------------------------------- 1 | .static-hello { 2 | font-size: 20px; 3 | font-weight: bold; 4 | color: red; 5 | } -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | const Enzyme = require('enzyme') 2 | const Adapter = require('enzyme-adapter-react-16') 3 | 4 | Enzyme.configure({adapter: new Adapter()}) 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8 2 | 3 | WORKDIR /app 4 | 5 | COPY . . 6 | 7 | RUN yarn 8 | RUN yarn build 9 | 10 | EXPOSE 3000 11 | CMD ["yarn", "prod"] 12 | -------------------------------------------------------------------------------- /lang/en/index.js: -------------------------------------------------------------------------------- 1 | const sample = require("./sample.json"); 2 | const meta = require("./meta.json"); 3 | 4 | module.exports = { 5 | ...sample, 6 | ...meta, 7 | }; -------------------------------------------------------------------------------- /lang/ko/index.js: -------------------------------------------------------------------------------- 1 | const sample = require("./sample.json"); 2 | const meta = require("./meta.json"); 3 | 4 | module.exports = { 5 | ...sample, 6 | ...meta, 7 | }; -------------------------------------------------------------------------------- /lang/ko/sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "sample.helloWorld": "헬로월드", 3 | "sample.boilerplate": "보일러플레이트", 4 | "sample.enterText": "텍스트를 입력하세요.", 5 | "sample": "sample" 6 | } -------------------------------------------------------------------------------- /lang/en/sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "sample.helloWorld": "HELLO WORLD", 3 | "sample.boilerplate": "Boilerplate", 4 | "sample.enterText": "Please enter text", 5 | "sample": "sample" 6 | } -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["server/**/*.ts", "lang/**/*"], 3 | "execMap": { 4 | "ts": "ts-node --typeCheck --compilerOptions \"{\\\"module\\\":\\\"commonjs\\\"}\"" 5 | } 6 | } -------------------------------------------------------------------------------- /client/api/sampleApi.ts: -------------------------------------------------------------------------------- 1 | import service from "./service"; 2 | 3 | export default () => { 4 | return service({ 5 | method: 'get', 6 | uri: '/sample/api', 7 | }) 8 | }; -------------------------------------------------------------------------------- /server/api/index.ts: -------------------------------------------------------------------------------- 1 | import {Router} from 'express'; 2 | import sample from './sample'; 3 | 4 | const router = Router(); 5 | 6 | router.use('/sample', sample); 7 | 8 | export default router; -------------------------------------------------------------------------------- /server/api/sample/index.ts: -------------------------------------------------------------------------------- 1 | import {Router} from 'express'; 2 | import * as controller from './controller'; 3 | 4 | const router = Router(); 5 | 6 | router.get('/api', controller.api); 7 | 8 | export default router; -------------------------------------------------------------------------------- /lang/ko/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta.title": "헬로월드 - Next.js 보일러플레이트 입니다.", 3 | "meta.description": "쉬운개발을 위한 Next.js 보일러플레이트 입니다.", 4 | "meta.keywords": "next.js, 타입스크립트, 리엑트, styled-components", 5 | "meta": "meta" 6 | } -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ 4 | "node_modules", 5 | "**/*.spec.ts", 6 | "**/*.spec.tsx", 7 | "**/*.test.ts", 8 | "**/*.test.tsx", 9 | ] 10 | } -------------------------------------------------------------------------------- /tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "sourceMap": false, 6 | "outDir": ".next/production-server/" 7 | }, 8 | "include": ["server/**/*.ts"] 9 | } -------------------------------------------------------------------------------- /lang/en/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta.title": "Hello World - This is Next.js boilerplate", 3 | "meta.description": "This is Next.js to easy development", 4 | "meta.keywords": "next.js, typescript, react, styled-components", 5 | "meta": "meta" 6 | } -------------------------------------------------------------------------------- /sagas/index.ts: -------------------------------------------------------------------------------- 1 | import { all } from 'redux-saga/effects' 2 | import sample from './sample'; 3 | 4 | const mergedSagas = [].concat( 5 | sample, 6 | ); 7 | 8 | function * rootSaga () { 9 | yield all(mergedSagas) 10 | } 11 | 12 | export default rootSaga -------------------------------------------------------------------------------- /server/api/sample/controller.ts: -------------------------------------------------------------------------------- 1 | import service from "../service"; 2 | 3 | export const api = (req, res) => { 4 | 5 | const options = { 6 | method: 'get', 7 | uri: `/shows/1/episodes?specials=1`, 8 | params: req.query, 9 | }; 10 | 11 | service(options, res) 12 | 13 | }; -------------------------------------------------------------------------------- /reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux-immutable'; 2 | import {Map} from 'immutable' 3 | import sample, { initialState as sampleState } from './sample'; 4 | export const rootInitialState = Map({ 5 | sample: sampleState, 6 | }); 7 | 8 | export default combineReducers({ 9 | sample, 10 | }); -------------------------------------------------------------------------------- /client/api/service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Client 3 | */ 4 | 5 | import axios from 'axios'; 6 | 7 | export default ({uri, method, params = null, data = null, headers = null}) => { 8 | return axios({ 9 | url: `${process.env.URL}/api${uri}`, 10 | method: method, 11 | params: params, 12 | data: data, 13 | headers: headers 14 | }) 15 | }; -------------------------------------------------------------------------------- /static/etc/hello.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 이벤트용 정적페이지 서빙 테스트 6 | 7 | 8 | 9 | 10 |
11 | Hello! 12 |
13 | 14 | -------------------------------------------------------------------------------- /lib/withIntl.ts: -------------------------------------------------------------------------------- 1 | import hoistNonReactStatics from 'hoist-non-react-statics' 2 | import { injectIntl } from 'react-intl' 3 | 4 | export const hoistStatics = (higherOrderComponent) => (BaseComponent) => { 5 | const NewComponent = higherOrderComponent(BaseComponent) 6 | hoistNonReactStatics(NewComponent, BaseComponent) 7 | 8 | return NewComponent 9 | } 10 | 11 | export default hoistStatics(injectIntl) 12 | -------------------------------------------------------------------------------- /svgs/Sample.tsx: -------------------------------------------------------------------------------- 1 | const Sample = ({color = "#ffffff"}) => ( 2 | 3 | ) 4 | 5 | export default Sample; -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps : [{ 3 | name: 'next', 4 | script: '.next/production-server/index.js', 5 | exec_mode: 'cluster', 6 | instances: 0, //0으로 지정 할 경우 cpu코어 수 만큼 인스턴스 생성 7 | autorestart: true, 8 | watch: false, 9 | max_memory_restart: '1G', 10 | env: { 11 | NODE_ENV: 'development' 12 | }, 13 | env_production: { 14 | NODE_ENV: 'production', 15 | } 16 | }], 17 | }; 18 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const TEST_REGEX = '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|js?|tsx?|ts?)$' 2 | 3 | module.exports = { 4 | setupFiles: ['/jest.setup.js'], 5 | testRegex: TEST_REGEX, 6 | transform: { 7 | '^.+\\.tsx?$': 'babel-jest' 8 | }, 9 | testPathIgnorePatterns: [ 10 | '/.next/', '/node_modules/' 11 | ], 12 | moduleFileExtensions: [ 13 | 'ts', 'tsx', 'js', 'jsx' 14 | ], 15 | collectCoverage: true 16 | } 17 | -------------------------------------------------------------------------------- /constants/index.ts: -------------------------------------------------------------------------------- 1 | //media query size 2 | export const $GRID_BREAKPOINTS_SM = 420; 3 | export const $GRID_BREAKPOINTS_MD = 768; 4 | 5 | //font 6 | export const $FONT_MALGUN = `"맑은 고딕", "Malgun Gothic", "돋움", "Dotum", sans-serif`; 7 | export const $FONT_OPEN_SANS = `"Open Sans", "맑은 고딕", "Malgun Gothic", sans-serif`; 8 | 9 | //color 10 | export const $BASE_COLOR = "#000000"; 11 | export const $BASE_FONT_SIZE = 13; 12 | export const $BASE_LINE_HEIGHT = 1.45; 13 | export const $BASE_LETTER_SPACING = 0; 14 | 15 | -------------------------------------------------------------------------------- /client/polyfills.js: -------------------------------------------------------------------------------- 1 | 2 | /* eslint no-extend-native: 0 */ 3 | // core-js comes with Next.js. So, you can import it like below 4 | // import includes from 'core-js/library/fn/string/virtual/includes' 5 | // import repeat from 'core-js/library/fn/string/virtual/repeat' 6 | import '@babel/polyfill'; 7 | 8 | // Add your polyfills 9 | // This files runs at the very beginning (even before React and Next.js core) 10 | console.log('Load your polyfills') 11 | 12 | // String.prototype.includes = includes 13 | // String.prototype.repeat = repeat -------------------------------------------------------------------------------- /sagas/sample.ts: -------------------------------------------------------------------------------- 1 | import {call, put, take, fork} from 'redux-saga/effects' 2 | import * as api from '../client/api' 3 | import { actionTypes, sampleApiSuccess, sampleApiFailure } from '../actions/sample' 4 | 5 | function * sampleApiSaga () { 6 | while(true) { 7 | yield take(actionTypes.SAMPLE_API_REQUEST); 8 | try { 9 | const res = yield call(api.sampleApi); 10 | yield put(sampleApiSuccess(res)) 11 | } catch (err) { 12 | yield put(sampleApiFailure(err)) 13 | } 14 | } 15 | } 16 | 17 | export default [ 18 | fork(sampleApiSaga) 19 | ] -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Dependency directories 9 | node_modules/ 10 | 11 | # Optional npm cache directory 12 | .npm 13 | 14 | # locking file 15 | package-lock.json 16 | yarn.lock 17 | 18 | # dotenv environment variables file 19 | .env 20 | 21 | # next.js build output 22 | .next 23 | out/ 24 | 25 | # vscode 26 | .vscode 27 | 28 | # jet brains idea 29 | .idea 30 | 31 | #cache 32 | .cache 33 | lang/.messages 34 | 35 | #source map 36 | *.map 37 | 38 | #typescript compiled server file 39 | server/**/*.js 40 | 41 | #jest test coverage 42 | coverage -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Dependency directories 9 | node_modules/ 10 | .idea/ 11 | 12 | # Optional npm cache directory 13 | .npm 14 | 15 | # locking file 16 | package-lock.json 17 | yarn.lock 18 | 19 | # dotenv environment variables file 20 | .env 21 | 22 | # next.js build output 23 | .next 24 | out/ 25 | 26 | # vscode 27 | .vscode 28 | 29 | # jet brains idea 30 | .idea 31 | 32 | #cache 33 | .cache 34 | lang/.messages 35 | 36 | #source map 37 | *.map 38 | 39 | #typescript compiled server file 40 | server/**/*.js 41 | 42 | #jest test coverage 43 | coverage -------------------------------------------------------------------------------- /actions/sample.ts: -------------------------------------------------------------------------------- 1 | export const actionTypes = { 2 | SAMPLE_API_REQUEST: 'SAMPLE_API_REQUEST', 3 | SAMPLE_API_SUCCESS: 'SAMPLE_API_SUCCESS', 4 | SAMPLE_API_FAILURE: 'SAMPLE_API_FAILURE', 5 | }; 6 | 7 | export function sampleApiRequest () { 8 | return { 9 | type: actionTypes.SAMPLE_API_REQUEST, 10 | } 11 | } 12 | 13 | export function sampleApiSuccess (res) { 14 | return { 15 | type: actionTypes.SAMPLE_API_SUCCESS, 16 | res 17 | } 18 | } 19 | 20 | export function sampleApiFailure (err) { 21 | return { 22 | type: actionTypes.SAMPLE_API_FAILURE, 23 | err 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "esnext", 6 | "jsx": "preserve", 7 | "allowJs": true, 8 | "moduleResolution": "node", 9 | "allowSyntheticDefaultImports": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "removeComments": false, 13 | "preserveConstEnums": true, 14 | "sourceMap": true, 15 | "skipLibCheck": true, 16 | "baseUrl": ".", 17 | "typeRoots": [ 18 | "./node_modules/@types" 19 | ], 20 | "lib": [ 21 | "dom", 22 | "es2015", 23 | "es2016", 24 | "es2017" 25 | ] 26 | } 27 | } -------------------------------------------------------------------------------- /components/sample/Sample.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { $themeColor } from '../../styles/theme' 4 | 5 | const Container = styled.div` 6 | padding: 5px 10px 6px; 7 | border-radius: 13px; 8 | color: ${$themeColor}; 9 | font-size: 11px; 10 | text-align: center; 11 | `; 12 | 13 | interface ISample { 14 | txt: string; 15 | } 16 | 17 | class Sample extends React.Component { 18 | render() { 19 | const {txt} = this.props; 20 | return ( 21 | 22 | {txt} 23 | 24 | ) 25 | } 26 | } 27 | 28 | export default Sample; -------------------------------------------------------------------------------- /server/api/service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Server 3 | */ 4 | 5 | import axios from 'axios'; 6 | 7 | export default ({uri, method, params = null, data = null, headers = null}, res) => { 8 | 9 | const respond = (response) => { 10 | res.json(response.data); 11 | }; 12 | 13 | const onNetworkError = (error) => { 14 | res.status(error.response.status).json({ 15 | message: error.message 16 | }); 17 | }; 18 | 19 | axios({ 20 | url: `${process.env.API_URL}${encodeURI(uri)}`, 21 | method: method, 22 | params: params, 23 | data: data, 24 | headers: headers 25 | }).then(respond).catch(onNetworkError); 26 | 27 | }; -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "next/babel", 4 | "@zeit/next-typescript/babel" 5 | ], 6 | "plugins": [ 7 | "styled-components", 8 | "inline-react-svg" 9 | ], 10 | "env": { 11 | "development": { 12 | "plugins": [ 13 | "react-intl" 14 | ] 15 | }, 16 | "production": { 17 | "plugins": [ 18 | "react-intl" 19 | ] 20 | }, 21 | "test": { 22 | "presets": [ 23 | [ 24 | "next/babel", 25 | { 26 | "preset-env": { 27 | "modules": "commonjs" 28 | } 29 | } 30 | ], 31 | "@zeit/next-typescript/babel" 32 | ] 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /pages/_error.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled, { css } from 'styled-components' 3 | import { $themeColor, $themeCommon } from '../styles/theme' 4 | import { trackerOopsVisit } from '../lib/tracker' 5 | 6 | const Container = styled.div`` 7 | 8 | export default class Error extends React.Component { 9 | static getInitialProps({ res, err }) { 10 | const statusCode = res ? res.statusCode : err ? err.statusCode : null 11 | return { statusCode } 12 | } 13 | render() { 14 | const { statusCode }: any = this.props 15 | return ( 16 | 17 | Error! 18 | {statusCode} 19 | 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /styles/theme.ts: -------------------------------------------------------------------------------- 1 | import theme from 'styled-theming'; 2 | 3 | //공통 4 | export const $themeCommon = { 5 | //state 6 | plus : '#2bb015', 7 | minus : '#bb200f', 8 | record :'#2887ff', 9 | }; 10 | 11 | export const $themeColor = theme('theme', { 12 | light: "#31313c", 13 | dark: "#ffffff" 14 | }); 15 | 16 | export const $themeColor2 = theme('theme', { 17 | light: "#ffffff", 18 | dark: "#31313c" 19 | }); 20 | 21 | export const $themeBgDoc = theme('theme', { 22 | light: "#f0f0f0", 23 | dark: "#1c1c1f" 24 | }); 25 | export const $themeFont = theme('theme', { 26 | light: "#43e4e6", 27 | dark: "#dee623" 28 | }); 29 | export const $themeBg = theme('theme', { 30 | light: "#1318f0", 31 | dark: "#e66e01" 32 | }); -------------------------------------------------------------------------------- /store/index.ts: -------------------------------------------------------------------------------- 1 | import {createStore, applyMiddleware} from 'redux' 2 | import createSagaMiddleware from 'redux-saga' 3 | import rootReducers, {rootInitialState} from '../reducers' 4 | import rootSaga from '../sagas' 5 | 6 | const sagaMiddleware = createSagaMiddleware() 7 | 8 | const bindMiddleware = (middleware) => { 9 | if (process.env.NODE_ENV !== 'production') { 10 | const { composeWithDevTools } = require('redux-devtools-extension') 11 | return composeWithDevTools(applyMiddleware(...middleware)) 12 | } 13 | return applyMiddleware(...middleware) 14 | } 15 | 16 | function configureStore (initialState = rootInitialState) { 17 | const store = createStore( 18 | rootReducers, 19 | initialState, 20 | bindMiddleware([sagaMiddleware]) 21 | ) 22 | 23 | store.runSagaTask = () => { 24 | store.sagaTask = sagaMiddleware.run(rootSaga) 25 | } 26 | 27 | store.runSagaTask() 28 | return store 29 | } 30 | 31 | export default configureStore -------------------------------------------------------------------------------- /pages/param.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import styled from 'styled-components' 3 | import { $themeColor, $themeColor2, $themeCommon } from '../styles/theme' 4 | import Layout, {ILayout} from '../components/Layout' 5 | 6 | const Container = styled.div`` 7 | const H2 = styled.h2` 8 | background-color: ${$themeColor}; 9 | color: ${$themeColor2}; 10 | ` 11 | const ParamValue = styled.h2` 12 | background-color: ${$themeCommon.record}; 13 | color: #ffffff; 14 | ` 15 | 16 | interface IParam extends ILayout { 17 | something: string; 18 | } 19 | 20 | class Param extends Component { 21 | static getInitialProps({ ctx }) { 22 | const { query } = ctx 23 | const { something } = query 24 | return { something } 25 | } 26 | render() { 27 | const { something } = this.props; 28 | return ( 29 | 30 | 31 |

Param Page

32 | 33 | something : {something} 34 | 35 |
36 |
37 | ) 38 | } 39 | } 40 | 41 | export default Param 42 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import styled from 'styled-components' 3 | import Layout, {ILayout} from '../components/Layout' 4 | import { $themeColor, $themeColor2 } from '../styles/theme' 5 | import * as sampleActions from '../actions/sample' 6 | import Sample from '../containers/Sample' 7 | 8 | const Container = styled.div`` 9 | const H2 = styled.h2` 10 | background-color: ${$themeColor}; 11 | color: ${$themeColor2}; 12 | ` 13 | 14 | interface IIndex extends ILayout { 15 | common_referrer: string; 16 | } 17 | 18 | class Index extends Component { 19 | static getInitialProps({ ctx }) { 20 | const { store } = ctx 21 | const state = store.getState(); 22 | const sample_api_loaded = state.getIn(['sample', 'sample_api', 'loaded']); 23 | if(!sample_api_loaded) { 24 | store.dispatch(sampleActions.sampleApiRequest()) 25 | } 26 | } 27 | render() { 28 | return ( 29 | 30 | 31 |

Index Page

32 | 33 |
34 |
35 | ) 36 | } 37 | } 38 | 39 | export default Index 40 | -------------------------------------------------------------------------------- /components/sample/__tests__/Login.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {shallow} from 'enzyme'; 3 | 4 | import Login from './../Login'; 5 | 6 | describe('Login', () => { 7 | it('renders the h1 title', () => { 8 | const login = shallow(); 9 | expect(login.find('h1').text()).toEqual('Login'); 10 | }); 11 | 12 | it('renders the form', () => { 13 | const login = shallow(); 14 | expect(login.find('form')).toHaveLength(1); 15 | }); 16 | 17 | it('changes the text of email', () => { 18 | const login = shallow(); 19 | login 20 | .find('#formEmail') 21 | .simulate('change', { 22 | target: { 23 | name: 'email', 24 | value: 'some@test.com' 25 | } 26 | }); 27 | expect(login.update().find('#formEmail').props().value).toEqual('some@test.com'); 28 | }); 29 | 30 | it('changes the text of login button after clicking it', () => { 31 | const login = shallow(); 32 | login 33 | .find('#loginSubmit') 34 | .simulate('click', {preventDefault() {}}); 35 | expect(login.update().find('#loginSubmit').text()).toEqual('Logging in...'); 36 | }); 37 | }); -------------------------------------------------------------------------------- /reducers/sample.ts: -------------------------------------------------------------------------------- 1 | import {actionTypes} from '../actions/sample' 2 | import { fromJS, Map } from 'immutable' 3 | 4 | export const initialState = Map({ 5 | sample_api: Map({ 6 | status: 0, 7 | loading: false, 8 | loaded: false, 9 | data: Map({}) 10 | }), 11 | }) 12 | 13 | function reducer (state = initialState, action) { 14 | switch (action.type) { 15 | case actionTypes.SAMPLE_API_REQUEST: 16 | return state 17 | .setIn(["sample_api", "loading"], true) 18 | .setIn(["sample_api", "loaded"], false); 19 | case actionTypes.SAMPLE_API_SUCCESS: 20 | return state 21 | .setIn(["sample_api", "status"], action.res.status) 22 | .setIn(["sample_api", "loading"], false) 23 | .setIn(["sample_api", "loaded"], true) 24 | .setIn(["sample_api", "data"], fromJS(action.res.data)); 25 | case actionTypes.SAMPLE_API_FAILURE: 26 | return state 27 | .setIn(["sample_api", "status"], action.err.response.status) 28 | .setIn(["sample_api", "loading"], false) 29 | .setIn(["sample_api", "loaded"], true) 30 | .setIn(["sample_api", "error"], fromJS(action.err.response)) 31 | default: 32 | return state 33 | } 34 | } 35 | 36 | export default reducer -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const Dotenv = require('dotenv-webpack'); 3 | const withTypescript = require('@zeit/next-typescript'); 4 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 5 | 6 | const dev = process.env.NODE_ENV !== "production"; 7 | const {CDN_URL} = process.env; 8 | 9 | 10 | 11 | module.exports = withTypescript({ 12 | assetPrefix: CDN_URL, 13 | webpack: function(config) { 14 | config.plugins = config.plugins || [] 15 | 16 | config.plugins = [ 17 | ...config.plugins, 18 | 19 | // Read the .env file 20 | new Dotenv({ 21 | path: path.join(__dirname, '.env'), 22 | systemvars: true 23 | }), 24 | 25 | !dev && new UglifyJsPlugin({ 26 | parallel: true, 27 | uglifyOptions: { 28 | compress: { 29 | warnings: false, 30 | drop_console: true 31 | } 32 | }, 33 | sourceMap: false 34 | }) 35 | ].filter(plugin => plugin) 36 | 37 | const originalEntry = config.entry 38 | config.entry = async () => { 39 | const entries = await originalEntry() 40 | 41 | if (entries['main.js'] && !entries['main.js'].includes('./client/polyfills.js')) { 42 | entries['main.js'].unshift('./client/polyfills.js') 43 | } 44 | 45 | return entries 46 | } 47 | 48 | return config; 49 | } 50 | }); -------------------------------------------------------------------------------- /components/SelectLanguage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { List, Map } from 'immutable' 3 | import Cookies from 'js-cookie'; 4 | 5 | interface ISelectLanguage { 6 | lang: string; 7 | } 8 | 9 | class SelectLanguage extends React.Component { 10 | state = { 11 | languages: List([ 12 | Map({ 13 | id: 'KO', 14 | value: 'ko', 15 | text: '한국어' 16 | }), 17 | Map({ 18 | id: 'EN', 19 | value: 'en', 20 | text: 'English' 21 | }) 22 | ]) 23 | } 24 | onChange = (e) => { 25 | const lang = e.target.value 26 | Cookies.set('lang', lang); 27 | window.location.reload() 28 | } 29 | render() { 30 | const { 31 | languages 32 | } = this.state; 33 | const { 34 | lang 35 | } = this.props; 36 | const current_language = languages.find(v => v.get('value') === lang).get('value') 37 | return ( 38 | 45 | ); 46 | } 47 | } 48 | 49 | export default SelectLanguage; -------------------------------------------------------------------------------- /pages/other.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import styled from 'styled-components' 3 | import { $themeBg, $themeColor, $themeColor2, $themeCommon, $themeFont } from '../styles/theme' 4 | import Layout, {ILayout} from '../components/Layout' 5 | import withIntl from '../lib/withIntl' 6 | import { InjectedIntlProps } from "react-intl" 7 | import Sample from '../containers/Sample' 8 | 9 | const Container = styled.div`` 10 | const H2 = styled.h2` 11 | background-color: ${$themeColor}; 12 | color: ${$themeColor2}; 13 | ` 14 | const TransContent = styled.div` 15 | background-color: ${$themeCommon.plus}; 16 | color: yellow; 17 | ` 18 | const ThemeColorComp = styled.div` 19 | background-color: ${$themeBg}; 20 | color: ${$themeFont}; 21 | ` 22 | 23 | interface IOther extends ILayout, InjectedIntlProps {} 24 | 25 | class Other extends Component { 26 | static getInitialProps() {} 27 | render() { 28 | const {intl} = this.props; 29 | return ( 30 | 31 | 32 |

Other Page

33 | {intl.formatMessage({id: "sample.boilerplate"})} 34 | 1234567890 35 | 36 |
37 |
38 | ) 39 | } 40 | } 41 | 42 | export default withIntl(Other) 43 | -------------------------------------------------------------------------------- /pages/path/depth.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import styled from 'styled-components' 3 | import { $themeColor, $themeColor2 } from '../../styles/theme' 4 | import Layout, {ILayout} from '../../components/Layout' 5 | import { connect } from 'react-redux' 6 | import * as sampleActions from '../../actions/sample' 7 | import {Map} from 'immutable'; 8 | import Sample from '../../containers/Sample' 9 | 10 | const Container = styled.div`` 11 | const H2 = styled.h2` 12 | background-color: ${$themeColor}; 13 | color: ${$themeColor2}; 14 | ` 15 | 16 | interface IDepth extends ILayout { 17 | sample_api_loaded: Map; 18 | sampleApiRequest(): void; 19 | } 20 | 21 | class Depth extends Component { 22 | static getInitialProps() {} 23 | componentDidMount() { 24 | const {sample_api_loaded, sampleApiRequest} = this.props; 25 | if(!sample_api_loaded) { 26 | sampleApiRequest(); 27 | } 28 | } 29 | render() { 30 | return ( 31 | 32 | 33 |

Depth Page

34 | 35 |
36 |
37 | ) 38 | } 39 | } 40 | 41 | const mapStateToProps = state => ({ 42 | sample_api_loaded: state.getIn([ 43 | 'sample', 44 | 'sample_api', 45 | 'loaded' 46 | ]), 47 | }) 48 | const mapDispatchToProps = (dispatch) => ({ 49 | sampleApiRequest: () => dispatch(sampleActions.sampleApiRequest()), 50 | }); 51 | 52 | export default connect( 53 | mapStateToProps, 54 | mapDispatchToProps 55 | )(Depth) 56 | -------------------------------------------------------------------------------- /contexts/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, {Component, createContext} from 'react'; 2 | import { ThemeProvider as StyledComponentsThemeProvider } from "styled-components"; 3 | import Cookies from 'js-cookie'; 4 | import GlobalStyle from "../styles/global"; 5 | 6 | const Context = createContext({ 7 | state: { 8 | theme: 'light' 9 | }, 10 | actions: { 11 | changeTheme: () => {} 12 | } 13 | }); 14 | 15 | 16 | const {Provider, Consumer: ThemeConsumer} = Context; 17 | 18 | interface IThemeProvider { 19 | theme: string, 20 | } 21 | 22 | /** 23 | * @TODO: context api로 테마 사용하는 예제 추가, (작업하면서 언어 바꾸는 예제도 같이 할꺼임.) 24 | */ 25 | class ThemeProvider extends Component { 26 | state = { 27 | theme: this.props.theme 28 | } 29 | actions = { 30 | changeTheme: () => { 31 | const theme = this.state.theme !== 'light' ? 'light' : 'dark'; 32 | Cookies.set('theme', theme); 33 | this.setState({theme}) 34 | }, 35 | } 36 | componentDidMount() { 37 | const {theme} = this.props; 38 | this.setState({theme}) 39 | } 40 | render() { 41 | const {state, actions} = this; 42 | const value = {state, actions}; 43 | const {theme} = state; 44 | 45 | return ( 46 | 47 | <> 48 | 49 | {this.props.children} 50 | 51 | 52 | ) 53 | } 54 | } 55 | 56 | export default ThemeProvider 57 | export {ThemeConsumer} -------------------------------------------------------------------------------- /containers/Sample.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import withIntl from '../lib/withIntl' 3 | import { connect } from 'react-redux' 4 | import * as sampleActions from '../actions/sample' 5 | import { Map } from 'immutable' 6 | import styled from 'styled-components' 7 | 8 | const Button = styled.button.attrs({type: "button"})` 9 | padding: 14px; 10 | font-size: 18px; 11 | font-weight: bold; 12 | background-color: #7f7f7f; 13 | color: #ffffff; 14 | 15 | ` 16 | 17 | interface ISample { 18 | sample_api: Map; 19 | sampleApiRequest(): void; 20 | } 21 | 22 | class Sample extends React.Component { 23 | render() { 24 | const { 25 | sample_api, 26 | sampleApiRequest 27 | } = this.props; 28 | const sample_api_loading = sample_api.get('loading'); 29 | const sample_api_data = sample_api.get('data'); 30 | return ( 31 |
32 |
33 | 34 |
35 | {sample_api_loading 36 | ? `loading...` 37 | : `data: ${JSON.stringify(sample_api_data.toJS())}` 38 | } 39 |
40 | ); 41 | } 42 | } 43 | 44 | const mapStateToProps = state => ({ 45 | sample_api: state.getIn([ 46 | 'sample', 47 | 'sample_api', 48 | ]), 49 | }) 50 | const mapDispatchToProps = (dispatch) => ({ 51 | sampleApiRequest: () => dispatch(sampleActions.sampleApiRequest()), 52 | }); 53 | 54 | export default withIntl(connect( 55 | mapStateToProps, 56 | mapDispatchToProps 57 | )(Sample)) -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Document, {Head, Main, NextScript} from 'next/document' 3 | import { ServerStyleSheet } from 'styled-components' 4 | 5 | interface IDocument { 6 | lang: string; 7 | locale: string; 8 | styleTags 9 | localeDataScript: string; 10 | } 11 | 12 | const {CDN_URL} = process.env; 13 | 14 | export default class extends Document { 15 | static async getInitialProps (context) { 16 | const sheet = new ServerStyleSheet() 17 | const props = await super.getInitialProps(context) 18 | const {req: {lang, localeDataScript}, renderPage} = context 19 | const page = renderPage(App => props => sheet.collectStyles()) 20 | const styleTags = sheet.getStyleElement() 21 | 22 | return { 23 | ...props, 24 | ...page, 25 | styleTags, 26 | lang, 27 | localeDataScript 28 | } 29 | } 30 | 31 | render () { 32 | const {lang} = this.props; 33 | const polyfill = `https://cdn.polyfill.io/v2/polyfill.min.js?features=Intl.~locale.${lang}` 34 | 35 | return ( 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | {this.props.styleTags} 44 | 45 | 46 |
47 |