├── .eslintignore ├── config ├── env │ ├── env │ │ ├── development.js │ │ ├── test.js │ │ ├── production.js │ │ └── base.js │ ├── index.js │ └── index.d.ts ├── plugins │ ├── polyfills │ │ ├── polyfills.js │ │ └── index.js │ ├── styled-components-css │ │ └── index.js │ ├── svg-sprite │ │ └── index.js │ ├── version-info │ │ └── index.js │ ├── next-starter-core │ │ └── index.js │ ├── alias │ │ └── index.js │ └── sqip │ │ └── index.js ├── routers.d.ts └── routers.js ├── src ├── pages │ ├── version.tsx │ ├── _app.tsx │ ├── index.tsx │ └── _document.tsx ├── components │ ├── base │ │ ├── IcoIcon │ │ │ ├── index.jsx │ │ │ ├── index.d.ts │ │ │ ├── IcoIcon.d.ts │ │ │ └── IcoIcon.jsx │ │ ├── Typography │ │ │ └── index.tsx │ │ ├── Empty │ │ │ └── index.tsx │ │ ├── Icon │ │ │ └── index.tsx │ │ ├── Link │ │ │ ├── formatUrl.ts │ │ │ ├── index.tsx │ │ │ └── __test__ │ │ │ │ └── formatUrl.test.js │ │ └── Image.tsx │ └── expand │ │ ├── Normalize.tsx │ │ ├── SvgSprite.tsx │ │ ├── GlobalStyle.tsx │ │ └── MuiProvider.tsx ├── resource │ ├── images │ │ └── cat.png │ └── icon │ │ └── success.svg ├── types │ └── redux.ts ├── utils │ ├── canUseDom.ts │ ├── __tests__ │ │ └── theme.test.ts │ └── theme.ts ├── redux │ ├── reducers.ts │ ├── app │ │ ├── reducers.ts │ │ └── actions.ts │ └── store.ts ├── containers │ ├── withRoot.tsx │ └── Version.tsx └── api │ └── base │ └── index.js ├── .prettierrc ├── public └── images │ └── cat.jpg ├── README.md ├── .gitignore ├── .stylelintrc.js ├── next-env.d.ts ├── .editorconfig ├── .babelrc ├── .eslintrc.js ├── next.config.js ├── script └── upload.js ├── server.js ├── tsconfig.json └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | !.*.js 2 | -------------------------------------------------------------------------------- /config/env/env/development.js: -------------------------------------------------------------------------------- 1 | module.exports = {} 2 | -------------------------------------------------------------------------------- /config/env/env/test.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | port: '' 3 | } 4 | -------------------------------------------------------------------------------- /config/plugins/polyfills/polyfills.js: -------------------------------------------------------------------------------- 1 | import '@babel/polyfill' 2 | -------------------------------------------------------------------------------- /src/pages/version.tsx: -------------------------------------------------------------------------------- 1 | export { default } from 'CONTAINERS/Version' 2 | -------------------------------------------------------------------------------- /src/components/base/IcoIcon/index.jsx: -------------------------------------------------------------------------------- 1 | export { default } from './IcoIcon' 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "jsxSingleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /public/images/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dragon0513/next-starter/HEAD/public/images/cat.jpg -------------------------------------------------------------------------------- /src/components/base/IcoIcon/index.d.ts: -------------------------------------------------------------------------------- 1 | export { default } from './IcoIcon' 2 | export * from './IcoIcon' 3 | -------------------------------------------------------------------------------- /src/resource/images/cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dragon0513/next-starter/HEAD/src/resource/images/cat.png -------------------------------------------------------------------------------- /src/components/base/Typography/index.tsx: -------------------------------------------------------------------------------- 1 | import Typography from '@material-ui/core/Typography' 2 | 3 | export default Typography 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next Starter 2 | 3 | ## master 4 | 5 | - Next.js 6 | - React 7 | - Redux 8 | - Styled-Components 9 | - Material-UI 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | .cache 4 | .next/ 5 | .out/ 6 | yarn-error.log 7 | yarn.lock 8 | out/ 9 | .idea/ 10 | *.DS_Store 11 | coverage/ 12 | -------------------------------------------------------------------------------- /config/env/env/production.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | port: '', 3 | service: { 4 | oss: { 5 | bucket: '', 6 | region: '' 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/types/redux.ts: -------------------------------------------------------------------------------- 1 | export interface AppStateTypes { 2 | language: string 3 | level1: any 4 | } 5 | 6 | export interface RootStateTypes { 7 | app: AppStateTypes 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/canUseDom.ts: -------------------------------------------------------------------------------- 1 | const canUseDom = !!( 2 | typeof window !== 'undefined' && 3 | window.document && 4 | window.document.createElement 5 | ) 6 | export default canUseDom 7 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | processors: ['stylelint-processor-styled-components'], 3 | extends: ['stylelint-config-standard', 'stylelint-config-styled-components'] 4 | } 5 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | declare module '*.png' { 5 | const src: string 6 | export default src 7 | } 8 | -------------------------------------------------------------------------------- /src/redux/reducers.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import useReduxControl from 'redux-control' 3 | 4 | import app from './app/reducers' 5 | 6 | export default useReduxControl(combineReducers({ app })) 7 | -------------------------------------------------------------------------------- /config/routers.d.ts: -------------------------------------------------------------------------------- 1 | export interface Routers { 2 | [key: string]: { 3 | page: string 4 | query: { 5 | [queryKey: string]: any 6 | } 7 | } 8 | } 9 | 10 | const routers: Routers 11 | 12 | export default routers 13 | -------------------------------------------------------------------------------- /config/env/index.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const common = require('./env/base') 3 | const env = process.env.BUILD_ENV || 'development' 4 | const config = require(`./env/${env}`) 5 | 6 | module.exports = _.merge(common, config, { env }) 7 | -------------------------------------------------------------------------------- /src/components/base/IcoIcon/IcoIcon.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export interface IcoIconProps { 4 | href: string 5 | } 6 | 7 | declare const IcoIcon: React.ComponentType 8 | 9 | export default IcoIcon 10 | -------------------------------------------------------------------------------- /src/components/expand/Normalize.tsx: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import normalize from 'normalize.css' 3 | import { createGlobalStyle } from 'styled-components' 4 | 5 | const Normalize = createGlobalStyle` 6 | ${normalize}; 7 | ` 8 | 9 | export default Normalize 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.py] 15 | indent_size = 4 -------------------------------------------------------------------------------- /config/env/env/base.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | port: '3000', 3 | domain: 'localhost', 4 | host: 'localhost:3000', 5 | staticSuffix: '', 6 | exportTarget: 'inner', 7 | service: { 8 | main: '//localhost:3000/api/v1', 9 | oss: { 10 | bucket: '', 11 | region: '' 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": [ 4 | "@babel/plugin-proposal-class-properties", 5 | [ 6 | "babel-plugin-styled-components", 7 | { 8 | "minify": true, 9 | "ssr": true, 10 | "displayName": true, 11 | "preprocess": false 12 | } 13 | ] 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /config/env/index.d.ts: -------------------------------------------------------------------------------- 1 | export interface ENV { 2 | port: string 3 | domain: string 4 | host: string 5 | target?: string 6 | staticSuffix: string 7 | exportTarget: string 8 | service: { 9 | mian: string 10 | oss: { 11 | backet: string 12 | region: string 13 | } 14 | } 15 | env: string 16 | } 17 | 18 | declare const env: ENV 19 | 20 | export default env 21 | -------------------------------------------------------------------------------- /src/components/base/Empty/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | interface EmptyProps extends React.HTMLAttributes { 5 | readonly width?: string 6 | readonly height?: string 7 | } 8 | 9 | const Empty = styled.div` 10 | width: ${props => props.width}; 11 | height: ${props => props.height}; 12 | ` 13 | 14 | export default Empty 15 | -------------------------------------------------------------------------------- /src/components/base/IcoIcon/IcoIcon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function IcoIcon (props) { 4 | const { href } = props 5 | 6 | return ( 7 | <> 8 | 9 | 10 | 11 | 12 | 13 | ) 14 | } 15 | 16 | export default IcoIcon 17 | -------------------------------------------------------------------------------- /config/plugins/styled-components-css/index.js: -------------------------------------------------------------------------------- 1 | const cssLoader = { 2 | test: /\.css$/, 3 | use: [{ loader: 'raw-loader' }] 4 | } 5 | 6 | module.exports = (nextConfig = {}) => { 7 | return Object.assign({}, nextConfig, { 8 | webpack (webpackConfig, options) { 9 | webpackConfig.module.rules.push(cssLoader) 10 | 11 | if (typeof nextConfig.webpack === 'function') { 12 | return nextConfig.webpack(webpackConfig, options) 13 | } 14 | 15 | return Object.assign({}, webpackConfig) 16 | } 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /src/components/expand/SvgSprite.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | // @ts-ignore 3 | import sprite from 'svg-sprite-loader/runtime/sprite.build' 4 | 5 | // @ts-ignore 6 | const requireAll = requireContext => requireContext.keys().map(requireContext) 7 | // @ts-ignore 8 | const req = require.context('RESOURCE/icon', true, /\.svg$/) 9 | requireAll(req) 10 | 11 | const spriteContent = sprite.stringify() 12 | 13 | const SvgSprite = () => { 14 | return
15 | } 16 | 17 | export default SvgSprite 18 | -------------------------------------------------------------------------------- /src/redux/app/reducers.ts: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions' 2 | 3 | const initialState = { 4 | language: '', 5 | level1: { 6 | level2: { 7 | level3: { 8 | name: '???' 9 | } 10 | }, 11 | array: [ 12 | { 13 | name: 1 14 | }, 15 | { 16 | name: 2 17 | } 18 | ] 19 | } 20 | } 21 | 22 | const appReducers = handleActions( 23 | { 24 | INIT_APP: state => { 25 | return state 26 | } 27 | }, 28 | initialState 29 | ) 30 | 31 | export default appReducers 32 | -------------------------------------------------------------------------------- /config/plugins/svg-sprite/index.js: -------------------------------------------------------------------------------- 1 | const svgLoader = { 2 | test: /\.svg$/, 3 | use: [{ loader: 'svg-sprite-loader', options: {} }, 'svgo-loader'] 4 | } 5 | 6 | module.exports = (nextConfig = {}) => { 7 | return Object.assign({}, nextConfig, { 8 | webpack (webpackConfig, options) { 9 | webpackConfig.module.rules.push(svgLoader) 10 | 11 | if (typeof nextConfig.webpack === 'function') { 12 | return nextConfig.webpack(webpackConfig, options) 13 | } 14 | 15 | return Object.assign({}, webpackConfig) 16 | } 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /src/components/expand/GlobalStyle.tsx: -------------------------------------------------------------------------------- 1 | import { breakpointsDown } from 'UTILS/theme' 2 | import { createGlobalStyle } from 'styled-components' 3 | 4 | const GlobalStyle = createGlobalStyle` 5 | html { 6 | ${breakpointsDown('sm')} { 7 | font-size: 14px; 8 | } 9 | } 10 | 11 | body { 12 | background: #fafcfe; 13 | overflow-x: hidden; 14 | font-family: "PingFangSC-Light", "Helvetica Neue", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif; 15 | } 16 | 17 | #__next { 18 | display: flex; 19 | flex-direction: column; 20 | } 21 | ` 22 | 23 | export default GlobalStyle 24 | -------------------------------------------------------------------------------- /config/plugins/version-info/index.js: -------------------------------------------------------------------------------- 1 | const { DefinePlugin } = require('webpack') 2 | 3 | module.exports = (nextConfig = {}) => { 4 | return Object.assign({}, nextConfig, { 5 | webpack (webpackConfig, options) { 6 | webpackConfig.plugins.push( 7 | new DefinePlugin({ 8 | 'process.env.BUILD_ENV': JSON.stringify(process.env.BUILD_ENV) 9 | }) 10 | ) 11 | 12 | webpackConfig.output.jsonpFunction = 'webpackJsonp_next_starter' 13 | 14 | if (typeof nextConfig.webpack === 'function') { 15 | return nextConfig.webpack(webpackConfig, options) 16 | } 17 | 18 | return Object.assign({}, webpackConfig) 19 | } 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | env: { 4 | browser: true, 5 | es6: true, 6 | jest: true 7 | }, 8 | extends: ['standard', 'plugin:react/recommended'], 9 | globals: { 10 | Atomics: 'readonly', 11 | SharedArrayBuffer: 'readonly' 12 | }, 13 | parserOptions: { 14 | ecmaFeatures: { 15 | jsx: true 16 | }, 17 | ecmaVersion: 2018, 18 | sourceType: 'module' 19 | }, 20 | plugins: ['@typescript-eslint'], 21 | rules: { 22 | 'react/prop-types': 0, 23 | '@typescript-eslint/no-unused-vars': 2 24 | }, 25 | settings: { 26 | react: { 27 | pragma: 'React', 28 | version: 'detect' 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/redux/app/actions.ts: -------------------------------------------------------------------------------- 1 | import { createActions } from 'redux-actions' 2 | import { tryToFetch } from 'redux-control' 3 | import { ThunkAction } from 'redux-thunk' 4 | import { RootStateTypes } from 'TYPES/redux' 5 | 6 | const { initApp } = createActions('INIT_APP') 7 | 8 | const getDate = () => 9 | new Promise(resolve => { 10 | setTimeout(() => { 11 | resolve({ name: 'redux' }) 12 | }, 1000) 13 | }) 14 | 15 | const tryToFetchLocationsConfig: () => ThunkAction< 16 | void, 17 | RootStateTypes, 18 | void, 19 | any 20 | > = () => 21 | tryToFetch({ 22 | path: 'app.load', 23 | fetchFunc: getDate, 24 | ttl: 10000 25 | }) 26 | 27 | export default { 28 | initApp, 29 | tryToFetchLocationsConfig 30 | } 31 | -------------------------------------------------------------------------------- /src/redux/store.ts: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, Store, Middleware } from 'redux' 2 | import { autoDispatch } from 'redux-control' 3 | 4 | import thunk from 'redux-thunk' 5 | import { createLogger } from 'redux-logger' 6 | import env from 'CONFIG/env' 7 | 8 | import reducers from '../redux/reducers' 9 | 10 | const middleware: Middleware[] = [thunk] 11 | 12 | if (env.env !== 'production') { 13 | middleware.push(createLogger({})) 14 | } 15 | 16 | let REDUX_STORE: Store | null = null 17 | 18 | const getStore: () => Store = () => { 19 | if (!REDUX_STORE) { 20 | REDUX_STORE = createStore(reducers, applyMiddleware(...middleware)) 21 | autoDispatch(REDUX_STORE) 22 | } 23 | return REDUX_STORE as Store 24 | } 25 | 26 | export default getStore 27 | -------------------------------------------------------------------------------- /src/components/expand/MuiProvider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { ThemeProvider, StylesProvider } from '@material-ui/styles' 4 | import theme from 'UTILS/theme' 5 | 6 | class MuiProvider extends React.PureComponent { 7 | componentDidMount () { 8 | // Remove the server-side injected CSS. 9 | const jssStyles = document.querySelector('#jss-server-side') 10 | if (jssStyles && jssStyles.parentNode) { 11 | jssStyles.parentNode.removeChild(jssStyles) 12 | } 13 | } 14 | 15 | render () { 16 | const { children } = this.props 17 | 18 | return ( 19 | 20 | {children} 21 | 22 | ) 23 | } 24 | } 25 | 26 | export default MuiProvider 27 | -------------------------------------------------------------------------------- /config/routers.js: -------------------------------------------------------------------------------- 1 | // const config = require('../config/env') 2 | // const { staticSuffix } = config 3 | 4 | const routers = [ 5 | '/index', 6 | '/version' 7 | // { key: '/api', page: '/apiPage' } 8 | // { key: '/weather/api', page: '/redirect/api' }, 9 | ] 10 | 11 | module.exports = routers.reduce((obj, item) => { 12 | const router = { ...obj } 13 | 14 | if (typeof item === 'string') { 15 | router[item] = { page: item } 16 | // TODO: check 17 | // if (staticSuffix && staticSuffix !== '') { 18 | // router[`${item}${staticSuffix}`] = { page: item } 19 | // } 20 | } else { 21 | const { key, page, ...others } = item 22 | router[key] = { page, query: others } 23 | // TODO: check 24 | // router[`${key}${staticSuffix}`] = { page, query: others } 25 | } 26 | 27 | return router 28 | }, {}) 29 | -------------------------------------------------------------------------------- /config/plugins/polyfills/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const polyfillPath = path.resolve(__dirname, './polyfills.js') 4 | 5 | module.exports = (nextConfig = {}) => { 6 | return Object.assign({}, nextConfig, { 7 | webpack (webpackConfig, options) { 8 | const originalEntry = webpackConfig.entry 9 | webpackConfig.entry = async () => { 10 | const entries = await originalEntry() 11 | 12 | if (entries['main.js'] && !entries['main.js'].includes(polyfillPath)) { 13 | entries['main.js'].unshift(polyfillPath) 14 | } 15 | 16 | return entries 17 | } 18 | 19 | if (typeof nextConfig.webpack === 'function') { 20 | return nextConfig.webpack(webpackConfig, options) 21 | } 22 | 23 | return Object.assign({}, webpackConfig) 24 | } 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const routers = require('./config/routers') 2 | const withPlugins = require('next-compose-plugins') 3 | 4 | const withVersionInfo = require('./config/plugins/version-info') 5 | const withPolyfills = require('./config/plugins/polyfills') 6 | const withCss = require('./config/plugins/styled-components-css') 7 | const withSvgSprite = require('./config/plugins/svg-sprite') 8 | const withSqip = require('./config/plugins/sqip') 9 | const withNextStarter = require('./config/plugins/next-starter-core') 10 | const withAlias = require('./config/plugins/alias') 11 | 12 | module.exports = withPlugins( 13 | [ 14 | withAlias, 15 | withVersionInfo, 16 | withPolyfills, 17 | withCss, 18 | withSvgSprite, 19 | withSqip, 20 | withNextStarter, 21 | { versionInfoOptions: {} } 22 | ], 23 | { 24 | exportPathMap: async function () { 25 | return routers 26 | } 27 | } 28 | ) 29 | -------------------------------------------------------------------------------- /src/containers/withRoot.tsx: -------------------------------------------------------------------------------- 1 | import AppActions from 'REDUX/app/actions' 2 | import React from 'react' 3 | import { connect } from 'react-redux' 4 | import { bindActionCreators, Dispatch } from 'redux' 5 | import { RootStateTypes } from 'TYPES/redux' 6 | 7 | const withRoot = (Component: React.ElementType) => { 8 | class WithRoot extends React.PureComponent { 9 | render () { 10 | console.log(this.props) 11 | const { ...props } = this.props 12 | 13 | return 14 | } 15 | } 16 | 17 | const mapStateToProps = (state: RootStateTypes) => { 18 | const { app } = state 19 | 20 | return { 21 | app 22 | } 23 | } 24 | 25 | const mapDispatchToProps = (dispatch: Dispatch) => ({ 26 | actions: bindActionCreators(AppActions, dispatch) 27 | }) 28 | 29 | return connect(mapStateToProps, mapDispatchToProps)(WithRoot) 30 | } 31 | 32 | export default withRoot 33 | -------------------------------------------------------------------------------- /script/upload.js: -------------------------------------------------------------------------------- 1 | const env = require('../config/env') 2 | const path = require('path') 3 | const OssCdnHelper = require('oss-cdn-helper').default 4 | 5 | const { service, host } = env 6 | const { oss } = service 7 | 8 | const process = async () => { 9 | const uploadFiles = await OssCdnHelper.upload({ 10 | uploadPath: path.resolve(__dirname, '../out'), 11 | targetPath: '', 12 | region: oss.bucket, 13 | bucket: oss.region, 14 | removeHtmlSuffix: true, 15 | removeHtmlSuffixIgnore: ['/index.html'], 16 | cleanTargetPath: false 17 | }) 18 | 19 | const paths = [`http://${host}/`, `https://${host}/`] 20 | const files = uploadFiles 21 | .filter(file => file.filePath.match(/.html$/)) 22 | .map( 23 | file => `http://${host}${file.fileName} \nhttps://${host}${file.fileName}` 24 | ) 25 | 26 | await OssCdnHelper.refresh({ 27 | enabled: false, 28 | log: true, 29 | paths, 30 | files 31 | }) 32 | } 33 | 34 | process() 35 | -------------------------------------------------------------------------------- /src/components/base/Icon/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import styled from 'styled-components' 3 | 4 | interface MySVGProps extends React.SVGAttributes { 5 | width?: string 6 | height?: string 7 | m?: string 8 | color?: string 9 | } 10 | 11 | export interface IconProps extends MySVGProps { 12 | name: string 13 | } 14 | 15 | const MySVG: FC = ({ width, height, m, color, ...other }) => ( 16 | 17 | ) 18 | 19 | const SVG = styled(MySVG)` 20 | width: ${props => props.width || '1em'}; 21 | height: ${props => props.height || '1em'}; 22 | margin: ${props => props.m || 0}; 23 | color: ${props => props.color}; 24 | vertical-align: -0.15em; 25 | fill: currentColor; 26 | overflow: hidden; 27 | flex: 0 0 auto; 28 | ` 29 | 30 | const Icon: FC = ({ name, ...props }) => ( 31 | 34 | ) 35 | 36 | export default Icon 37 | -------------------------------------------------------------------------------- /config/plugins/next-starter-core/index.js: -------------------------------------------------------------------------------- 1 | const { DefinePlugin } = require('webpack') 2 | 3 | module.exports = (nextConfig = {}) => { 4 | return Object.assign({}, nextConfig, { 5 | webpack (webpackConfig, options) { 6 | webpackConfig.plugins.push( 7 | new DefinePlugin({ 8 | 'process.env.npm_package_version': JSON.stringify( 9 | process.env.npm_package_version 10 | ), 11 | 'process.env.BUILD_TIME': JSON.stringify(new Date()), 12 | 'process.env.npm_package_gitHead': JSON.stringify( 13 | process.env.npm_package_gitHead 14 | ), 15 | 'process.env.npm_package_dependencies_react': JSON.stringify( 16 | process.env.npm_package_dependencies_react 17 | ) 18 | }) 19 | ) 20 | 21 | if (typeof nextConfig.webpack === 'function') { 22 | return nextConfig.webpack(webpackConfig, options) 23 | } 24 | 25 | return Object.assign({}, webpackConfig) 26 | } 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /config/plugins/alias/index.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path') 2 | 3 | const ROOT_PATH = resolve(__dirname, '../../../') 4 | const alias = { 5 | API: resolve(ROOT_PATH, './src/api'), 6 | COMPONENTS: resolve(ROOT_PATH, './src/components'), 7 | CONTAINERS: resolve(ROOT_PATH, './src/containers'), 8 | REDUX: resolve(ROOT_PATH, './src/redux'), 9 | RESOURCE: resolve(ROOT_PATH, './src/resource'), 10 | ENV: resolve(ROOT_PATH, './config/env'), 11 | UTILS: resolve(ROOT_PATH, './src/utils'), 12 | CONFIG: resolve(ROOT_PATH, './config') 13 | } 14 | 15 | module.exports = (nextConfig = {}) => { 16 | return Object.assign({}, nextConfig, { 17 | webpack (webpackConfig, options) { 18 | webpackConfig.resolve.alias = { 19 | ...webpackConfig.resolve.alias, 20 | ...alias 21 | } 22 | 23 | if (typeof nextConfig.webpack === 'function') { 24 | return nextConfig.webpack(webpackConfig, options) 25 | } 26 | 27 | return Object.assign({}, webpackConfig) 28 | } 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa') 2 | const next = require('next') 3 | const Router = require('koa-router') 4 | const env = require('./config/env') 5 | 6 | const { port } = env 7 | const dev = process.env.NODE_ENV !== 'production' 8 | const app = next({ dev }) 9 | const handle = app.getRequestHandler() 10 | 11 | const redirects = [ 12 | // { from: '/', to: '/'} 13 | ] 14 | 15 | app.prepare().then(() => { 16 | const server = new Koa() 17 | const router = new Router() 18 | 19 | redirects.forEach(({ from, to, method = 'get' }) => { 20 | router[method](from, async ctx => { 21 | await app.render(ctx.req, ctx.res, to, ctx.query) 22 | ctx.respond = false 23 | }) 24 | }) 25 | 26 | router.get('*', async ctx => { 27 | await handle(ctx.req, ctx.res) 28 | ctx.respond = false 29 | }) 30 | 31 | server.use(async (ctx, next) => { 32 | ctx.res.statusCode = 200 33 | await next() 34 | }) 35 | 36 | server.use(router.routes()) 37 | server.listen(port, () => { 38 | console.log(`> Ready on http://localhost:${port}`) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import MuiProvider from 'COMPONENTS/expand/MuiProvider' 2 | import configureStore from 'REDUX/store' 3 | import App from 'next/app' 4 | import Head from 'next/head' 5 | import React from 'react' 6 | import { Provider } from 'react-redux' 7 | import CssBaseline from '@material-ui/core/CssBaseline' 8 | import { ThemeProvider } from 'styled-components' 9 | import theme from 'UTILS/theme' 10 | 11 | const store = configureStore() 12 | 13 | class MyApp extends App { 14 | render () { 15 | const { Component, pageProps } = this.props 16 | return ( 17 | <> 18 | 19 | next-starter 20 | 21 | 22 | 23 | {/* */} 24 | {/* */} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ) 33 | } 34 | } 35 | 36 | export default MyApp 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "allowJs": true, 5 | "allowSyntheticDefaultImports": true, 6 | "jsx": "preserve", 7 | "lib": ["dom", "es2017"], 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "noEmit": true, 11 | "noUnusedLocals": false, 12 | "noUnusedParameters": false, 13 | "preserveConstEnums": true, 14 | "removeComments": false, 15 | "skipLibCheck": true, 16 | "sourceMap": true, 17 | "strict": true, 18 | "target": "esnext", 19 | "paths": { 20 | "API/*": ["src/api/*"], 21 | "COMPONENTS/*": ["src/components/*"], 22 | "CONTAINERS/*": ["src/containers/*"], 23 | "REDUX/*": ["src/redux/*"], 24 | "UTILS/*": ["src/utils/*"], 25 | "RESOURCE/*": ["src/resource/*"], 26 | "TYPES/*": ["src/types/*"], 27 | "CONFIG/*": ["config/*"] 28 | }, 29 | "forceConsistentCasingInFileNames": true, 30 | "esModuleInterop": true, 31 | "resolveJsonModule": true, 32 | "isolatedModules": true 33 | }, 34 | "exclude": ["node_modules", "dist", ".next", "out", "coverage"], 35 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"] 36 | } 37 | -------------------------------------------------------------------------------- /src/resource/icon/success.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 注册成功-icon 6 | 7 | 8 | Created with Sketch. 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /config/plugins/sqip/index.js: -------------------------------------------------------------------------------- 1 | const { NODE_ENV } = process.env 2 | const isProduction = NODE_ENV === 'production' 3 | const ImageminPlugin = require('imagemin-webpack-plugin').default 4 | 5 | const getImageLoader = config => ({ 6 | test: /\.(png|jpe?g)$/, 7 | use: [ 8 | { 9 | loader: 'sqip-loader', 10 | options: { 11 | numberOfPrimitives: 20, 12 | blur: 4, 13 | skipPreviewIfBase64: true 14 | } 15 | }, 16 | { 17 | loader: 'url-loader', 18 | options: { 19 | fallback: 'file-loader', 20 | limit: '8192', 21 | publicPath: '/_next/static/images/', 22 | outputPath: `${config.isServer ? '../' : ''}static/images/`, 23 | name: '[name]-[hash].[ext]' 24 | } 25 | } 26 | ] 27 | }) 28 | 29 | module.exports = (nextConfig = {}) => { 30 | return Object.assign({}, nextConfig, { 31 | webpack (webpackConfig, options) { 32 | webpackConfig.module.rules.push(getImageLoader(options)) 33 | 34 | if (isProduction) { 35 | webpackConfig.plugins.push( 36 | new ImageminPlugin({ 37 | jpegtran: { progressive: true }, 38 | pngquant: { 39 | quality: '95-100' 40 | } 41 | }) 42 | ) 43 | } 44 | 45 | if (typeof nextConfig.webpack === 'function') { 46 | return nextConfig.webpack(webpackConfig, options) 47 | } 48 | 49 | return Object.assign({}, webpackConfig) 50 | } 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /src/components/base/Link/formatUrl.ts: -------------------------------------------------------------------------------- 1 | import parse from 'url-parse' 2 | import { Routers } from 'CONFIG/routers' 3 | 4 | interface FormatUrlOption { 5 | target?: string 6 | staticSuffix: string 7 | host: string 8 | routers: Routers 9 | } 10 | 11 | const formatUrl: ( 12 | href: string | undefined, 13 | option: FormatUrlOption 14 | ) => { 15 | nextHref: string 16 | nextAs: string 17 | aHref: string 18 | next: boolean 19 | } = (href, { target, staticSuffix, host, routers }) => { 20 | const unifyHref = href || '/' 21 | const res = { 22 | nextHref: '', 23 | nextAs: '', 24 | aHref: '', 25 | next: false 26 | } 27 | 28 | // 站外链接 29 | if (!unifyHref.match(/^\/[^/]/) && unifyHref !== '/') { 30 | res.aHref = unifyHref 31 | 32 | return res 33 | } 34 | 35 | const parseUrl = parse(unifyHref) 36 | const { pathname, query } = parseUrl 37 | 38 | if (routers[pathname] != null) { 39 | res.nextHref = routers[pathname].page 40 | } else { 41 | res.nextHref = pathname 42 | } 43 | 44 | if (res.nextHref.match(/\/$/)) { 45 | res.nextAs = `${pathname}${ 46 | staticSuffix === '' ? '' : `index${staticSuffix}` 47 | }${query}` 48 | res.aHref = res.nextAs 49 | } else { 50 | res.nextAs = `${pathname}${staticSuffix}${query}` 51 | res.aHref = res.nextAs 52 | } 53 | 54 | // 代码导出绝对链接 55 | if (target === 'outer') { 56 | res.aHref = `//${host}${res.aHref}` 57 | 58 | return res 59 | } 60 | 61 | res.next = true 62 | return res 63 | } 64 | 65 | export default formatUrl 66 | -------------------------------------------------------------------------------- /src/utils/__tests__/theme.test.ts: -------------------------------------------------------------------------------- 1 | import { checkBy, not, fp } from '../theme' 2 | 3 | interface Props1 { 4 | hidden: boolean 5 | } 6 | 7 | it('should filter parameter', () => { 8 | const res = not('hidden')('color: red;')({ 9 | hidden: true 10 | }) 11 | expect(res).toEqual('') 12 | }) 13 | 14 | interface Props2 { 15 | hidden?: boolean 16 | hover?: boolean 17 | } 18 | 19 | it('should filter parameter', () => { 20 | const res = not('hidden')('color: red;')({}) 21 | expect(res).toEqual('color: red;') 22 | }) 23 | 24 | it('should filter multiple parameters', () => { 25 | const res = not('hover', 'hidden')('color: red;')({ 26 | hidden: true 27 | }) 28 | expect(res).toEqual('') 29 | }) 30 | 31 | interface Props3 { 32 | color: string 33 | hidden?: boolean 34 | } 35 | 36 | it('should handle functions', () => { 37 | const res = not('hidden')( 38 | props => `color: ${props.color};` 39 | )({ 40 | color: 'white' 41 | }) 42 | expect(res).toEqual('color: white;') 43 | }) 44 | 45 | it('should check props by color', () => { 46 | const res = checkBy('color', { 47 | white: '#fff', 48 | black: '#000' 49 | })({ color: 'black' }) 50 | 51 | expect(res).toEqual('#000') 52 | }) 53 | 54 | it('should mapping enable function', () => { 55 | const res = checkBy('color', { 56 | white: props => props.whiteColor, 57 | black: props => props.blackColor 58 | })({ color: 'black', blackColor: '#000' }) 59 | 60 | expect(res).toEqual('#000') 61 | }) 62 | 63 | it('should filter props', () => { 64 | const res = fp({ a: 'a', b: 'b', c: 'c' }, ['a', 'c']) 65 | expect(res).toEqual({ b: 'b' }) 66 | }) 67 | -------------------------------------------------------------------------------- /src/components/base/Link/index.tsx: -------------------------------------------------------------------------------- 1 | import env from 'CONFIG/env' 2 | import { check, fp } from 'UTILS/theme' 3 | import NextLink from 'next/link' 4 | import React from 'react' 5 | import styled from 'styled-components' 6 | import { StandardProps } from '@material-ui/core' 7 | import MuiLink, { 8 | LinkProps as MuiLinkProps, 9 | LinkClassKey as MuiLinkClassKey 10 | } from '@material-ui/core/Link' 11 | 12 | import routers from 'CONFIG/routers' 13 | import formatUrl from './formatUrl' 14 | 15 | export interface LinkProps 16 | extends StandardProps { 17 | withoutLineHeight?: boolean 18 | } 19 | 20 | const MyLink = (props: LinkProps, ref: React.Ref) => ( 21 | 22 | ) 23 | 24 | const StyledLink = styled(React.forwardRef(MyLink))` 25 | ${check('withoutLineHeight')(` 26 | line-height: 1; 27 | `)} 28 | ` 29 | 30 | const { host, target: exportTarget, staticSuffix } = env 31 | 32 | const Link: React.ComponentType = React.forwardRef( 33 | (props, ref: React.Ref) => { 34 | const { href, children, ...other } = props 35 | 36 | const res = formatUrl(href, { 37 | host, 38 | target: exportTarget, 39 | staticSuffix, 40 | routers 41 | }) 42 | 43 | if (!res.next) { 44 | return ( 45 | 46 | {children} 47 | 48 | ) 49 | } 50 | 51 | return ( 52 | 53 | 54 | {children} 55 | 56 | 57 | ) 58 | } 59 | ) 60 | 61 | export default Link 62 | -------------------------------------------------------------------------------- /src/containers/Version.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import Typography from 'COMPONENTS/base/Typography' 4 | import Empty from 'COMPONENTS/base/Empty' 5 | import withRoot from 'CONTAINERS/withRoot' 6 | 7 | const Container = styled.div` 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | justify-content: center; 12 | flex-wrap: wrap; 13 | ` 14 | 15 | const getBuildTime: () => string = () => { 16 | const buildTime = process.env.BUILD_TIME 17 | if (buildTime) { 18 | const time = new Date(buildTime) 19 | return `${time.getFullYear()}-${time.getMonth() + 20 | 1}-${time.getDay()} ${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}` 21 | } 22 | return 'Unknow' 23 | } 24 | 25 | const VersionInfo: React.FC = () => { 26 | const config = [ 27 | { 28 | title: 'NPM VERSION', 29 | value: process.env.npm_package_version 30 | }, 31 | { 32 | title: 'NODE ENV', 33 | value: process.env.NODE_ENV 34 | }, 35 | { 36 | title: 'BUILD ENV', 37 | value: process.env.BUILD_ENV 38 | }, 39 | { 40 | title: 'BUILD TIME', 41 | value: getBuildTime() 42 | }, 43 | { 44 | title: 'GIT COMMIT ID', 45 | value: process.env.npm_package_gitHead || 'unknown' 46 | }, 47 | { 48 | title: 'REACT VERSION', 49 | value: process.env.npm_package_dependencies_react 50 | } 51 | ] 52 | 53 | return ( 54 | 55 | 56 | 57 | 58 | {config.map((item, index) => ( 59 | 60 | 63 | 66 | 67 | ))} 68 | 69 |
61 | {item.title}: 62 | 64 | {item.value} 65 |
70 |
71 | ) 72 | } 73 | 74 | export default withRoot(VersionInfo) 75 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Empty from 'COMPONENTS/base/Empty' 2 | import Icon from 'COMPONENTS/base/Icon' 3 | import Link from 'COMPONENTS/base/Link' 4 | import Image from 'COMPONENTS/base/Image' 5 | import AppActions from 'REDUX/app/actions' 6 | import { set } from 'redux-control' 7 | import { palettePrimaryMain } from 'UTILS/theme' 8 | import React from 'react' 9 | import { connect } from 'react-redux' 10 | import styled from 'styled-components' 11 | 12 | import Button from '@material-ui/core/Button' 13 | 14 | const StyledButton = styled(Button)` 15 | font-size: 50px; 16 | ` 17 | 18 | class App extends React.Component { 19 | state = {} 20 | 21 | componentDidMount () { 22 | AppActions.initApp() 23 | 24 | const fetch = async () => { 25 | set('app.level1.array[1].name', 'english?') 26 | console.log('fetch1') 27 | const data1 = await AppActions.tryToFetchLocationsConfig() 28 | console.log('data1', data1) 29 | 30 | console.log('fetch2') 31 | const data2 = await AppActions.tryToFetchLocationsConfig() 32 | console.log('data2', data2) 33 | } 34 | 35 | fetch() 36 | } 37 | 38 | render () { 39 | return ( 40 | <> 41 | 42 | Hello world 43 | 44 | 45 | 56 | 57 | 58 | LINK 59 | 60 | 61 | 62 | 63 | 67 | 68 | 69 | 70 | ) 71 | } 72 | } 73 | 74 | const mapStateToProps = (state: any) => { 75 | return { level1: state.app.level1 } 76 | } 77 | 78 | export default connect(mapStateToProps)(App) 79 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Document, { 3 | Head, 4 | Main, 5 | NextScript, 6 | DocumentContext, 7 | DocumentInitialProps 8 | } from 'next/document' 9 | import IcoIcon from 'COMPONENTS/base/IcoIcon' 10 | import { palettePrimaryMain } from 'UTILS/theme' 11 | import { ServerStyleSheet as StyledServerStyleSheet } from 'styled-components' 12 | import flush from 'styled-jsx/server' 13 | import { ServerStyleSheets as MuiServerStyleSheet } from '@material-ui/styles' 14 | import SvgSprite from 'COMPONENTS/expand/SvgSprite' 15 | 16 | class MyDocument extends Document { 17 | render () { 18 | return ( 19 | 20 | 21 | 22 | {/* Use minimum-scale=1 to enable GPU rasterization */} 23 | 27 | 28 | {/* Cache */} 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 | 38 | 39 | 40 | ) 41 | } 42 | 43 | static async getInitialProps ( 44 | ctx: DocumentContext 45 | ): Promise { 46 | const styledSheet = new StyledServerStyleSheet() 47 | const sheets = new MuiServerStyleSheet() 48 | const originalRenderPage = ctx.renderPage 49 | 50 | try { 51 | ctx.renderPage = () => 52 | originalRenderPage({ 53 | enhanceApp: App => props => sheets.collect() 54 | }) 55 | 56 | const initialProps = await Document.getInitialProps(ctx) 57 | 58 | return { 59 | ...initialProps, 60 | // Styles fragment is rendered after the app and page rendering finish. 61 | styles: [ 62 | ...React.Children.toArray(initialProps.styles), 63 | sheets.getStyleElement(), 64 | flush() || null, 65 | ...React.Children.toArray(styledSheet.getStyleElement()) 66 | ] 67 | } 68 | /* eslint-enable */ 69 | } catch (err) { 70 | console.error(err) 71 | const initialProps = await Document.getInitialProps(ctx) 72 | return { 73 | ...initialProps 74 | } 75 | } finally { 76 | styledSheet.seal() 77 | } 78 | } 79 | } 80 | 81 | export default MyDocument 82 | -------------------------------------------------------------------------------- /src/api/base/index.js: -------------------------------------------------------------------------------- 1 | import config from 'CONFIG/env' 2 | import configureStore from 'REDUX/store' 3 | import axios from 'axios' 4 | 5 | axios.defaults.withCredentials = true 6 | 7 | const { service } = config 8 | const { main, mock } = service 9 | 10 | const handler = res => { 11 | const { data } = res 12 | return data 13 | } 14 | 15 | const getApiServer = useMock => { 16 | return useMock ? mock : main 17 | } 18 | 19 | const errorHandler = error => { 20 | if (error.response && error.response.data) { 21 | // const { 22 | // message 23 | // error: errorType 24 | // } = error.response.data 25 | // const store = configureStore() 26 | } 27 | return null 28 | } 29 | 30 | const wait = time => () => 31 | new Promise(resolve => { 32 | setTimeout(resolve, time) 33 | }) 34 | 35 | const getOption = async () => { 36 | const store = configureStore() 37 | let isLogin = null 38 | 39 | while (isLogin === null) { 40 | const store = configureStore() 41 | const state = store.getState() 42 | const { user = {} } = state 43 | isLogin = user.isLogin 44 | 45 | await wait(100)() 46 | } 47 | 48 | const state = store.getState() 49 | const { user = {} } = state 50 | const { token } = user 51 | 52 | return { 53 | headers: { 54 | Authorization: `Bearer ${token}` 55 | } 56 | } 57 | } 58 | 59 | export const postData = async (path, data, useMock) => { 60 | const option = await getOption() 61 | return axios 62 | .post(`${getApiServer(useMock)}${path}`, data, option) 63 | .then(handler) 64 | .catch(errorHandler) 65 | } 66 | 67 | export const deleteData = async (path, data, useMock) => { 68 | const option = await getOption() 69 | return axios 70 | .delete( 71 | `${getApiServer(useMock)}${path}`, 72 | Object.assign({ data: data }, option) 73 | ) 74 | .then(handler) 75 | .catch(errorHandler) 76 | } 77 | 78 | export const getData = async (path, params, useMock) => { 79 | const option = await getOption() 80 | return axios 81 | .get( 82 | `${getApiServer(useMock)}${path}`, 83 | Object.assign({ params: params }, option) 84 | ) 85 | .then(handler) 86 | .catch(errorHandler) 87 | } 88 | 89 | export const putData = async (path, data, useMock) => { 90 | const option = await getOption() 91 | return axios 92 | .put(`${getApiServer(useMock)}${path}`, data, option) 93 | .then(handler) 94 | .catch(errorHandler) 95 | } 96 | 97 | export const patchData = async (path, data, useMock) => { 98 | const option = await getOption() 99 | return axios 100 | .patch(`${getApiServer(useMock)}${path}`, data, option) 101 | .then(handler) 102 | .catch(errorHandler) 103 | } 104 | 105 | export const ApiPath = `${main}` 106 | -------------------------------------------------------------------------------- /src/components/base/Link/__test__/formatUrl.test.js: -------------------------------------------------------------------------------- 1 | import formatUrl from '../formatUrl' 2 | 3 | const routers = { 4 | '/index': { page: '/index' }, 5 | '/api': { page: '/apiPage' } 6 | } 7 | 8 | it('', () => { 9 | const res = formatUrl('//www.shianqi.com', { 10 | target: 'inner', 11 | staticSuffix: '.html', 12 | host: 'www.shianqi.com', 13 | routers 14 | }) 15 | 16 | expect(res).toEqual({ 17 | nextHref: '', 18 | nextAs: '', 19 | aHref: '//www.shianqi.com', 20 | next: false 21 | }) 22 | }) 23 | 24 | it('', () => { 25 | const res = formatUrl('/', { 26 | target: 'inner', 27 | staticSuffix: '.html', 28 | host: 'www.shianqi.com', 29 | routers 30 | }) 31 | 32 | expect(res).toEqual({ 33 | nextHref: '/', 34 | nextAs: '/index.html', 35 | aHref: '/index.html', 36 | next: true 37 | }) 38 | }) 39 | 40 | it('', () => { 41 | const res = formatUrl('/', { 42 | target: 'inner', 43 | staticSuffix: '', 44 | host: 'www.shianqi.com', 45 | routers 46 | }) 47 | 48 | expect(res).toEqual({ 49 | nextHref: '/', 50 | nextAs: '/', 51 | aHref: '/', 52 | next: true 53 | }) 54 | }) 55 | 56 | it('', () => { 57 | const res = formatUrl(null, { 58 | target: 'inner', 59 | staticSuffix: '.html', 60 | host: 'www.shianqi.com', 61 | routers 62 | }) 63 | 64 | expect(res).toEqual({ 65 | nextHref: '/', 66 | nextAs: '/index.html', 67 | aHref: '/index.html', 68 | next: true 69 | }) 70 | }) 71 | 72 | it('', () => { 73 | const res = formatUrl('/?id=0', { 74 | target: 'inner', 75 | staticSuffix: '.html', 76 | host: 'www.shianqi.com', 77 | routers 78 | }) 79 | 80 | expect(res).toEqual({ 81 | nextHref: '/', 82 | nextAs: '/index.html?id=0', 83 | aHref: '/index.html?id=0', 84 | next: true 85 | }) 86 | }) 87 | 88 | it('should process outer use code', () => { 89 | const res = formatUrl('/a?id=0', { 90 | target: 'outer', 91 | staticSuffix: '.html', 92 | host: 'www.shianqi.com', 93 | routers 94 | }) 95 | 96 | expect(res).toEqual({ 97 | nextHref: '/a', 98 | nextAs: '/a.html?id=0', 99 | aHref: '//www.shianqi.com/a.html?id=0', 100 | next: false 101 | }) 102 | }) 103 | 104 | it('', () => { 105 | const res = formatUrl('/a?id=0', { 106 | target: 'inner', 107 | staticSuffix: '', 108 | host: 'www.shianqi.com', 109 | routers 110 | }) 111 | 112 | expect(res).toEqual({ 113 | nextHref: '/a', 114 | nextAs: '/a?id=0', 115 | aHref: '/a?id=0', 116 | next: true 117 | }) 118 | }) 119 | 120 | it('', () => { 121 | const res = formatUrl('/api?id=0', { 122 | target: 'inner', 123 | staticSuffix: '', 124 | host: 'www.shianqi.com', 125 | routers 126 | }) 127 | 128 | expect(res).toEqual({ 129 | nextHref: '/apiPage', 130 | nextAs: '/api?id=0', 131 | aHref: '/api?id=0', 132 | next: true 133 | }) 134 | }) 135 | -------------------------------------------------------------------------------- /src/components/base/Image.tsx: -------------------------------------------------------------------------------- 1 | import { breakpointsDown, fp } from 'UTILS/theme' 2 | import React, { useEffect, useState } from 'react' 3 | import styled from 'styled-components' 4 | interface Gradient { 5 | width: string 6 | } 7 | 8 | type SrcKey = 'xs' | 'sm' | 'md' | 'lg' | 'xl' 9 | 10 | interface SrcSet { 11 | xs?: string 12 | sm?: string 13 | md?: string 14 | lg?: string 15 | xl?: string 16 | } 17 | 18 | export interface ImageProps { 19 | src: 20 | | string 21 | | { 22 | src: string 23 | preview: string 24 | } 25 | srcSet?: SrcSet // TODO: 26 | gradient?: Gradient | null // TODO: 27 | pictureProps?: object 28 | color?: string 29 | } 30 | 31 | const Banner = styled(props =>
)` 32 | position: relative; 33 | overflow: hidden; 34 | background: ${props => props.color}; 35 | ` 36 | 37 | const Content = styled.div` 38 | position: relative; 39 | ` 40 | 41 | const VerticalPicture = styled.picture` 42 | position: absolute; 43 | display: block; 44 | height: 100%; 45 | left: 50%; 46 | transform: translateX(-50%); 47 | ` 48 | 49 | const VerticalImage = styled.img` 50 | position: absolute; 51 | top: 0; 52 | bottom: 0; 53 | height: 100%; 54 | left: 50%; 55 | transform: translateX(-50%); 56 | user-select: none; 57 | ` 58 | 59 | const VerticalPreviewImage = styled(VerticalImage)` 60 | transition: all ease 1000ms; 61 | ` 62 | 63 | const pick: (key: string) => (props: any) => string = key => props => props[key] 64 | 65 | // prettier-ignore 66 | const Gradient = styled((props) =>
)` 67 | position: absolute; 68 | width: ${pick('width')}; 69 | height: 100%; 70 | top: 0; 71 | left: 0; 72 | background-image: linear-gradient(to right, ${pick('color')} 0%, rgba(0, 0, 0, 0) 100%); 73 | ` 74 | 75 | // prettier-ignore 76 | const GradientRight = styled(Gradient)` 77 | left: unset; 78 | right: 0; 79 | background-image: linear-gradient(to left, ${pick('color')} 0%, rgba(0, 0, 0, 0) 100%); 80 | ` 81 | 82 | const Image: React.FC = props => { 83 | const { 84 | src, 85 | srcSet = {}, 86 | gradient, 87 | children, 88 | pictureProps, 89 | color, 90 | ...otherProps 91 | } = props 92 | 93 | const objectKeys = Object.keys(srcSet) as SrcKey[] 94 | 95 | const set = objectKeys.map(key => ({ 96 | media: breakpointsDown(key).replace('@media', ''), 97 | srcSet: srcSet[key] 98 | })) 99 | 100 | const isStringSrc = typeof src === 'string' 101 | const [loading, setLoading] = useState(true) 102 | const [imageSrc, setImageSrc] = useState('') 103 | const [time] = useState(new Date().valueOf()) 104 | 105 | const onImageLoad = () => { 106 | setLoading(false) 107 | } 108 | 109 | let imagePreview = '' 110 | 111 | if (typeof src === 'string') { 112 | imagePreview = '' 113 | } else { 114 | imagePreview = src.preview 115 | } 116 | 117 | useEffect(() => { 118 | if (typeof src === 'string') { 119 | setImageSrc(src) 120 | } else { 121 | setImageSrc(src.src) 122 | } 123 | }) 124 | 125 | const spend = new Date().valueOf() - time 126 | 127 | return ( 128 | 129 | 130 | {set.map(item => ( 131 | 132 | ))} 133 | 134 | {!isStringSrc && ( 135 | 300 ? 'all ease-in-out 1s' : 'none' // TODO: props 141 | }} 142 | /> 143 | )} 144 | {gradient && } 145 | {gradient && } 146 | 147 | {children} 148 | 149 | ) 150 | } 151 | 152 | export default Image 153 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-starter", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest src --coverage", 8 | "test-watch": "jest src --watch --coverage", 9 | "build": "npm run clean && next build", 10 | "start-dev": "BUILD_ENV=development next", 11 | "start-server-test": "BUILD_ENV=test npm run build && NODE_ENV=production node ./server.js", 12 | "start-server-production": "BUILD_ENV=production npm run build && NODE_ENV=production node ./server.js", 13 | "clean": "rm -rf out && rm -rf .next", 14 | "eslint": "eslint --ext .js,jsx,ts,tsx .", 15 | "eslint-fix": "eslint --ext .js,jsx,ts,tsx . --fix", 16 | "stylelint": "stylelint './src/**/*.jsx'", 17 | "deploy": "npm run build && next export && node ./script/upload.js", 18 | "deploy-production": "npm install && NODE_ENV=production BUILD_ENV=production npm run deploy", 19 | "deploy-test": "BUILD_ENV=test npm run deploy" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/shianqi/next-starter.git" 24 | }, 25 | "keywords": [], 26 | "author": "", 27 | "license": "ISC", 28 | "bugs": { 29 | "url": "https://github.com/shianqi/next-starter/issues" 30 | }, 31 | "homepage": "https://github.com/shianqi/next-starter#readme", 32 | "importSort": { 33 | ".js, .jsx, .es6, .es": { 34 | "parser": "babylon", 35 | "style": "module-scoped", 36 | "options": {} 37 | }, 38 | ".ts, .tsx": { 39 | "parser": "typescript", 40 | "style": "module-scoped", 41 | "options": {} 42 | } 43 | }, 44 | "jest": { 45 | "moduleNameMapper": { 46 | "^CONFIG/(.*)$": "/config/$1" 47 | } 48 | }, 49 | "dependencies": { 50 | "@babel/polyfill": "^7.8.3", 51 | "@material-ui/core": "^4.9.3", 52 | "@material-ui/styles": "^4.9.0", 53 | "@zeit/next-typescript": "^1.1.1", 54 | "lodash": "^4.17.15", 55 | "next": "^9.2.2", 56 | "normalize.css": "^8.0.1", 57 | "react": "^16.12.0", 58 | "react-dom": "^16.12.0", 59 | "react-redux": "^7.2.0", 60 | "redux": "^4.0.5", 61 | "redux-actions": "^2.6.5", 62 | "redux-batched-actions": "^0.4.1", 63 | "redux-control": "^0.4.0", 64 | "redux-thunk": "^2.3.0", 65 | "styled-components": "^5.0.1", 66 | "url-parse": "^1.4.7" 67 | }, 68 | "devDependencies": { 69 | "@babel/cli": "^7.8.4", 70 | "@babel/core": "^7.8.4", 71 | "@babel/plugin-proposal-class-properties": "^7.8.3", 72 | "@babel/preset-env": "^7.8.4", 73 | "@types/jest": "^25.1.3", 74 | "@types/lodash": "^4.14.149", 75 | "@types/react": "^16.9.22", 76 | "@types/react-redux": "^7.1.7", 77 | "@types/redux-actions": "^2.6.1", 78 | "@types/redux-logger": "^3.0.7", 79 | "@types/styled-components": "^5.0.0", 80 | "@types/styled-jsx": "^2.2.8", 81 | "@types/url-parse": "^1.4.3", 82 | "@typescript-eslint/eslint-plugin": "^2.20.0", 83 | "@typescript-eslint/parser": "^2.20.0", 84 | "babel-eslint": "^10.0.2", 85 | "babel-plugin-styled-components": "^1.10.7", 86 | "eslint": "^6.8.0", 87 | "eslint-config-prettier": "^6.10.0", 88 | "eslint-config-standard": "^14.1.0", 89 | "eslint-plugin-import": "^2.20.1", 90 | "eslint-plugin-node": "^11.0.0", 91 | "eslint-plugin-prettier": "^3.1.2", 92 | "eslint-plugin-promise": "^4.2.1", 93 | "eslint-plugin-react": "^7.18.3", 94 | "eslint-plugin-standard": "^4.0.1", 95 | "file-loader": "^5.1.0", 96 | "imagemin-webpack-plugin": "^2.4.2", 97 | "jest": "^25.1.0", 98 | "next-compose-plugins": "^2.2.0", 99 | "oss-cdn-helper": "^0.1.0", 100 | "prettier": "^1.19.1", 101 | "prettier-eslint": "^9.0.1", 102 | "raw-loader": "^4.0.0", 103 | "redux-logger": "^3.0.6", 104 | "sqip-loader": "^1.0.0", 105 | "stylelint": "^13.2.0", 106 | "stylelint-config-standard": "^20.0.0", 107 | "stylelint-config-styled-components": "^0.1.1", 108 | "stylelint-processor-styled-components": "^1.10.0", 109 | "svg-sprite-loader": "4.1.3", 110 | "svgo": "^1.3.2", 111 | "svgo-loader": "^2.2.1", 112 | "typescript": "^3.8.2", 113 | "url-loader": "^3.0.0" 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/utils/theme.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { css } from 'styled-components' 3 | 4 | import { createMuiTheme } from '@material-ui/core/styles' 5 | import { ThemeOptions } from '@material-ui/core/styles/createMuiTheme' 6 | 7 | const myTypographyFontFamily = { 8 | 100: '"PingFangSC-Ultralight", "Helvetica Neue", Helvetica, Arial, "Hiragino Sans GB", "Heiti SC", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;', 9 | 200: '"PingFangSC-Thin", "Helvetica Neue", Helvetica, Arial, "Hiragino Sans GB", "Heiti SC", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;', 10 | 300: '"PingFangSC-Light", "Helvetica Neue", Helvetica, Arial, "Hiragino Sans GB", "Heiti SC", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;', 11 | 400: '"PingFangSC", "Helvetica Neue", Helvetica, Arial, "Hiragino Sans GB", "Heiti SC", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;', 12 | 500: '"PingFangSC-Medium", "Helvetica Neue", Helvetica, Arial, "Hiragino Sans GB", "Heiti SC", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;', 13 | 900: '"PingFangSC-Semibold", "Helvetica Neue", Helvetica, Arial, "Hiragino Sans GB", "Heiti SC", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;' 14 | } 15 | 16 | const defaultTheme: ThemeOptions = { 17 | palette: { 18 | type: 'light', 19 | primary: { 20 | main: '#58C1B6', 21 | light: '#79D8CE', 22 | contrastText: '#fff' 23 | }, 24 | secondary: { 25 | main: '#5a8dbe', 26 | light: '#5cb1b7', 27 | contrastText: '#fff' 28 | }, 29 | text: { 30 | primary: '#272E3E', 31 | secondary: '#545A69', 32 | disabled: '#797F8F' 33 | }, 34 | error: { 35 | main: '#D94F43' 36 | }, 37 | tonalOffset: 0.05, 38 | action: { 39 | active: 'rgba(0, 0, 0, 0.54)', 40 | hover: 'rgba(0, 0, 0, 0.1)', 41 | hoverOpacity: 0.05, 42 | selected: 'rgba(0, 0, 0, 0.14)', 43 | disabled: 'rgba(0, 0, 0, 0.26)', 44 | disabledBackground: 'rgba(0, 0, 0, 0.12)' 45 | } 46 | }, 47 | mixins: { 48 | toolbar: { 49 | minHeight: 56, 50 | '@media (min-width:0px) and (orientation: landscape)': { 51 | minHeight: 56 52 | }, 53 | '@media (min-width:600px)': { 54 | minHeight: 56 55 | } 56 | } 57 | }, 58 | typography: { 59 | fontFamily: 60 | '"PingFangSC", "Helvetica Neue", Helvetica, Arial, "Hiragino Sans GB", "Heiti SC", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;', 61 | h1: { 62 | fontSize: '2.375rem', // 38px 63 | lineHeight: 1.3 // 37.33333px 64 | }, 65 | h2: { 66 | fontSize: '2.25rem', // 36px 67 | lineHeight: 1.3 // 34.66667px 68 | }, 69 | h3: { 70 | fontSize: '1.5rem', // 24px 71 | lineHeight: 1 // 32px 72 | }, 73 | h4: { 74 | fontSize: '1.375rem', // 22px 75 | lineHeight: 18 / 11 // 29.3333333px 76 | }, 77 | h5: { 78 | fontSize: '1.25rem', // 20px 79 | lineHeight: 1 // 26.666667px 80 | }, 81 | h6: { 82 | fontSize: '1.125rem', // 18px 83 | lineHeight: 1.3 // 24px 84 | }, 85 | subtitle1: { 86 | fontSize: '1rem', 87 | lineHeight: 12 / 7 88 | }, 89 | body2: { 90 | fontSize: '0.875rem', // 14px 91 | fontWeight: 400, 92 | lineHeight: 12 / 7 // 20px 93 | }, 94 | button: { 95 | fontSize: '0.875rem', 96 | fontWeight: 500, 97 | fontFamily: myTypographyFontFamily[500], 98 | lineHeight: '1' 99 | } 100 | }, 101 | breakpoints: { 102 | values: { 103 | xs: 0, 104 | sm: 600, 105 | md: 960, 106 | lg: 1200, 107 | xl: 1920 108 | } 109 | } 110 | } 111 | 112 | export const getTheme = (newTheme?: ThemeOptions) => 113 | createMuiTheme(_.merge(defaultTheme, newTheme)) 114 | 115 | // https://material-ui.com/customization/default-theme/ 116 | const theme = getTheme() 117 | 118 | export const scrollbar = css` 119 | -webkit-overflow-scrolling: touch; 120 | 121 | ::-webkit-scrollbar { 122 | width: 4px; 123 | height: 4px; 124 | } 125 | 126 | ::-webkit-scrollbar-track { 127 | border-radius: 2px; 128 | transition: all 0.3s; 129 | background: rgba(0, 0, 0, 0); 130 | box-shadow: inset 0 0 5px rgba(0, 0, 0, 0); 131 | } 132 | 133 | ::-webkit-scrollbar-thumb { 134 | border-radius: 2px; 135 | transition: all 0.3s; 136 | background: rgba(0, 0, 0, 0); 137 | box-shadow: inset 0 0 10px rgba(0, 0, 0, 0); 138 | } 139 | 140 | &:hover { 141 | ::-webkit-scrollbar-thumb { 142 | background: rgba(0, 0, 0, 0.12); 143 | box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.12); 144 | } 145 | } 146 | ` 147 | 148 | export const getMyTypographyFontFamily = (fontWeight: string) => { 149 | const keys = Object.keys(myTypographyFontFamily) 150 | for (const key of keys) { 151 | const parsedKey = parseInt(key, 10) 152 | const parsedFontWeight = parseInt(fontWeight, 10) 153 | if (parsedFontWeight <= parsedKey) { 154 | // @ts-ignore 155 | return myTypographyFontFamily[key] 156 | } 157 | } 158 | return myTypographyFontFamily['400'] 159 | } 160 | 161 | export const palettePrimaryMain = () => theme.palette.primary.main 162 | export const palettePrimaryLight = () => theme.palette.primary.light 163 | export const palettePrimaryDark = () => theme.palette.primary.dark 164 | 165 | export const paletteSecondaryMain = () => theme.palette.secondary.main 166 | export const paletteSecondaryLight = () => theme.palette.secondary.light 167 | 168 | export const paletteBackgroundDefault = () => theme.palette.background.default 169 | export const paletteTextPrimary = () => theme.palette.text.primary 170 | export const paletteTextSecondary = () => theme.palette.text.secondary 171 | export const paletteTextDisabled = () => theme.palette.text.disabled 172 | export const paletteErrorMain = () => theme.palette.error.main 173 | export const paletteErrorLight = () => theme.palette.error.light 174 | 175 | export const paletteActionBackgroundColor = () => 176 | theme.palette.action.disabledBackground 177 | export const paletteDividerColor = () => theme.palette.divider 178 | 179 | export const breakpointsDown = theme.breakpoints.down 180 | export const breakpointsValues = theme.breakpoints.values 181 | 182 | export const transitionsCreate = () => theme.transitions.create 183 | 184 | export const spacingUnit = theme.spacing 185 | export const typographyFontSize = () => theme.typography.fontSize 186 | export const typographyH6FontSize = () => theme.typography.h6.fontSize 187 | export const typographyH6LineHeight = () => theme.typography.h6.lineHeight 188 | export const typographyButtonFontSize = () => theme.typography.button.fontSize 189 | export const typographyBody2LineHeight = () => theme.typography.body2.lineHeight 190 | export const typographyButtonLineHeight = () => 191 | theme.typography.button.lineHeight 192 | export const shapeBorderRadius = () => theme.shape.borderRadius 193 | 194 | export const not = (...keys: K[]) => ( 195 | value: ((props: P) => string) | string 196 | ) => (props: P) => { 197 | for (const key of keys) { 198 | if (props[key]) { 199 | return '' 200 | } 201 | } 202 | return typeof value === 'function' ? value(props) : value 203 | } 204 | 205 | export const check = (...keys: K[]) => ( 206 | value: ((props: P) => string) | string 207 | ) => (props: P) => { 208 | for (const key of keys) { 209 | if (props[key]) { 210 | return typeof value === 'function' ? value(props) : value 211 | } 212 | } 213 | return '' 214 | } 215 | 216 | // TODO: Fix ts-ignore 217 | export const checkBy = (property: string, mapping: {}) => (props: {}) => { 218 | // @ts-ignore 219 | const value = mapping[props[property]] 220 | return typeof value === 'function' ? value(props) : value 221 | } 222 | 223 | export const fp = (props: O, keys: K[]) => { 224 | const { ...newProps } = props 225 | 226 | for (const key of keys) { 227 | delete newProps[key] 228 | } 229 | 230 | return newProps as { 231 | [key in Exclude]: O[key] 232 | } 233 | } 234 | 235 | export const filterProps = fp 236 | 237 | export default theme 238 | --------------------------------------------------------------------------------