= ({
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 |
--------------------------------------------------------------------------------