├── .babelrc ├── .env.example ├── .envrc ├── .eslintrc ├── .github └── workflows │ ├── azure-static-web-apps-green-dune-04ae8691e.yml │ └── lighthouse-ci.yml ├── .gitignore ├── .node-version ├── .prettierrc ├── .storybook ├── addons.js ├── babel.config.js ├── config.js ├── i18n.js ├── jest_setup.js ├── story.css ├── transoformer.js └── webpack.config.js ├── .vscode └── settings.json ├── README.md ├── docker-compose.yml ├── jest.config.js ├── lighthouserc.yml ├── next-env.d.ts ├── next.config.js ├── nginx.conf.d └── default.conf ├── package.json ├── pages ├── _app.tsx ├── _document.tsx ├── current │ └── [code].tsx ├── forecast │ └── [code].tsx └── index.tsx ├── public ├── css │ └── styles.css ├── favicon.ico ├── images │ └── .gitkeep ├── static │ └── locales │ │ ├── en_us │ │ └── common.json │ │ └── ja_jp │ │ └── common.json └── staticwebapp.config.json ├── src ├── __fixtures__ │ ├── .gitkeep │ ├── current_weather.ts │ └── forecast.ts ├── __mocks__ │ └── nextConfig.ts ├── __stories__ │ ├── button.stories.tsx │ ├── notisnack.stories.tsx │ └── toolbar.stories.tsx ├── components │ ├── ForecastCard │ │ ├── index.stories.tsx │ │ ├── index.tsx │ │ └── styles.tsx │ ├── GlobalStyle │ │ └── index.tsx │ ├── Header │ │ ├── components │ │ │ ├── MenuItem │ │ │ │ └── index.tsx │ │ │ ├── index.stories.tsx │ │ │ ├── index.tsx │ │ │ └── styles.tsx │ │ ├── constants.ts │ │ ├── index.tsx │ │ └── types.ts │ ├── LoadingCover │ │ ├── index.stories.tsx │ │ ├── index.tsx │ │ └── styles.tsx │ ├── WeatherCard │ │ ├── index.stories.tsx │ │ ├── index.tsx │ │ └── styles.tsx │ └── WindIcon │ │ ├── index.stories.tsx │ │ ├── index.tsx │ │ └── styles.tsx ├── containers │ └── views │ │ ├── CurrentWeather │ │ ├── components │ │ │ ├── index.stories.tsx │ │ │ ├── index.tsx │ │ │ └── styles.tsx │ │ ├── hooks.ts │ │ └── index.tsx │ │ ├── Forecast │ │ ├── components │ │ │ ├── index.stories.tsx │ │ │ ├── index.tsx │ │ │ └── styles.tsx │ │ ├── hooks.ts │ │ └── index.tsx │ │ └── Home │ │ ├── components │ │ ├── index.stories.tsx │ │ ├── index.tsx │ │ └── styles.tsx │ │ └── index.tsx ├── globalStyles.ts ├── i18n.ts ├── layouts │ ├── default.tsx │ └── index.tsx ├── requests │ ├── WeatherAPI.ts │ ├── client.ts │ └── core │ │ ├── Client.ts │ │ ├── Endpoint.ts │ │ └── Result.ts ├── theme.ts ├── types.ts └── utils │ ├── config.tsx │ ├── device.ts │ ├── weather.test.ts │ └── weather.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "next/babel" 4 | ], 5 | "plugins": [ 6 | ["@babel/plugin-proposal-decorators", {"legacy": true}], 7 | ["@babel/plugin-proposal-class-properties", {"loose": true}], 8 | ["babel-plugin-styled-components", { "ssr": true, "displayName": true, "preprocess": false }], 9 | [ 10 | "module-resolver", 11 | { 12 | "alias": { 13 | "~": "./src/" 14 | } 15 | } 16 | ] 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | TZ=Asia/Tokyo 2 | WEATHER_API_ENDPOINT=http://api.weatherapi.com/v1 3 | WEATHER_API_KEY=Your Api key -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | dotenv -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "react": { 4 | "pragma": "React", 5 | "version": "detect" 6 | } 7 | }, 8 | "parser": "@typescript-eslint/parser", 9 | "parserOptions": { 10 | "ecmaVersion": 2018, 11 | "sourceType": "module", 12 | "project": "./tsconfig.json", 13 | "ecmaFeatures": { 14 | "jsx": true 15 | } 16 | }, 17 | "extends": [ 18 | "eslint:recommended", 19 | "plugin:react/recommended", 20 | "prettier" 21 | ], 22 | "plugins": [ 23 | "@typescript-eslint", 24 | "react" 25 | ], 26 | "rules": { 27 | "react/prop-types": "off", 28 | "no-undef": "off", 29 | "no-unused-vars": "off", 30 | "require-atomic-updates": "off", 31 | "@typescript-eslint/no-unused-vars": [ 32 | "error", 33 | { 34 | "argsIgnorePattern": "^_", 35 | "ignoreRestSiblings": true 36 | } 37 | ], 38 | "semi": [ 39 | "error", 40 | "never" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/azure-static-web-apps-green-dune-04ae8691e.yml: -------------------------------------------------------------------------------- 1 | name: Azure Static Web Apps CI/CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: [opened, synchronize, reopened, closed] 9 | branches: 10 | - master 11 | 12 | jobs: 13 | build_and_deploy_job: 14 | if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') 15 | runs-on: ubuntu-latest 16 | name: Build and Deploy Job 17 | steps: 18 | - uses: actions/checkout@v2 19 | with: 20 | submodules: true 21 | - name: Setup nodejs 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: '12.x' 25 | - run: yarn install --dev 26 | - run: yarn test:ci 27 | - name: Build And Deploy 28 | id: builddeploy 29 | uses: Azure/static-web-apps-deploy@v1 30 | env: 31 | WEATHER_API_ENDPOINT: https://api.weatherapi.com/v1 32 | WEATHER_API_KEY: ${{ secrets.WEATHER_API_KEY }} 33 | with: 34 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_GREEN_DUNE_04AE8691E }} 35 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) 36 | action: "upload" 37 | ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### 38 | # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig 39 | app_location: "/" # App source code path 40 | api_location: "" # Api source code path - optional 41 | output_location: "out" # Built app content directory - optional 42 | ###### End of Repository/Build Configurations ###### 43 | 44 | close_pull_request_job: 45 | if: github.event_name == 'pull_request' && github.event.action == 'closed' 46 | runs-on: ubuntu-latest 47 | name: Close Pull Request Job 48 | steps: 49 | - name: Close Pull Request 50 | id: closepullrequest 51 | uses: Azure/static-web-apps-deploy@v1 52 | with: 53 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_GREEN_DUNE_04AE8691E }} 54 | action: "close" 55 | -------------------------------------------------------------------------------- /.github/workflows/lighthouse-ci.yml: -------------------------------------------------------------------------------- 1 | name: lighthouse-ci 2 | 3 | on: [push] 4 | 5 | jobs: 6 | lighthouseci: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | with: 11 | ref: ${{ github.event.pull_request.head.sha }} 12 | - name: Setup nodejs 13 | uses: actions/setup-node@v2 14 | with: 15 | node-version: '12.x' 16 | - run: yarn install --dev 17 | - name: Create .env 18 | run: | 19 | echo "" > .env 20 | echo "TZ="| tee -a .env 21 | echo "WEATHER_API_ENDPOINT=http://api.weatherapi.com/v1" | tee -a .env 22 | echo "WEATHER_API_KEY=${{ secrets.WEATHER_API_KEY }}"| tee -a .env 23 | - name: Build 24 | run: yarn build 25 | - name: Build 26 | run: docker-compose up -d 27 | - name: run Lighthouse CI 28 | run: | 29 | npx lhci autorun || echo "LHCI failed!" 30 | env: 31 | LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # local env files 27 | .env 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | junit.xml -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 12.20.2 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false, 3 | "trailingComma": "es5", 4 | "semi": false 5 | } 6 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-viewport/register' 2 | import 'storybook-addon-i18n/register.js' 3 | -------------------------------------------------------------------------------- /.storybook/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { 4 | useBuiltIns: 'usage', 5 | corejs: 3, 6 | }], 7 | ['@babel/preset-typescript', { 8 | isTSX: true, 9 | allExtensions: true 10 | }], 11 | '@babel/preset-react' 12 | ], 13 | plugins: [ 14 | ["@babel/plugin-proposal-class-properties", {"loose": true}], 15 | ["babel-plugin-styled-components", { "ssr": true, "displayName": true, "preprocess": false }], 16 | [ 17 | "module-resolver", 18 | { 19 | "alias": { 20 | "~": "./src/" 21 | } 22 | } 23 | ] 24 | ], 25 | babelrc: false, 26 | } 27 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { configure, addDecorator, addParameters } from '@storybook/react' 3 | import { withInfo } from "@storybook/addon-info" 4 | import './story.css' 5 | import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport' 6 | import { I18nextProvider } from 'react-i18next' 7 | import { withI18n } from 'storybook-addon-i18n' 8 | import i18n from './i18n' 9 | import {muiTheme} from 'storybook-addon-material-ui' 10 | // @ts-ignore 11 | import theme from '~/theme' 12 | 13 | const req = require.context('../src/', true, /.*\.stories\.tsx?$/); 14 | function loadStories() { 15 | req.keys().forEach(filename => req(filename)); 16 | } 17 | 18 | if (process.env.NODE_ENV !== 'test') { 19 | addDecorator(withInfo) 20 | addParameters({ info: { inline: true } }) 21 | } 22 | 23 | addDecorator(muiTheme([theme])) 24 | 25 | const I18nProviderWrapper = ({ children, i18n, locale }) => { 26 | React.useEffect(() => { 27 | i18n.changeLanguage(locale); 28 | }, [i18n, locale]); 29 | return {children}; 30 | } 31 | addDecorator(withI18n) 32 | addParameters({ 33 | i18n: { 34 | provider: I18nProviderWrapper, 35 | providerProps: { 36 | i18n 37 | }, 38 | supportedLocales: ["en", "ja"], 39 | providerLocaleKey: "locale", 40 | } 41 | }) 42 | 43 | addParameters({ 44 | viewport: { 45 | viewports: INITIAL_VIEWPORTS, 46 | }, 47 | }); 48 | 49 | configure(loadStories, module); 50 | -------------------------------------------------------------------------------- /.storybook/i18n.js: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next' 2 | import LanguageDetector from 'i18next-browser-languagedetector' 3 | import { initReactI18next } from 'react-i18next' 4 | import enCommonJson from '../public/static/locales/en_us/common.json' 5 | import jpCommonJson from '../public/static/locales/ja_jp/common.json' 6 | 7 | i18n 8 | .use(initReactI18next) 9 | .use(LanguageDetector) 10 | .init({ 11 | resources: { 12 | en: { 13 | common: enCommonJson, 14 | }, 15 | ja: { 16 | common: jpCommonJson, 17 | }, 18 | }, 19 | lng: 'en', 20 | fallbackLng: 'en', 21 | keySeparator: '^', 22 | nsSeparator: '|', 23 | // react-i18next options 24 | react: { 25 | wait: true, 26 | }, 27 | }); 28 | 29 | export default i18n 30 | -------------------------------------------------------------------------------- /.storybook/jest_setup.js: -------------------------------------------------------------------------------- 1 | import registerRequireContextHook from 'babel-plugin-require-context-hook/register'; 2 | registerRequireContextHook(); 3 | -------------------------------------------------------------------------------- /.storybook/story.css: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/earlyaccess/notosansjp.css); 2 | 3 | body { 4 | font-family: 'Noto Sans JP', sans-serif; 5 | } 6 | 7 | #story-root { 8 | padding: 0 40px 40px; 9 | } 10 | -------------------------------------------------------------------------------- /.storybook/transoformer.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | const babelJest = require('babel-jest'); 3 | const babelConfig = require('./babel.config.js') 4 | babelConfig.plugins.push('require-context-hook') 5 | module.exports = babelJest.createTransformer(babelConfig); 6 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const babelConfig = require('./babel.config.js') 3 | 4 | module.exports = ({ config }) => { 5 | config.module.rules.push({ 6 | test: /\.tsx?$/, 7 | use: [ 8 | { 9 | loader: 'babel-loader', 10 | options: babelConfig, 11 | }, 12 | { 13 | loader: 'react-docgen-typescript-loader', 14 | options: { 15 | tsconfigPath: path.join(__dirname, "../tsconfig.json"), 16 | }, 17 | }, 18 | ] 19 | }); 20 | config.resolve.extensions.push('.ts', '.tsx', '.js', '.jsx'); 21 | config.resolve.alias['next/config'] = path.resolve(__dirname, '../src/__mocks__/nextConfig') 22 | 23 | return config; 24 | }; 25 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.trimTrailingWhitespace": true, 3 | "tslint.packageManager": "yarn", 4 | "tslint.autoFixOnSave": true, 5 | "tslint.exclude": "**/node_modules/**/*" 6 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js app boilerplate 2 | 3 | ## technical elements 4 | 5 | - [react](https://ja.reactjs.org/) 6 | - [mobx](https://mobx.js.org/README.html) 7 | - [nextjs](https://nextjs.org/) 8 | - [material-ui](https://material-ui.com/) 9 | - [storybook](https://storybook.js.org/) 10 | - [i18n - storybook-addon-i18n](https://github.com/goooseman/storybook-addon-i18n) 11 | - [mobile - @storybook/addon-viewport](https://github.com/storybookjs/storybook/tree/master/addons/viewport) 12 | - [material-ui (storybook-addon-material-ui](https://github.com/react-theming/storybook-addon-material-ui) 13 | - [jest](https://jestjs.io/) 14 | 15 | ## test on dev 16 | 17 | ``` 18 | $ yarn build 19 | $ docker-compose up -d 20 | $ curl http://localhost 21 | ``` 22 | 23 | ## TODO 24 | 25 | - i18n for Static HTML Export - [next-translate](https://github.com/vinissimus/next-translate)? 26 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | nextjs-template: 5 | container_name: nextjs-template 6 | image: nginx:latest 7 | ports: 8 | - 80:80 9 | volumes: 10 | - ./out:/usr/share/nginx/html:cached 11 | - ./nginx.conf.d:/etc/nginx/conf.d 12 | restart: always 13 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '^.+\\.(js|jsx|ts|tsx)$': './.storybook/transoformer.js', 4 | }, 5 | collectCoverageFrom: [ 6 | "./src/**/*.{ts,tsx}", 7 | "!**/__stories__/**", 8 | "!**/__fixtures__/**", 9 | "!**/*.stories.tsx", 10 | "!**/node_modules/**", 11 | "!**/build/**", 12 | "!**/out/**" 13 | ], 14 | moduleNameMapper: { 15 | "\\.(css|less|scss|sass)$": "identity-obj-proxy", 16 | '^~(.*)$': '/src$1', 17 | }, 18 | setupFiles: [ 19 | '/.storybook/jest_setup.js' 20 | ], 21 | transformIgnorePatterns: [ 22 | '/(node_modules)/(?!react-syntax-highlighter)', 23 | ] 24 | }; 25 | -------------------------------------------------------------------------------- /lighthouserc.yml: -------------------------------------------------------------------------------- 1 | ci: 2 | collect: 3 | # collect options here 4 | numberOfRuns: 1 # Lighthouse の試行回数は1回 5 | puppeteerLaunchOptions: 6 | defaultViewport: null 7 | startServerCommand: 'npm run start' # プロダクションモードでローカルサーバーを起動する 8 | url: 9 | - 'http://localhost/' 10 | - 'http://localhost/current/tokyo' 11 | - 'http://localhost/forcast/tokyo' 12 | settings: 13 | disableStorageReset: true 14 | 15 | assert: 16 | # assert options here 17 | 18 | upload: 19 | # upload options here 20 | target: 'temporary-public-storage' 21 | server: 22 | # server options here 23 | 24 | wizard: 25 | # wizard options here 26 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | const withImages = require('next-images') 3 | 4 | module.exports = withImages({ 5 | publicRuntimeConfig: { 6 | WEATHER_API_ENDPOINT: process.env.WEATHER_API_ENDPOINT, 7 | WEATHER_API_KEY: process.env.WEATHER_API_KEY, 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /nginx.conf.d/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | 5 | #charset koi8-r; 6 | #access_log /var/log/nginx/host.access.log main; 7 | 8 | location / { 9 | root /usr/share/nginx/html; 10 | index index.html index.htm; 11 | try_files $uri $uri.html /index.html; 12 | } 13 | 14 | #error_page 404 /404.html; 15 | 16 | # redirect server error pages to the static page /50x.html 17 | # 18 | error_page 500 502 503 504 /50x.html; 19 | location = /50x.html { 20 | root /usr/share/nginx/html; 21 | } 22 | 23 | location /current/tokyo { 24 | root /usr/share/nginx/html; 25 | rewrite ^ /current/tokyo.html break; 26 | } 27 | 28 | location /current/osaka { 29 | root /usr/share/nginx/html; 30 | rewrite ^ /current/osaka.html break; 31 | } 32 | location /forecast/tokyo { 33 | root /usr/share/nginx/html; 34 | rewrite ^ /forecast/[code].html break; 35 | } 36 | 37 | location /forecast/osaka { 38 | root /usr/share/nginx/html; 39 | rewrite ^ /forecast/[code].html break; 40 | } 41 | 42 | # location /forecast { 43 | # root /usr/share/nginx/html; 44 | # rewrite ^/forecast/(.*)$ /forecast/[code].html break; 45 | # } 46 | 47 | # proxy the PHP scripts to Apache listening on 127.0.0.1:80 48 | # 49 | #location ~ \.php$ { 50 | # proxy_pass http://127.0.0.1; 51 | #} 52 | 53 | # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 54 | # 55 | #location ~ \.php$ { 56 | # root html; 57 | # fastcgi_pass 127.0.0.1:9000; 58 | # fastcgi_index index.php; 59 | # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; 60 | # include fastcgi_params; 61 | #} 62 | 63 | # deny access to .htaccess files, if Apache's document root 64 | # concurs with nginx's one 65 | # 66 | #location ~ /\.ht { 67 | # deny all; 68 | #} 69 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learn-starter", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "rm -rf ./.next ./out && next build && next export", 8 | "start": "next start", 9 | "test": "NODE_ENV=test jest --no-cache --env=jsdom", 10 | "test:ci": "NODE_ENV=test jest --ci --reporters=default --reporters=jest-junit --no-cache", 11 | "storybook": "start-storybook -p 6006 -s ./public", 12 | "build-storybook": "build-storybook", 13 | "lint:ts": "eslint --ext .ts,.tsx ." 14 | }, 15 | "dependencies": { 16 | "@material-ui/core": "^4.10.1", 17 | "@material-ui/icons": "^4.9.1", 18 | "camelcase-keys": "^6.2.2", 19 | "date-fns": "^2.14.0", 20 | "isomorphic-unfetch": "^3.0.0", 21 | "next": "9.3.5", 22 | "next-i18next": "^4.4.2", 23 | "notistack": "^0.9.17", 24 | "query-string": "^6.13.1", 25 | "react": "16.13.1", 26 | "react-dom": "16.13.1", 27 | "react-use": "^17.2.4", 28 | "snakecase-keys": "^3.2.0", 29 | "styled-components": "^5.1.1", 30 | "tslint": "^6.1.2", 31 | "webfontloader": "^1.6.28" 32 | }, 33 | "devDependencies": { 34 | "@babel/core": "^7.10.2", 35 | "@babel/plugin-proposal-decorators": "^7.10.1", 36 | "@babel/plugin-transform-runtime": "^7.10.1", 37 | "@babel/preset-env": "^7.10.2", 38 | "@babel/preset-react": "^7.10.1", 39 | "@babel/preset-typescript": "^7.10.1", 40 | "@storybook/addon-actions": "^5.3.19", 41 | "@storybook/addon-info": "^5.3.19", 42 | "@storybook/addon-links": "^5.3.19", 43 | "@storybook/addon-viewport": "^5.3.19", 44 | "@storybook/addons": "^5.3.19", 45 | "@storybook/react": "^5.3.19", 46 | "@types/jest": "^26.0.6", 47 | "@types/node": "^14.0.11", 48 | "@types/react": "^16.9.35", 49 | "@types/styled-components": "^5.1.0", 50 | "@typescript-eslint/eslint-plugin": "^3.1.0", 51 | "@typescript-eslint/parser": "^3.1.0", 52 | "babel-jest": "^26.1.0", 53 | "babel-loader": "^8.1.0", 54 | "babel-plugin-module-resolver": "^4.0.0", 55 | "babel-plugin-require-context-hook": "^1.0.0", 56 | "babel-plugin-styled-components": "^1.10.7", 57 | "eslint": "^7.2.0", 58 | "eslint-config-prettier": "^6.11.0", 59 | "eslint-plugin-react": "^7.20.0", 60 | "jest": "^26.1.0", 61 | "jest-junit": "^11.0.1", 62 | "jest-snapshot": "^26.1.0", 63 | "jest-styled-components": "^7.0.2", 64 | "next-images": "^1.4.0", 65 | "prettier": "^2.0.5", 66 | "react-docgen-typescript-loader": "^3.7.2", 67 | "storybook-addon-i18n": "^5.1.13", 68 | "storybook-addon-material-ui": "^0.9.0-alpha.21", 69 | "tslint-config-prettier": "^1.18.0", 70 | "tslint-react": "^5.0.0", 71 | "tslint-react-hooks": "^2.2.2", 72 | "typescript": "^4.2.3" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { default as NextApp } from 'next/app' 3 | import { ThemeProvider } from '@material-ui/styles' 4 | import theme from '~/theme' 5 | import GlobalStyle from '~/components/GlobalStyle' 6 | import Layout from '~/layouts' 7 | import { SnackbarProvider } from 'notistack' 8 | 9 | export default class App extends NextApp { 10 | componentDidMount () { 11 | const jssStyles = document.querySelector('#jss-server-side') 12 | if (jssStyles && jssStyles.parentNode) { 13 | jssStyles.parentNode.removeChild(jssStyles) 14 | } 15 | } 16 | 17 | render () { 18 | const { Component, pageProps, router } = this.props 19 | 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Document, { Html, Head, Main, NextScript, DocumentContext } from 'next/document' 3 | import { ServerStyleSheet as StyledComponentSheet } from 'styled-components' 4 | import { ServerStyleSheets as MaterialUiStyleSheets } from '@material-ui/styles' 5 | 6 | class MyDocument extends Document { 7 | static async getInitialProps (ctx: DocumentContext) { 8 | const styledComponentSheet = new StyledComponentSheet() 9 | const materialUiStyleSheets = new MaterialUiStyleSheets() 10 | const originalRenderPage = ctx.renderPage 11 | 12 | try { 13 | ctx.renderPage = () => 14 | originalRenderPage({ 15 | enhanceApp: (App: React.ComponentType) => { 16 | return (props: any) => { 17 | return styledComponentSheet.collectStyles( 18 | materialUiStyleSheets.collect(), 19 | ) 20 | } 21 | }, 22 | }) 23 | 24 | const initialProps = await Document.getInitialProps(ctx) 25 | 26 | return { 27 | ...initialProps, 28 | styles: [ 29 | initialProps.styles, 30 | styledComponentSheet.getStyleElement(), 31 | materialUiStyleSheets.getStyleElement(), 32 | ], 33 | } as any 34 | } catch (err) { 35 | console.error(err.stack) 36 | } finally { 37 | styledComponentSheet.seal() 38 | } 39 | } 40 | 41 | render() { 42 | return ( 43 | 44 | 45 | 46 | 47 | 48 | 49 | {(this.props as any).styleTags} 50 | 51 | 52 |
53 |
54 |
55 | 56 | 57 | 58 | ) 59 | } 60 | } 61 | 62 | MyDocument.getInitialProps = async (ctx) => { 63 | // const sheets = new ServerStyleSheets() 64 | // const originalRenderPage = ctx.renderPage 65 | 66 | // ctx.renderPage = () => 67 | // originalRenderPage({ 68 | // enhanceApp: (App) => (props) => sheets.collect(), 69 | // }) 70 | 71 | const initialProps = await Document.getInitialProps(ctx) 72 | 73 | return { 74 | ...initialProps, 75 | styles: [...React.Children.toArray(initialProps.styles)], 76 | } 77 | } 78 | 79 | export default MyDocument 80 | -------------------------------------------------------------------------------- /pages/current/[code].tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { NextPage } from 'next' 3 | import CurrentContainerView from '~/containers/views/CurrentWeather' 4 | 5 | type Props = React.ComponentProps 6 | 7 | const CurrentWeather: NextPage = (props) => { 8 | return ( 9 | 10 | ) 11 | } 12 | 13 | export const getStaticPaths = async () => { 14 | return { 15 | paths: [ 16 | {params: {code: 'tokyo'}}, 17 | {params: {code: 'osaka'}}, 18 | ], 19 | fallback: false, 20 | } 21 | } 22 | 23 | export const getStaticProps = async ({params} : any) => { 24 | return { 25 | props: { 26 | code: String(params.code), 27 | }, 28 | } 29 | } 30 | 31 | export default CurrentWeather 32 | -------------------------------------------------------------------------------- /pages/forecast/[code].tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { NextPage } from 'next' 3 | import ForecastView from '~/containers/views/Forecast' 4 | import { useRouter } from 'next/router' 5 | 6 | type Props = React.ComponentProps 7 | 8 | const ForecastPage: NextPage = () => { 9 | const router = useRouter() 10 | 11 | return ( 12 | 13 | ) 14 | } 15 | 16 | export default ForecastPage 17 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { NextPage } from 'next' 3 | import HomeContainer from '~/containers/views/Home' 4 | 5 | type Props ={ 6 | } 7 | 8 | const Index: NextPage = () => { 9 | return ( 10 | 11 | ) 12 | } 13 | 14 | export default Index 15 | -------------------------------------------------------------------------------- /public/css/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Noto Sans JP', sans-serif; 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaz29/nextjs-template/3d239b93e417578209f8d77b4df18a57d9335be6/public/favicon.ico -------------------------------------------------------------------------------- /public/images/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaz29/nextjs-template/3d239b93e417578209f8d77b4df18a57d9335be6/public/images/.gitkeep -------------------------------------------------------------------------------- /public/static/locales/en_us/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "Welcome to": "Welcome to" 3 | } -------------------------------------------------------------------------------- /public/static/locales/ja_jp/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "Welcome to": "ようこそ" 3 | } -------------------------------------------------------------------------------- /public/staticwebapp.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [ 3 | { 4 | "route": "/current/tokyo", 5 | "rewrite": "/current/tokyo.html" 6 | }, { 7 | "route": "/current/osaka", 8 | "rewrite": "/current/osaka.html" 9 | }, { 10 | "route": "/forecast/*", 11 | "rewrite": "/forecast/[code].html" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /src/__fixtures__/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaz29/nextjs-template/3d239b93e417578209f8d77b4df18a57d9335be6/src/__fixtures__/.gitkeep -------------------------------------------------------------------------------- /src/__fixtures__/current_weather.ts: -------------------------------------------------------------------------------- 1 | import { CurrentWeather } from '~/types' 2 | 3 | export const currentWeather: CurrentWeather = { 4 | location: { 5 | name: 'Tokyo', 6 | region: 'Tōkyō', 7 | country: 'Japan', 8 | lat: 35.69, 9 | lon: 139.69, 10 | tz_id: 'Asia/Tokyo', 11 | localtime_epoch: 1594763203, 12 | localtime: '2020-07-15 6:46', 13 | }, 14 | current: { 15 | last_updated_epoch: 1594763137, 16 | last_updated: '2020-07-15 06:45', 17 | temp_c: 21, 18 | temp_f: 69.8, 19 | is_day: 1, 20 | condition: { 21 | text: 'Light rain', 22 | icon: '//cdn.weatherapi.com/weather/64x64/day/296.png', 23 | code: 1183, 24 | }, 25 | wind_mph: 8.1, 26 | wind_kph: 13, 27 | wind_degree: 50, 28 | wind_dir: 'NE', 29 | pressure_mb: 999, 30 | pressure_in: 30, 31 | precip_mm: 0.3, 32 | precip_in: 0.01, 33 | humidity: 94, 34 | cloud: 75, 35 | feelslike_c: 21, 36 | feelslike_f: 69.8, 37 | vis_km: 8, 38 | vis_miles: 4, 39 | uv: 5, 40 | gust_mph: 16.8, 41 | gust_kph: 27, 42 | }, 43 | } 44 | -------------------------------------------------------------------------------- /src/__fixtures__/forecast.ts: -------------------------------------------------------------------------------- 1 | import { Forecast } from '~/types' 2 | 3 | export const forecast: Forecast = { 4 | location: { 5 | name: 'Tokyo', 6 | region: 'TÅkyÅ', 7 | country: 'Japan', 8 | lat: 35.69, 9 | lon: 139.69, 10 | tz_id: 'Asia/Tokyo', 11 | localtime_epoch: 1594977674, 12 | localtime: '2020-07-17 18:21', 13 | }, 14 | current: { 15 | last_updated_epoch: 1594977322, 16 | last_updated: '2020-07-17 18:15', 17 | temp_c: 20, 18 | temp_f: 68, 19 | is_day: 1, 20 | condition: { 21 | text: 'Light rain shower', 22 | icon: '//cdn.weatherapi.com/weather/64x64/day/353.png', 23 | code: 1240, 24 | }, 25 | wind_mph: 11.9, 26 | wind_kph: 19.1, 27 | wind_degree: 40, 28 | wind_dir: 'NE', 29 | pressure_mb: 1011, 30 | pressure_in: 30.3, 31 | precip_mm: 0.6, 32 | precip_in: 0.02, 33 | humidity: 94, 34 | cloud: 75, 35 | feelslike_c: 20, 36 | feelslike_f: 68, 37 | vis_km: 10, 38 | vis_miles: 6, 39 | uv: 6, 40 | gust_mph: 12.1, 41 | gust_kph: 19.4, 42 | }, 43 | forecast: { 44 | forecastday: [ 45 | { 46 | date: '2020-07-17', 47 | date_epoch: 1594944000, 48 | day: { 49 | maxtemp_c: 26.5, 50 | maxtemp_f: 79.7, 51 | mintemp_c: 21.5, 52 | mintemp_f: 70.7, 53 | avgtemp_c: 23.6, 54 | avgtemp_f: 74.5, 55 | maxwind_mph: 9.4, 56 | maxwind_kph: 15.1, 57 | totalprecip_mm: 4.1, 58 | totalprecip_in: 0.16, 59 | avgvis_km: 6.9, 60 | avgvis_miles: 4, 61 | avghumidity: 73, 62 | daily_will_it_rain: 1, 63 | daily_chance_of_rain: 95, 64 | daily_will_it_snow: 0, 65 | daily_chance_of_snow: 0, 66 | condition: { 67 | text: 'Light drizzle', 68 | icon: '//cdn.weatherapi.com/weather/64x64/day/266.png', 69 | code: 1153, 70 | }, 71 | uv: 7, 72 | }, 73 | astro: { 74 | sunrise: '04:38 AM', 75 | sunset: '06:56 PM', 76 | moonrise: '01:21 AM', 77 | moonset: '03:50 PM', 78 | }, 79 | }, 80 | { 81 | date: '2020-07-18', 82 | date_epoch: 1595030400, 83 | day: { 84 | maxtemp_c: 24.3, 85 | maxtemp_f: 75.7, 86 | mintemp_c: 16.3, 87 | mintemp_f: 61.3, 88 | avgtemp_c: 22.1, 89 | avgtemp_f: 71.8, 90 | maxwind_mph: 10.1, 91 | maxwind_kph: 16.2, 92 | totalprecip_mm: 41.4, 93 | totalprecip_in: 1.63, 94 | avgvis_km: 8, 95 | avgvis_miles: 4, 96 | avghumidity: 83, 97 | daily_will_it_rain: 1, 98 | daily_chance_of_rain: 88, 99 | daily_will_it_snow: 0, 100 | daily_chance_of_snow: 0, 101 | condition: { 102 | text: 'Moderate rain', 103 | icon: '//cdn.weatherapi.com/weather/64x64/day/302.png', 104 | code: 1189, 105 | }, 106 | uv: 5, 107 | }, 108 | astro: { 109 | sunrise: '04:39 AM', 110 | sunset: '06:56 PM', 111 | moonrise: '02:01 AM', 112 | moonset: '04:50 PM', 113 | }, 114 | }, 115 | { 116 | date: '2020-07-19', 117 | date_epoch: 1595116800, 118 | day: { 119 | maxtemp_c: 27.7, 120 | maxtemp_f: 81.9, 121 | mintemp_c: 24.2, 122 | mintemp_f: 75.6, 123 | avgtemp_c: 25.7, 124 | avgtemp_f: 78.3, 125 | maxwind_mph: 17.2, 126 | maxwind_kph: 27.7, 127 | totalprecip_mm: 0.5, 128 | totalprecip_in: 0.02, 129 | avgvis_km: 9.4, 130 | avgvis_miles: 5, 131 | avghumidity: 71, 132 | daily_will_it_rain: 1, 133 | daily_chance_of_rain: 78, 134 | daily_will_it_snow: 0, 135 | daily_chance_of_snow: 0, 136 | condition: { 137 | text: 'Moderate rain at times', 138 | icon: '//cdn.weatherapi.com/weather/64x64/day/299.png', 139 | code: 1186, 140 | }, 141 | uv: 5, 142 | }, 143 | astro: { 144 | sunrise: '04:39 AM', 145 | sunset: '06:55 PM', 146 | moonrise: '02:48 AM', 147 | moonset: '05:49 PM', 148 | }, 149 | }, 150 | ], 151 | }, 152 | alert: {}, 153 | } 154 | -------------------------------------------------------------------------------- /src/__mocks__/nextConfig.ts: -------------------------------------------------------------------------------- 1 | const getConfig = () => { 2 | return { 3 | publicRuntimeConfig: { 4 | WEATHER_API_ENDPOINT: process.env.WEATHER_API_ENDPOINT, 5 | WEATHER_API_KEY: process.env.WEATHER_API_KEY, 6 | }, 7 | } 8 | } 9 | 10 | export default getConfig 11 | -------------------------------------------------------------------------------- /src/__stories__/button.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import { Button, Grid, makeStyles } from '@material-ui/core' 4 | 5 | const useStyles = makeStyles(() => ({ 6 | button: { 7 | marginBottom: 16, 8 | }, 9 | container: { 10 | marginTop: 16, 11 | }, 12 | })) 13 | 14 | storiesOf('material-ui/Button', module) 15 | .add('default', () => ( 16 | 17 | )) 18 | .add('list', () => { 19 | // tslint:disable-next-line:react-hooks-nesting 20 | const classes = useStyles() 21 | return ( 22 | 29 | 30 | 31 | 32 | 35 | 38 | 39 | 40 | 41 | 42 | 45 | 46 | 49 | 52 | 55 | 58 | 59 | 60 | 61 | ) 62 | }, { info: { disable: true }}) 63 | -------------------------------------------------------------------------------- /src/__stories__/notisnack.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import Button from '@material-ui/core/Button' 4 | import { SnackbarProvider, useSnackbar } from 'notistack' 5 | 6 | type ButtonProps = { 7 | variant: 'default' | 'error' | 'success' | 'warning' | 'info' 8 | message: string 9 | } 10 | 11 | type Props = { 12 | button: ButtonProps 13 | } 14 | 15 | const AddSnackButton: React.FC = ({ 16 | button, 17 | }) => { 18 | const { enqueueSnackbar } = useSnackbar() 19 | 20 | return ( 21 |
22 | 32 |
33 | ) 34 | } 35 | 36 | const buttons: ButtonProps[] = [ 37 | { variant: 'success', message: 'Successfully done the operation.' }, 38 | { variant: 'error', message: 'Something went wrong.' }, 39 | { variant: 'warning', message: 'Be careful of what you just did!' }, 40 | { variant: 'info', message: 'For your info...' }, 41 | ] 42 | 43 | storiesOf('material-ui/Notisnack', module) 44 | .add('default', () => { 45 | return ( 46 | 53 | { 54 | buttons.map((button, index) => ( 55 | 59 | )) 60 | } 61 | 62 | ) 63 | }) 64 | -------------------------------------------------------------------------------- /src/__stories__/toolbar.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import { createStyles, makeStyles, Theme } from '@material-ui/core' 4 | import AppBar from '@material-ui/core/AppBar' 5 | import Toolbar from '@material-ui/core/Toolbar' 6 | import Typography from '@material-ui/core/Typography' 7 | import Button from '@material-ui/core/Button' 8 | import IconButton from '@material-ui/core/IconButton' 9 | import MenuIcon from '@material-ui/icons/Menu' 10 | 11 | const useStyles = makeStyles((theme: Theme) => 12 | createStyles({ 13 | root: { 14 | flexGrow: 1, 15 | }, 16 | menuButton: { 17 | marginRight: theme.spacing(2), 18 | }, 19 | title: { 20 | flexGrow: 1, 21 | }, 22 | }), 23 | ) 24 | 25 | storiesOf('material-ui/Toolbar', module) 26 | .add('default', () => { 27 | // tslint:disable-next-line: react-hooks-nesting 28 | const classes = useStyles() 29 | 30 | return ( 31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | News 39 | 40 | 41 | 42 | 43 |
44 | ) 45 | }) 46 | -------------------------------------------------------------------------------- /src/components/ForecastCard/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import ForecastCard from '.' 4 | import { forecast } from '~/__fixtures__/forecast' 5 | 6 | storiesOf('components/ForecastCard', module) 7 | .add('default', () => ( 8 | 11 | )) 12 | -------------------------------------------------------------------------------- /src/components/ForecastCard/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ForecastDay } from '~/types' 3 | import { Card, CardHeader, CardContent, Typography, CardMedia, Box } from '@material-ui/core' 4 | import { useStyles } from './styles' 5 | import BeachAccessIcon from '@material-ui/icons/BeachAccess' 6 | 7 | type Props = { 8 | forecastDay: ForecastDay 9 | } 10 | 11 | const ForecastCard: React.FC = ({ 12 | forecastDay, 13 | }) => { 14 | const classes = useStyles() 15 | 16 | return ( 17 | 18 | 22 | 25 | 26 | 27 | {forecastDay.day.condition.text} 28 | 29 | 30 | 31 | {forecastDay.day.maxtemp_c} 32 | 33 | 34 | 35 | / 36 | 37 | 38 | {forecastDay.day.mintemp_c} 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | {forecastDay.day.daily_chance_of_rain} 48 | % 49 | 50 | 51 | 52 | 53 | ) 54 | } 55 | 56 | export default ForecastCard 57 | -------------------------------------------------------------------------------- /src/components/ForecastCard/styles.tsx: -------------------------------------------------------------------------------- 1 | import { createStyles, makeStyles, Theme } from '@material-ui/core' 2 | 3 | export const useStyles = makeStyles((theme: Theme) => 4 | createStyles({ 5 | container: { 6 | height: 250, 7 | width: 320, 8 | [theme.breakpoints.up('sm')]: { 9 | width: '100%', 10 | }, 11 | marginTop: 16, 12 | }, 13 | icon: { 14 | width: 64, 15 | height: 64, 16 | marginLeft: 'auto', 17 | marginRight: 'auto', 18 | }, 19 | windIconContainer: { 20 | marginTop: 16, 21 | }, 22 | minTempText: { 23 | color: theme.palette.info.main, 24 | }, 25 | separater: { 26 | marginLeft: 8, 27 | marginRight: 8, 28 | }, 29 | }), 30 | ) 31 | -------------------------------------------------------------------------------- /src/components/GlobalStyle/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { CssBaseline } from '@material-ui/core' 3 | 4 | const GlobalStyle: React.FC = () => { 5 | useEffect(() => { 6 | const WebFontLoader = require('webfontloader') 7 | WebFontLoader.load({ 8 | timeout: 3000, 9 | google: { 10 | families: [ 11 | 'Noto+Sans+JP:400,700', 12 | ], 13 | }, 14 | }) 15 | }, []) 16 | return () 17 | } 18 | 19 | export default GlobalStyle 20 | -------------------------------------------------------------------------------- /src/components/Header/components/MenuItem/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { default as MaterialUIMenuItem } from '@material-ui/core/MenuItem' 3 | 4 | type Props = { 5 | label: string 6 | currentPath: string 7 | path: string 8 | as: string 9 | onClick: (path: string, as: string) => void 10 | } 11 | 12 | const MenuItem: React.FC = ({ 13 | label, 14 | currentPath, 15 | path, 16 | as, 17 | onClick, 18 | }) => { 19 | 20 | return onClick(path, as)}> 23 | {label} 24 | 25 | } 26 | 27 | export default MenuItem 28 | -------------------------------------------------------------------------------- /src/components/Header/components/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import Header from './' 4 | import { useTheme } from '@material-ui/core' 5 | import { menus } from '../constants' 6 | 7 | storiesOf('components/Header', module) 8 | .add('default', () => { 9 | // tslint:disable-next-line: react-hooks-nesting 10 | const theme = useTheme() 11 | 12 | return ( 13 |
18 |
console.log(`menuSelected: ${path}`)} 22 | /> 23 |
24 | ) 25 | }) 26 | -------------------------------------------------------------------------------- /src/components/Header/components/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import Box from '@material-ui/core/Box' 3 | import AppBar from '@material-ui/core/AppBar' 4 | import Toolbar from '@material-ui/core/Toolbar' 5 | import Typography from '@material-ui/core/Typography' 6 | import IconButton from '@material-ui/core/IconButton' 7 | import MenuIcon from '@material-ui/icons/Menu' 8 | import {default as MenuComponent } from '@material-ui/core/Menu' 9 | import { useStyles } from './styles' 10 | import MenuItem from './MenuItem' 11 | import { Menu } from '../types' 12 | import { Divider } from '@material-ui/core' 13 | 14 | type Props = { 15 | menus: Menu[] 16 | currentPath: string 17 | handleMenuSelect: (path: string, as: string) => void 18 | } 19 | 20 | const Presentational: React.FC = ({ 21 | menus, 22 | currentPath, 23 | handleMenuSelect, 24 | ...rest 25 | }) => { 26 | const classes = useStyles() 27 | const [anchorEl, setAnchorEl] = useState(null) 28 | 29 | const handleClick = (event: React.MouseEvent) => { 30 | setAnchorEl(event.currentTarget) 31 | } 32 | 33 | const handleClose = () => { 34 | setAnchorEl(null) 35 | } 36 | 37 | const handleMenuClick = (path: string, as: string) => { 38 | handleMenuSelect(path, as) 39 | setAnchorEl(null) 40 | } 41 | 42 | return ( 43 | 44 | 45 | 46 | 47 | 54 | 55 | 56 | 57 | NextJS template 58 | 59 | 60 | 61 | 68 | {menus.map((menu, index) => { 69 | if (menu.path && menu.as) { 70 | return 78 | } else { 79 | return 82 | } 83 | })} 84 | 85 | 86 | 87 | ) 88 | } 89 | 90 | export default Presentational 91 | -------------------------------------------------------------------------------- /src/components/Header/components/styles.tsx: -------------------------------------------------------------------------------- 1 | import { createStyles, makeStyles, Theme } from '@material-ui/core' 2 | 3 | export const useStyles = makeStyles((theme: Theme) => 4 | createStyles({ 5 | wrapper: { 6 | backgroundColor: theme.palette.primary.main, 7 | position: 'relative', 8 | flex: 1, 9 | minWidth: '100%', 10 | minHeight: 56, 11 | [theme.breakpoints.up('sm')]: { 12 | minHeight: 64, 13 | }, 14 | }, 15 | menuButton: { 16 | marginRight: theme.spacing(2), 17 | }, 18 | title: { 19 | flexGrow: 1, 20 | }, 21 | container: { 22 | position: 'absolute', 23 | top: 0, 24 | left: 0, 25 | bottom: 0, 26 | right: 0, 27 | margin: 'auto', 28 | maxWidth: theme.breakpoints.values.sm, 29 | [theme.breakpoints.up('md')]: { 30 | maxWidth: theme.breakpoints.values.md, 31 | }, 32 | }, 33 | }), 34 | ) 35 | -------------------------------------------------------------------------------- /src/components/Header/constants.ts: -------------------------------------------------------------------------------- 1 | import { Menu } from './types' 2 | 3 | export const menus: Menu[] = [ 4 | { 5 | label: 'Home', 6 | path: '/', 7 | as: '/', 8 | },{ 9 | label: 'Current - Tokyo', 10 | },{ 11 | label: 'Current - Tokyo', 12 | path: '/current/[code]', 13 | as: '/current/tokyo', 14 | },{ 15 | label: 'Current - Osaka', 16 | path: '/current/[code]', 17 | as: '/current/osaka', 18 | },{ 19 | label: 'Current - Tokyo', 20 | },{ 21 | label: 'Forecast - Tokyo', 22 | path: '/forecast/[code]', 23 | as: '/forecast/tokyo', 24 | },{ 25 | label: 'Forecast - Osaka', 26 | path: '/forecast/[code]', 27 | as: '/forecast/osaka', 28 | }, 29 | ] 30 | -------------------------------------------------------------------------------- /src/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { } from 'react' 2 | import Presentational from './components' 3 | import Router, { useRouter } from 'next/router' 4 | import { menus } from './constants' 5 | 6 | type Props = { 7 | } 8 | 9 | const Header: React.FC = (props) => { 10 | const router = useRouter() 11 | 12 | const handleMenuSelect = (path: string, as: string) => { 13 | Router.push(path, as) 14 | } 15 | 16 | return ( 17 | 23 | ) 24 | } 25 | 26 | export default Header 27 | -------------------------------------------------------------------------------- /src/components/Header/types.ts: -------------------------------------------------------------------------------- 1 | export type Menu = { 2 | label: string 3 | path?: string 4 | as?: string 5 | icon?: string 6 | } 7 | -------------------------------------------------------------------------------- /src/components/LoadingCover/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import LoadingCover from './' 4 | 5 | storiesOf('components/LoadingCover', module) 6 | .add('default', () => ( 7 |
12 | 13 |
14 | )) 15 | -------------------------------------------------------------------------------- /src/components/LoadingCover/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { CircularProgress, Box } from '@material-ui/core' 3 | import { useStyles } from './styles' 4 | 5 | const LoadingCover: React.FC = (props) => { 6 | const classes = useStyles() 7 | 8 | return ( 9 | 10 | 11 | 12 | ) 13 | } 14 | 15 | export default LoadingCover 16 | -------------------------------------------------------------------------------- /src/components/LoadingCover/styles.tsx: -------------------------------------------------------------------------------- 1 | import { createStyles, makeStyles } from '@material-ui/core' 2 | import { colors } from '~/theme' 3 | 4 | export const useStyles = makeStyles(() => 5 | createStyles({ 6 | wrapper: { 7 | backgroundColor: colors.coverBackground, 8 | width: '100%', 9 | height: '100%', 10 | position: 'relative', 11 | textAlign: 'center', 12 | }, 13 | progress: { 14 | position: 'absolute', 15 | top: 0, 16 | left: 0, 17 | bottom: 0, 18 | right: 0, 19 | margin: 'auto', 20 | }, 21 | }), 22 | ) 23 | -------------------------------------------------------------------------------- /src/components/WeatherCard/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import WeatherCard from '.' 4 | import { currentWeather } from '~/__fixtures__/current_weather' 5 | 6 | storiesOf('components/WeatherCard', module) 7 | .add('default', () => ( 8 | 11 | )) 12 | -------------------------------------------------------------------------------- /src/components/WeatherCard/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { CurrentWeather } from '~/types' 3 | import { Card, CardHeader, CardContent, Typography, CardMedia, Box } from '@material-ui/core' 4 | import { useStyles } from './styles' 5 | import WindIcon from '~/components/WindIcon' 6 | 7 | type Props = { 8 | currentWeather: CurrentWeather 9 | } 10 | 11 | const WeatherCard: React.FC = ({ 12 | currentWeather, 13 | }) => { 14 | const classes = useStyles() 15 | 16 | return ( 17 | 18 | 24 | 27 | 28 | 29 | {currentWeather.current.condition.text} 30 | 31 | 32 | {currentWeather.current.temp_c}℃ 33 | 34 | 35 | 40 | 41 | 42 | 43 | ) 44 | } 45 | 46 | export default WeatherCard 47 | -------------------------------------------------------------------------------- /src/components/WeatherCard/styles.tsx: -------------------------------------------------------------------------------- 1 | import { createStyles, makeStyles } from '@material-ui/core' 2 | 3 | export const useStyles = makeStyles(() => 4 | createStyles({ 5 | container: { 6 | width: 320, 7 | height: 260, 8 | marginTop: 16, 9 | marginLeft: 'auto', 10 | marginRight: 'auto', 11 | }, 12 | icon: { 13 | width: 64, 14 | height: 64, 15 | marginLeft: 'auto', 16 | marginRight: 'auto', 17 | }, 18 | windIconContainer: { 19 | marginTop: 8, 20 | }, 21 | }), 22 | ) 23 | -------------------------------------------------------------------------------- /src/components/WindIcon/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import WindIcon from './' 4 | import { Box } from '@material-ui/core' 5 | 6 | storiesOf('components/WindIcon', module) 7 | .add('default', () => { 8 | 9 | return ( 10 | 11 | 16 | 17 | ) 18 | }) 19 | .add('list', () => { 20 | const winds = [ 21 | {degree: 0, dir: 'N', kph: 10}, 22 | {degree: 45, dir: 'NE', kph: 11}, 23 | {degree: 90, dir: 'E', kph: 12}, 24 | {degree: 135, dir: 'SE', kph: 13}, 25 | {degree: 180, dir: 'S', kph: 14}, 26 | {degree: 225, dir: 'SW', kph: 15}, 27 | {degree: 270, dir: 'W', kph: 16}, 28 | {degree: 315, dir: 'NW', kph: 17}, 29 | ] 30 | 31 | return ( 32 |
33 | { 34 | winds.map((wind, index) => { 35 | return ( 36 | 37 | 42 | 43 | ) 44 | }) 45 | } 46 |
47 | ) 48 | }, { info: { disable: true } }) 49 | -------------------------------------------------------------------------------- /src/components/WindIcon/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import NavigationIcon from '@material-ui/icons/Navigation' 3 | import { useStyles } from './styles' 4 | import { Typography } from '@material-ui/core' 5 | 6 | type Props = { 7 | degree: number 8 | kph: number 9 | dir: string 10 | } 11 | 12 | const WindIcon: React.FC = ({ 13 | degree, 14 | kph, 15 | dir, 16 | }) => { 17 | const classes = useStyles({degree}) 18 | 19 | return ( 20 | <> 21 | 22 | 23 | {`${dir}/${kph}kph`} 24 | 25 | 26 | ) 27 | } 28 | 29 | export default WindIcon 30 | -------------------------------------------------------------------------------- /src/components/WindIcon/styles.tsx: -------------------------------------------------------------------------------- 1 | import { makeStyles } from '@material-ui/core' 2 | import { getWindIconDegree } from '~/utils/weather' 3 | 4 | type Props = { 5 | degree: number 6 | } 7 | 8 | export const useStyles = makeStyles({ 9 | icon: (props: Props) => ({ 10 | transform: `rotateZ(${getWindIconDegree(props.degree)}deg)`, 11 | marginRight: 8, 12 | }), 13 | }) 14 | -------------------------------------------------------------------------------- /src/containers/views/CurrentWeather/components/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import Presentational from '.' 4 | import { currentWeather } from '~/__fixtures__/current_weather' 5 | 6 | storiesOf('containers/views/CurrentWeather', module) 7 | .add('default', () => ( 8 | 12 | )) 13 | -------------------------------------------------------------------------------- /src/containers/views/CurrentWeather/components/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useStyles } from './styles' 3 | import { Box } from '@material-ui/core' 4 | import { CurrentWeather } from '~/types' 5 | import WeatherCard from '~/components/WeatherCard' 6 | import LoadingCover from '~/components/LoadingCover' 7 | 8 | type Props = { 9 | loading: boolean 10 | currentWeather?: CurrentWeather 11 | } 12 | 13 | const Presentational: React.FC = ({ 14 | loading, 15 | currentWeather, 16 | }) => { 17 | const classes = useStyles() 18 | 19 | if (loading) { 20 | return ( 21 | 22 | 23 | 24 | ) 25 | } 26 | 27 | return ( 28 | 29 | { 30 | currentWeather && 31 | 32 | } 33 | 34 | ) 35 | } 36 | 37 | export default Presentational 38 | -------------------------------------------------------------------------------- /src/containers/views/CurrentWeather/components/styles.tsx: -------------------------------------------------------------------------------- 1 | import { createStyles, makeStyles } from '@material-ui/core' 2 | 3 | export const useStyles = makeStyles(() => 4 | createStyles({ 5 | loadingContainer: { 6 | marginTop: 32, 7 | }, 8 | container: { 9 | width: '100%', 10 | height: '100%', 11 | marginTop: 16, 12 | }, 13 | }), 14 | ) 15 | -------------------------------------------------------------------------------- /src/containers/views/CurrentWeather/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useSnackbar } from 'notistack' 2 | import { useAsync } from 'react-use' 3 | import client from '~/requests/client' 4 | import WeatherAPI from '~/requests/WeatherAPI' 5 | 6 | interface Props { 7 | code?: string 8 | } 9 | 10 | export const useCurrentWeather = ({ code }: Props) => { 11 | const { enqueueSnackbar } = useSnackbar() 12 | 13 | const { loading, value } = useAsync(async () => { 14 | if (!code) return 15 | 16 | try { 17 | const { response } = await client.request(WeatherAPI.getCurrent({ 18 | q: code, 19 | key: WeatherAPI.getApiKey(), 20 | })) 21 | 22 | return response 23 | } catch (error) { 24 | enqueueSnackbar('Error', { variant: 'error'}) 25 | } 26 | }, [code]) 27 | 28 | return { 29 | loading, 30 | currentWeather: value || undefined, 31 | } 32 | } -------------------------------------------------------------------------------- /src/containers/views/CurrentWeather/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Presentational from './components' 3 | import { useCurrentWeather } from './hooks' 4 | 5 | type Props = { 6 | code?: string 7 | } 8 | 9 | const CurrentContainerView: React.FC = ({ 10 | code, 11 | }) => { 12 | const { loading, currentWeather } = useCurrentWeather({ code }) 13 | 14 | return ( 15 | 16 | ) 17 | } 18 | 19 | export default CurrentContainerView 20 | -------------------------------------------------------------------------------- /src/containers/views/Forecast/components/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import Presentational from '.' 4 | import { forecast } from '~/__fixtures__/forecast' 5 | 6 | storiesOf('containers/views/Forecast', module) 7 | .add('default', () => ( 8 | 12 | )) 13 | -------------------------------------------------------------------------------- /src/containers/views/Forecast/components/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useStyles } from './styles' 3 | import { Box, Grid } from '@material-ui/core' 4 | import { Forecast } from '~/types' 5 | import ForecastCard from '~/components/ForecastCard' 6 | import WeatherCard from '~/components/WeatherCard' 7 | import withWidth, { isWidthUp } from '@material-ui/core/withWidth' 8 | import { Breakpoint } from '@material-ui/core/styles/createBreakpoints' 9 | import LoadingCover from '~/components/LoadingCover' 10 | 11 | type Props = { 12 | loading: boolean 13 | forecast?: Forecast 14 | width?: Breakpoint 15 | } 16 | 17 | const Presentational: React.FC = ({ 18 | loading, 19 | forecast, 20 | width, 21 | }) => { 22 | const classes = useStyles() 23 | 24 | if (loading) { 25 | return ( 26 | 27 | 28 | 29 | ) 30 | } 31 | 32 | return ( 33 | 34 | 41 | 42 | 43 | { 44 | forecast && 45 | 46 | } 47 | 48 | 49 | { 50 | forecast && 51 | forecast.forecast.forecastday.map((forecastDay, index) => ( 52 | 53 | 54 | 55 | )) 56 | } 57 | 58 | 59 | ) 60 | } 61 | 62 | export default withWidth()(Presentational) 63 | -------------------------------------------------------------------------------- /src/containers/views/Forecast/components/styles.tsx: -------------------------------------------------------------------------------- 1 | import { createStyles, makeStyles } from '@material-ui/core' 2 | 3 | export const useStyles = makeStyles(() => 4 | createStyles({ 5 | loadingContainer: { 6 | marginTop: 32, 7 | }, 8 | container: { 9 | marginTop: 16, 10 | }, 11 | }), 12 | ) 13 | -------------------------------------------------------------------------------- /src/containers/views/Forecast/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useSnackbar } from 'notistack' 2 | import { useAsync } from 'react-use' 3 | import client from '~/requests/client' 4 | import WeatherAPI from '~/requests/WeatherAPI' 5 | 6 | interface Props { 7 | code?: string 8 | } 9 | 10 | export const useForecast = ({ code }: Props) => { 11 | const { enqueueSnackbar } = useSnackbar() 12 | 13 | const { loading, value } = useAsync(async () => { 14 | if (!code) return 15 | 16 | try { 17 | const { response } = await client.request(WeatherAPI.getForecast({ 18 | q: code, 19 | days: 7, 20 | key: WeatherAPI.getApiKey(), 21 | })) 22 | 23 | return response 24 | } catch (error) { 25 | enqueueSnackbar('Error', { variant: 'error'}) 26 | } 27 | }, [code]) 28 | 29 | return { 30 | loading, 31 | forecast: value || undefined, 32 | } 33 | } -------------------------------------------------------------------------------- /src/containers/views/Forecast/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Presentational from './components' 3 | import { useForecast } from './hooks' 4 | 5 | type Props = { 6 | code?: string 7 | } 8 | const ForecastView: React.FC = ({ 9 | code, 10 | }) => { 11 | const { 12 | loading, 13 | forecast, 14 | } = useForecast({ code }) 15 | 16 | return ( 17 | 18 | ) 19 | } 20 | 21 | export default ForecastView 22 | -------------------------------------------------------------------------------- /src/containers/views/Home/components/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import Presentational from '.' 4 | 5 | storiesOf('containers/views/Home', module) 6 | .add('default', () => ( 7 | 8 | )) 9 | -------------------------------------------------------------------------------- /src/containers/views/Home/components/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useStyles } from './styles' 3 | import { Box, Typography } from '@material-ui/core' 4 | import { useTranslation } from 'react-i18next' 5 | 6 | type Props = { 7 | } 8 | 9 | const Presentational: React.FC = () => { 10 | const classes = useStyles() 11 | const { t } = useTranslation('common') 12 | 13 | return ( 14 | 15 | 18 | {t('Welcome to')} NextJS Template 19 | 20 | 21 | ) 22 | } 23 | 24 | export default Presentational 25 | -------------------------------------------------------------------------------- /src/containers/views/Home/components/styles.tsx: -------------------------------------------------------------------------------- 1 | import { createStyles, makeStyles } from '@material-ui/core' 2 | 3 | export const useStyles = makeStyles(() => 4 | createStyles({ 5 | container: { 6 | width: '100%', 7 | height: '100%', 8 | marginTop: 16, 9 | }, 10 | }), 11 | ) 12 | -------------------------------------------------------------------------------- /src/containers/views/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Presentational from './components' 3 | 4 | type Props = { 5 | } 6 | const HomeContainer: React.FC = () => { 7 | return ( 8 | 9 | ) 10 | } 11 | 12 | export default HomeContainer 13 | -------------------------------------------------------------------------------- /src/globalStyles.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://material-ui.com/customization/globals/#global-css 3 | */ 4 | 5 | export const globalStyles = { 6 | html: { 7 | margin: 0, 8 | padding: 0, 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /src/i18n.ts: -------------------------------------------------------------------------------- 1 | import NextI18Next from 'next-i18next' 2 | 3 | const languages = [ 4 | 'ja_jp', 5 | 'en_us', 6 | ] 7 | 8 | const i18nInstance = new NextI18Next({ 9 | defaultLanguage: languages[0], 10 | otherLanguages: languages.slice(1), 11 | browserLanguageDetection: true, 12 | // serverLanguageDetection: true, 13 | }) 14 | 15 | export function t(sentence: string, options: any = {}): string { 16 | return i18nInstance.i18n.t ? i18nInstance.i18n.t(sentence, options) : sentence 17 | } 18 | 19 | export default i18nInstance 20 | -------------------------------------------------------------------------------- /src/layouts/default.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { withStyles, Theme, Box } from '@material-ui/core' 3 | import Header from '~/components/Header' 4 | 5 | const DefaultLayout: React.FC = ({ children, ...rest }) => { 6 | 7 | return ( 8 | 9 |
10 | 11 | {children} 12 | 13 | 14 | ) 15 | } 16 | 17 | const Wrapper = withStyles((theme: Theme) => ({ 18 | root: { 19 | backgroundColor: theme.palette.background.default, 20 | flex: 1, 21 | minWidth: '100vw', 22 | minHeight: '100vh', 23 | }, 24 | }))(Box) 25 | 26 | const ContentWrapper = withStyles((theme: Theme) => ({ 27 | root: { 28 | backgroundColor: theme.palette.background.default, 29 | position: 'relative', 30 | flex: 1, 31 | minHeight: '100%', 32 | margin: 'auto', 33 | maxWidth: theme.breakpoints.values.sm, 34 | [theme.breakpoints.up('md')]: { 35 | maxWidth: theme.breakpoints.values.md, 36 | }, 37 | }, 38 | }))(Box) 39 | 40 | export default DefaultLayout 41 | -------------------------------------------------------------------------------- /src/layouts/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import DefaultLayout from './default' 3 | 4 | type Props = { 5 | pathname: string 6 | } 7 | 8 | enum LayoutType { 9 | DEFAULT = 'default', 10 | } 11 | 12 | const getLayoutType = (pathname: string): LayoutType => { 13 | switch (pathname) { 14 | default: 15 | return LayoutType.DEFAULT 16 | } 17 | } 18 | 19 | const Layout: React.FC = ({ pathname, ...rest }) => { 20 | const layoutType = getLayoutType(pathname) 21 | 22 | switch(layoutType) { 23 | default: 24 | return () 25 | } 26 | } 27 | 28 | export default Layout -------------------------------------------------------------------------------- /src/requests/WeatherAPI.ts: -------------------------------------------------------------------------------- 1 | import { CurrentWeather, Forecast } from '~/types' 2 | import Endpoint from './core/Endpoint' 3 | import getConfig from '~/utils/config' 4 | 5 | export type GetCurrentParam = { 6 | q: string, 7 | key: string, 8 | } 9 | 10 | export type GetForecastParam = { 11 | q: string, 12 | days: number, 13 | key: string, 14 | } 15 | 16 | class WeatherAPI { 17 | static getApiKey = (): string => { 18 | const apikey = getConfig('WEATHER_API_KEY') 19 | return apikey || '' 20 | } 21 | 22 | static getCurrent = (params: GetCurrentParam) => { 23 | return new Endpoint('GET', '/current', { 24 | params, 25 | }) 26 | } 27 | 28 | static getForecast = (params: GetForecastParam) => { 29 | return new Endpoint('GET', '/forecast', { 30 | params, 31 | }) 32 | } 33 | } 34 | 35 | export default WeatherAPI 36 | -------------------------------------------------------------------------------- /src/requests/client.ts: -------------------------------------------------------------------------------- 1 | import Client from './core/Client' 2 | import getConfig from '~/utils/config' 3 | 4 | const browserBaseURL = getConfig('WEATHER_API_ENDPOINT') 5 | const client = new Client({ 6 | browserBaseURL: browserBaseURL!, 7 | defaultHeaders: { 8 | 'Content-Type': 'application/json; charset=utf-8', 9 | }, 10 | }) 11 | 12 | export default client 13 | -------------------------------------------------------------------------------- /src/requests/core/Client.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-unfetch' 2 | import { stringify } from 'query-string' 3 | import { AnyObject } from '~/types' 4 | import Endpoint from './Endpoint' 5 | import Result from './Result' 6 | 7 | type Headers = { [key: string]: any } 8 | 9 | type Config = Partial & Required> 10 | 11 | class Client { 12 | browserBaseURL: string 13 | defaultHeaders?: Headers 14 | 15 | validate = (res: Response): boolean => { 16 | return res.status >= 200 && res.status < 400 17 | } 18 | 19 | constructor({ browserBaseURL, ...rest }: Config) { 20 | this.browserBaseURL = browserBaseURL 21 | Object.assign(this, rest) 22 | } 23 | 24 | public async request(endpoint: Endpoint): Promise> { 25 | const { method, config } = endpoint 26 | const { params = {} } = config 27 | 28 | let query: AnyObject = {} 29 | let body: AnyObject | undefined 30 | 31 | if (method === 'GET') { 32 | query = params as AnyObject 33 | } else { 34 | body = params as AnyObject 35 | } 36 | 37 | const baseURL = this.browserBaseURL 38 | const queryString = stringify(query, { arrayFormat: 'bracket' }) 39 | const buildURL = `${baseURL}${endpoint.path}.json?${queryString}` 40 | 41 | return fetch(buildURL, { 42 | method: endpoint.method, 43 | headers: { 44 | ...this.defaultHeaders, 45 | }, 46 | mode: 'cors', 47 | body: JSON.stringify(body), 48 | }) 49 | .then(async (res) => { 50 | if (!this.validate(res)) { 51 | return Promise.reject(res) 52 | } 53 | 54 | const response = await res.json() 55 | return new Result( 56 | res.status, 57 | response, 58 | ) 59 | }) 60 | } 61 | } 62 | 63 | export default Client 64 | -------------------------------------------------------------------------------- /src/requests/core/Endpoint.ts: -------------------------------------------------------------------------------- 1 | import { AnyObject } from '~/types' 2 | 3 | export type Method = 'GET' | 'POST' | 'PUT' |'PATCH' | 'DELETE' 4 | 5 | type Config = { 6 | params?: AnyObject 7 | } 8 | 9 | class Endpoint { 10 | constructor( 11 | public readonly method: Method, 12 | public readonly path: string, 13 | public readonly config: Config = {}, 14 | ) { 15 | Object.assign(this, config) 16 | } 17 | } 18 | 19 | export default Endpoint 20 | -------------------------------------------------------------------------------- /src/requests/core/Result.ts: -------------------------------------------------------------------------------- 1 | class Result { 2 | constructor( 3 | public readonly statusCode: number, 4 | public readonly response: MainResponse, 5 | ) {} 6 | } 7 | 8 | export default Result 9 | -------------------------------------------------------------------------------- /src/theme.ts: -------------------------------------------------------------------------------- 1 | import { createMuiTheme, Theme } from '@material-ui/core/styles' 2 | import { red, indigo, pink, orange, blue, green, grey } from '@material-ui/core/colors' 3 | import { BreakpointValues } from '@material-ui/core/styles/createBreakpoints' 4 | import { globalStyles } from './globalStyles' 5 | 6 | const defaultFontFamily = '"Roboto", "Helvetica", "Arial", sans-serif' 7 | const defaultBreakpoints: BreakpointValues = { 8 | xs: 0, 9 | sm: 600, 10 | md: 960, 11 | lg: 1280, 12 | xl: 1920, 13 | } 14 | 15 | export const colors = { 16 | coverBackground: 'rgba(0, 0, 0, .2)', 17 | } 18 | 19 | const theme: Theme = createMuiTheme({ 20 | overrides: { 21 | MuiCssBaseline: { 22 | '@global': globalStyles, 23 | }, 24 | }, 25 | 26 | breakpoints: { 27 | values: defaultBreakpoints, 28 | }, 29 | mixins: { 30 | toolbar: { 31 | minHeight: 56, 32 | ['@media (min-width:0px) and (orientation: landscape)']: { 33 | minHeight: 48, 34 | }, 35 | [`@media (min-width:${defaultBreakpoints.sm}px)`]: { 36 | minHeight: 64, 37 | }, 38 | }, 39 | }, 40 | palette: { 41 | common: { 42 | black: '#000', 43 | white: '#fff', 44 | }, 45 | primary: { 46 | light: indigo[300], 47 | main: indigo[500], 48 | dark: indigo[700], 49 | }, 50 | secondary: { 51 | light: pink.A200, 52 | main: pink.A400, 53 | dark: pink.A700, 54 | }, 55 | error: { 56 | light: red[300], 57 | main: red[500], 58 | dark: red[700], 59 | }, 60 | warning: { 61 | light: orange[300], 62 | main: orange[500], 63 | dark: orange[700], 64 | }, 65 | info: { 66 | light: blue[300], 67 | main: blue[500], 68 | dark: blue[700], 69 | }, 70 | success: { 71 | light: green[300], 72 | main: green[500], 73 | dark: green[700], 74 | }, 75 | text: { 76 | primary: 'rgba(0, 0, 0, 0.87)', 77 | secondary: 'rgba(0, 0, 0, 0.54)', 78 | disabled: 'rgba(0, 0, 0, 0.38)', 79 | hint: 'rgba(0, 0, 0, 0.38)', 80 | }, 81 | background: { 82 | paper: '#fff', 83 | default: grey[50], 84 | }, 85 | action: { 86 | active: 'rgba(0, 0, 0, 0.54)', 87 | hover: 'rgba(0, 0, 0, 0.1)', 88 | hoverOpacity: 0.1, 89 | selected: 'rgba(0, 0, 0, 0.08)', 90 | selectedOpacity: 0.08, 91 | disabled: 'rgba(0, 0, 0, 0.26)', 92 | disabledBackground: 'rgba(0, 0, 0, 0.12)', 93 | disabledOpacity: 0.38, 94 | focus: 'rgba(0, 0, 0, 0.12)', 95 | focusOpacity: 0.12, 96 | activatedOpacity: 0.12, 97 | }, 98 | }, 99 | typography: { 100 | htmlFontSize: 16, 101 | fontFamily: defaultFontFamily, 102 | fontSize: 14, 103 | fontWeightLight: 300, 104 | fontWeightRegular: 400, 105 | fontWeightMedium: 500, 106 | fontWeightBold: 700, 107 | h1: { 108 | fontFamily: defaultFontFamily, 109 | fontWeight: 300, 110 | fontSize: '6rem', 111 | lineHeight: 1.167, 112 | letterSpacing: '-0.01562em', 113 | }, 114 | h2: { 115 | fontFamily: defaultFontFamily, 116 | fontWeight: 300, 117 | fontSize: '3.75rem', 118 | lineHeight: 1.2, 119 | letterSpacing: '-0.00833em', 120 | }, 121 | h3: { 122 | fontFamily: defaultFontFamily, 123 | fontWeight: 400, 124 | fontSize: '3rem', 125 | lineHeight: 1.167, 126 | letterSpacing: '0em', 127 | }, 128 | h4: { 129 | fontFamily: defaultFontFamily, 130 | fontWeight: 400, 131 | fontSize: '2.125rem', 132 | lineHeight: 1.235, 133 | letterSpacing: '0.00735em', 134 | }, 135 | h5: { 136 | fontFamily: defaultFontFamily, 137 | fontWeight: 400, 138 | fontSize: '1.5rem', 139 | lineHeight: 1.334, 140 | letterSpacing: '0em', 141 | }, 142 | h6: { 143 | fontFamily: defaultFontFamily, 144 | fontWeight: 500, 145 | fontSize: '1.25rem', 146 | lineHeight: 1.6, 147 | letterSpacing: '0.0075em', 148 | }, 149 | subtitle1: { 150 | fontFamily: defaultFontFamily, 151 | fontWeight: 400, 152 | fontSize: '1rem', 153 | lineHeight: 1.75, 154 | letterSpacing: '0.00938em', 155 | }, 156 | subtitle2: { 157 | fontFamily: defaultFontFamily, 158 | fontWeight: 500, 159 | fontSize: '0.875rem', 160 | lineHeight: 1.57, 161 | letterSpacing: '0.00714em', 162 | }, 163 | body1: { 164 | fontFamily: defaultFontFamily, 165 | fontWeight: 400, 166 | fontSize: '1rem', 167 | lineHeight: 1.5, 168 | letterSpacing: '0.00938em', 169 | }, 170 | body2: { 171 | fontFamily: defaultFontFamily, 172 | fontWeight: 400, 173 | fontSize: '0.875rem', 174 | lineHeight: 1.43, 175 | letterSpacing: '0.01071em', 176 | }, 177 | button: { 178 | fontFamily: defaultFontFamily, 179 | fontWeight: 500, 180 | fontSize: '0.875rem', 181 | lineHeight: 1.75, 182 | letterSpacing: '0.02857em', 183 | textTransform: 'uppercase', 184 | }, 185 | caption: { 186 | fontFamily: defaultFontFamily, 187 | fontWeight: 400, 188 | fontSize: '0.75rem', 189 | lineHeight: 1.66, 190 | letterSpacing: '0.03333em', 191 | }, 192 | overline: { 193 | fontFamily: defaultFontFamily, 194 | fontWeight: 400, 195 | fontSize: '0.75rem', 196 | lineHeight: 2.66, 197 | letterSpacing: '0.08333em', 198 | textTransform: 'uppercase', 199 | }, 200 | }, 201 | shape: { 202 | borderRadius: 4, 203 | }, 204 | }) 205 | 206 | export default theme 207 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type AnyObject = { [key: string] : any } 2 | 3 | export type ISize = { 4 | width: number 5 | height: number 6 | } 7 | 8 | export type Location = { 9 | name: string, 10 | region: string, 11 | country: string, 12 | lat: number, 13 | lon: number, 14 | tz_id: string, 15 | localtime_epoch: number, 16 | localtime: string, 17 | } 18 | 19 | export type Condition = { 20 | text: string 21 | icon: string 22 | code: number 23 | } 24 | 25 | export type Weather = { 26 | last_updated_epoch: number, 27 | last_updated: string 28 | temp_c: number 29 | temp_f: number 30 | is_day: number 31 | condition: Condition, 32 | wind_mph: number, 33 | wind_kph: number, 34 | wind_degree: number, 35 | wind_dir: string, 36 | pressure_mb: number, 37 | pressure_in: number, 38 | precip_mm: number, 39 | precip_in: number, 40 | humidity: number, 41 | cloud: number, 42 | feelslike_c: number, 43 | feelslike_f: number, 44 | vis_km: number, 45 | vis_miles: number, 46 | uv: number, 47 | gust_mph: number, 48 | gust_kph: number, 49 | } 50 | 51 | export type ForecastData = { 52 | maxtemp_c: number, 53 | maxtemp_f: number, 54 | mintemp_c: number, 55 | mintemp_f: number, 56 | avgtemp_c: number, 57 | avgtemp_f: number, 58 | maxwind_mph: number, 59 | maxwind_kph: number, 60 | totalprecip_mm: number, 61 | totalprecip_in: number, 62 | avgvis_km: number, 63 | avgvis_miles: number, 64 | avghumidity: number, 65 | daily_will_it_rain: number, 66 | daily_chance_of_rain: number, 67 | daily_will_it_snow: number, 68 | daily_chance_of_snow: number, 69 | condition: Condition, 70 | uv: number, 71 | } 72 | 73 | export type Astro = { 74 | sunrise: string, 75 | sunset: string, 76 | moonrise: string, 77 | moonset: string, 78 | } 79 | 80 | export type ForecastDay = { 81 | date: string, 82 | date_epoch: number, 83 | day: ForecastData, 84 | astro: Astro, 85 | } 86 | 87 | export type CurrentWeather = { 88 | location: Location, 89 | current: Weather, 90 | } 91 | 92 | export type Alert = {} 93 | 94 | export type Forecast = { 95 | location: Location, 96 | current: Weather, 97 | forecast: { 98 | forecastday: ForecastDay[], 99 | }, 100 | alert: Alert, 101 | } 102 | -------------------------------------------------------------------------------- /src/utils/config.tsx: -------------------------------------------------------------------------------- 1 | import {default as getConfigNext } from 'next/config' 2 | 3 | const getConfig = (name: string): string | undefined => { 4 | let publicRuntimeConfig 5 | try { 6 | const config = getConfigNext() 7 | publicRuntimeConfig = config.publicRuntimeConfig 8 | } catch (error) { 9 | /** 10 | * MEMO: SSG時のgetStaticPathsでは、publicRuntimeConfig使用できないためenvから取得する 11 | * @see https://nextjs.org/docs/api-reference/next.config.js/environment-variables 12 | */ 13 | publicRuntimeConfig = process.env 14 | } 15 | const result = publicRuntimeConfig[name] 16 | 17 | return result ? result: undefined 18 | } 19 | export default getConfig 20 | -------------------------------------------------------------------------------- /src/utils/device.ts: -------------------------------------------------------------------------------- 1 | export const isServer = typeof window === 'undefined' 2 | export const isClient = !isServer 3 | -------------------------------------------------------------------------------- /src/utils/weather.test.ts: -------------------------------------------------------------------------------- 1 | import { getWindIconDegree } from './weather' 2 | 3 | describe('getWindTransformDegree', () => { 4 | test('should return valid degree', () => { 5 | expect(getWindIconDegree(0)).toBe(180) 6 | expect(getWindIconDegree(45)).toBe(225) 7 | expect(getWindIconDegree(90)).toBe(270) 8 | expect(getWindIconDegree(135)).toBe(315) 9 | expect(getWindIconDegree(180)).toBe(0) 10 | expect(getWindIconDegree(225)).toBe(45) 11 | expect(getWindIconDegree(270)).toBe(90) 12 | expect(getWindIconDegree(315)).toBe(135) 13 | }) 14 | }) -------------------------------------------------------------------------------- /src/utils/weather.ts: -------------------------------------------------------------------------------- 1 | export const getWindIconDegree = (degree: number): number => { 2 | return (degree+180)%360 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "baseUrl": "./", 10 | "paths": { 11 | "~/*": [ 12 | "./src/*" 13 | ] 14 | }, 15 | "allowJs": true, 16 | "skipLibCheck": true, 17 | "strict": true, 18 | "forceConsistentCasingInFileNames": true, 19 | "noEmit": true, 20 | "esModuleInterop": true, 21 | "module": "esnext", 22 | "moduleResolution": "node", 23 | "resolveJsonModule": true, 24 | "isolatedModules": true, 25 | "jsx": "preserve", 26 | "experimentalDecorators": true 27 | }, 28 | "exclude": [ 29 | "node_modules" 30 | ], 31 | "include": [ 32 | "next-env.d.ts", 33 | "pages/**/*.ts", 34 | "pages/**/*.tsx", 35 | "src/**/*.ts", 36 | "src/**/*.tsx" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended", 5 | "tslint-react", 6 | "tslint-react-hooks", 7 | "tslint-config-prettier" 8 | ], 9 | "jsRules": { 10 | "quotemark": [true, "single"], 11 | "semicolon": [true, "never"], 12 | "object-literal-sort-keys": [false], 13 | "ordered-imports": [false] 14 | }, 15 | "rules": { 16 | "quotemark": [ 17 | true, 18 | "single", 19 | "jsx-double" 20 | ], 21 | "semicolon": [ 22 | true, 23 | "never" 24 | ], 25 | "object-literal-sort-keys": [ 26 | false 27 | ], 28 | "ordered-imports": [ 29 | false 30 | ], 31 | "interface-name": [ 32 | false 33 | ], 34 | "no-require-imports": false, 35 | "no-trailing-whitespace": true, 36 | "max-line-length": [ 37 | true, 38 | 150 39 | ], 40 | "no-empty-interface": [ 41 | false 42 | ], 43 | "member-access": false, 44 | "no-console": false, 45 | "no-bitwise": false, 46 | "no-var-requires": false, 47 | "jsx-no-multiline-js": false, 48 | "object-literal-key-quotes": [ 49 | true, 50 | "as-needed" 51 | ], 52 | "trailing-comma": [ 53 | true, 54 | { 55 | "multiline": { 56 | "objects": "always", 57 | "arrays": "always", 58 | "functions": "always", 59 | "typeLiterals": "ignore" 60 | }, 61 | "esSpecCompliant": true 62 | } 63 | ], 64 | "jsx-no-lambda": false, 65 | "react-hooks-nesting": "error" 66 | }, 67 | "rulesDirectory": [], 68 | "linterOptions": { 69 | "exclude": [ 70 | "static/lp/common/**/*.js" 71 | ] 72 | } 73 | } 74 | --------------------------------------------------------------------------------