diff --git a/package.json b/package.json index d23a6cc7..e0843a6a 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,12 @@ }, "dependencies": { "@reach/combobox": "^0.10.4", + "date-fns": "^2.14.0", "history": "^4.10.1", "lodash": "^4.17.15", "node-sass": "^4.14.1", "react": "^16.13.1", + "react-datepicker": "^3.1.3", "react-dom": "^16.13.1", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", @@ -42,6 +44,7 @@ "@types/lodash": "^4.14.154", "@types/node": "^12.0.0", "@types/react": "^16.9.0", + "@types/react-datepicker": "^3.0.2", "@types/react-dom": "^16.9.0", "@types/react-router": "^5.1.7", "@types/react-router-dom": "^5.1.5", diff --git a/public/index.html b/public/index.html index e91d53dd..382252e4 100644 --- a/public/index.html +++ b/public/index.html @@ -6,7 +6,7 @@ - + Instat TV diff --git a/src/config/procedures.tsx b/src/config/procedures.tsx index 50f3c0de..21badd69 100644 --- a/src/config/procedures.tsx +++ b/src/config/procedures.tsx @@ -2,6 +2,7 @@ export const PROCEDURES = { auth_user: 'auth_user', create_user: 'create_user', get_cities: 'get_cities', + get_matches: 'get_matches', get_players_teams_tournaments: 'get_players_teams_tournaments', logout_user: 'logout_user', lst_c_country: 'lst_c_country', diff --git a/src/features/DateFilter/components/DatePicker/hooks.tsx b/src/features/DateFilter/components/DatePicker/hooks.tsx new file mode 100644 index 00000000..4de51321 --- /dev/null +++ b/src/features/DateFilter/components/DatePicker/hooks.tsx @@ -0,0 +1,25 @@ +import { useEffect } from 'react' + +import ru from 'date-fns/locale/ru' +import en from 'date-fns/locale/en-GB' + +import { registerLocale, setDefaultLocale } from 'react-datepicker' + +import { useLexicsStore } from 'features/LexicsStore' + +/** + * react-datepicker использует date-fns локейлы для локализации + * регистрируем [en, ru] чтобы названия дней недели менялись при смене языка + */ +export const useDatepickerLocales = () => { + const { lang } = useLexicsStore() + + useEffect(() => { + registerLocale('ru', ru) + registerLocale('en', en) + }, []) + + useEffect(() => { + setDefaultLocale(lang) + }, [lang]) +} diff --git a/src/features/DateFilter/components/DatePicker/index.tsx b/src/features/DateFilter/components/DatePicker/index.tsx new file mode 100644 index 00000000..31cde6e7 --- /dev/null +++ b/src/features/DateFilter/components/DatePicker/index.tsx @@ -0,0 +1,79 @@ +import React from 'react' + +import type { ReactDatePickerProps } from 'react-datepicker' +import DatePickerComponent from 'react-datepicker' +import 'react-datepicker/dist/react-datepicker.css' + +import addMonths from 'date-fns/addMonths' + +import { useLexicsStore } from 'features/LexicsStore' +import { getDisplayDate, getMonthName } from 'features/DateFilter/helpers' + +import { useDatepickerLocales } from './hooks' +import { + Wrapper, + MonthsWrapper, + CurrentMonth, + MonthButton, +} from './styled' + +type Args = ( + Parameters>[0] + & { lang: string } +) + +const monthType = 'short' + +const renderCustomHeader = ({ + date, + decreaseMonth, + increaseMonth, + lang, +}: Args) => { + const prevMonth = getMonthName({ + date: addMonths(date, -1), + lang, + monthType, + }) + const nextMonth = getMonthName({ + date: addMonths(date, 1), + lang, + monthType, + }) + const currentDate = getDisplayDate({ + date, + lang, + }) + return ( + + {prevMonth} + {currentDate.month}, {currentDate.year} + {nextMonth} + + ) +} + +type Props = Pick + +export const DatePicker = ({ + onChange, + open, + selected, +}: Props) => { + const { lang } = useLexicsStore() + useDatepickerLocales() + return ( + + renderCustomHeader({ ...props, lang })} + /> + + ) +} diff --git a/src/features/DateFilter/components/DatePicker/styled.tsx b/src/features/DateFilter/components/DatePicker/styled.tsx new file mode 100644 index 00000000..103877f7 --- /dev/null +++ b/src/features/DateFilter/components/DatePicker/styled.tsx @@ -0,0 +1,138 @@ +import styled from 'styled-components/macro' + +export const Wrapper = styled.div` + position: absolute; + left: 0; + top: calc(100% + 14px); + + .react-datepicker { + margin: 0; + border: none; + width: 288px; + background-color: #666666; + box-shadow: 0px 2px 50px #000000; + border-radius: 2px; + font-family: inherit; + color: #fff; + } + + .react-datepicker__header { + border: none; + border-radius: 2; + padding-top: 12px; + background-color: #666666; + } + + .react-datepicker__month-container { + width: 100%; + } + + .react-datepicker__day-names { + margin-top: 24px; + margin-bottom: 12px; + display: flex; + justify-content: space-around; + } + + .react-datepicker__day-name { + margin: 0; + width: 28px; + color: inherit; + font-size: 16px; + line-height: 16px; + text-align: center; + letter-spacing: 0.1px; + } + + .react-datepicker__month { + margin: 0; + } + + .react-datepicker__week { + margin: 8px 0; + display: flex; + justify-content: space-around; + } + + .react-datepicker__day { + margin: 0; + border: none; + outline: none; + width: 28px; + height: 28px; + color: inherit; + opacity: 0.6; + font-weight: normal; + font-size: 12px; + border-radius: 50%; + + :hover { + background-color: rgba(255, 255, 255, 0.4); + } + } + + .react-datepicker__day--selected { + color: #005EDD; + background-color: #ffffff; + opacity: 1; + } + + .react-datepicker__day--keyboard-selected { + color: inherit; + background-color: transparent; + } + + .react-datepicker__day--outside-month { + opacity: 0.2 + } + + .react-datepicker-popper { + transform: translate(0, 0) !important; + } + + .react-datepicker-popper[data-placement^="bottom"] { + margin-top: 0px; + } + + .react-datepicker__input-container, + .react-datepicker__triangle { + display: none; + } +` + +export const MonthsWrapper = styled.div` + width: 100%; + height: 28px; + padding: 0 12px; + display: flex; + align-items: center; + justify-content: space-between; + font-weight: 600; + font-size: 16px; + line-height: 16px; + text-align: center; + letter-spacing: 0.1px; + text-transform: capitalize; +` + +export const CurrentMonth = styled.span` + min-width: 100px; + height: 100%; + padding: 0 14px; + display: flex; + align-items: center; + justify-content: center; + background-color: #005EDD; + border-radius: 16px; +` + +export const MonthButton = styled.button` + border: none; + outline: none; + padding: 0; + opacity: 0.4; + color: inherit; + background-color: transparent; + text-transform: inherit; + cursor: pointer; +` diff --git a/src/features/DateFilter/helpers.tsx b/src/features/DateFilter/helpers.tsx new file mode 100644 index 00000000..86f56a4e --- /dev/null +++ b/src/features/DateFilter/helpers.tsx @@ -0,0 +1,27 @@ +type Args = { + date: Date, + lang: string, + monthType?: 'long' | 'short', +} + +export const getMonthName = ({ + date, + lang, + monthType, +}: Args) => ( + date.toLocaleString(lang, { month: monthType }) +) + +export const getDisplayDate = ({ + date, + lang, + monthType = 'long', +}: Args) => ({ + day: date.getDate(), + month: getMonthName({ + date, + lang, + monthType, + }), + year: date.getFullYear(), +}) diff --git a/src/features/DateFilter/hooks/index.tsx b/src/features/DateFilter/hooks/index.tsx new file mode 100644 index 00000000..cfea2951 --- /dev/null +++ b/src/features/DateFilter/hooks/index.tsx @@ -0,0 +1,69 @@ +import { + useState, + useCallback, + useEffect, +} from 'react' + +import debounce from 'lodash/debounce' +import format from 'date-fns/format' +import addDays from 'date-fns/addDays' +import startOfDay from 'date-fns/startOfDay' + +import { getMatches } from 'requests' +import { useToggle } from 'hooks' +import { useLexicsStore } from 'features/LexicsStore' + +import { getDisplayDate } from '../helpers' + +const dateFormat = 'dd/MM/yyyy HH:mm:ss' + +export const useDateFilter = () => { + const { lang } = useLexicsStore() + const { + close, + isOpen, + open, + } = useToggle() + const fetchMatches = useCallback(debounce(getMatches, 300), []) + const [currentDate, setCurrentDate] = useState(new Date()) + + const date = getDisplayDate({ + date: currentDate, + lang, + }) + + const onPreviousClick = () => { + setCurrentDate(addDays(currentDate, -1)) + } + + const onNextClick = () => { + setCurrentDate(addDays(currentDate, 1)) + } + + const onDateChange = (newDate: Date | null) => { + if (newDate) { + setCurrentDate(newDate) + close() + } + } + + useEffect(() => { + const formattedDate = format(startOfDay(currentDate), dateFormat) + fetchMatches({ + date: formattedDate, + sportType: 1, + tournamentId: 8, + }) + }, [fetchMatches, currentDate]) + + return { + close, + currentDate, + date, + isOpen, + onDateChange, + onNextClick, + onPreviousClick, + open, + } +} diff --git a/src/features/DateFilter/index.tsx b/src/features/DateFilter/index.tsx index 640a302f..925b2999 100644 --- a/src/features/DateFilter/index.tsx +++ b/src/features/DateFilter/index.tsx @@ -1,5 +1,54 @@ import React from 'react' -import { Wrapper } from './styled' +import { OutsideClick } from 'features/OutsideClick' -export const DateFilter = () => +import { useDateFilter } from './hooks' +import { DatePicker } from './components/DatePicker' +import { + Wrapper, + Button, + ArrowLeft, + ArrowRight, + DateButton, + Day, + MonthYearWrapper, + Month, + Year, +} from './styled' + +export const DateFilter = () => { + const { + close, + currentDate, + date, + isOpen, + onDateChange, + onNextClick, + onPreviousClick, + open, + } = useDateFilter() + return ( + + + + + {date.day} + + {date.month} + {date.year} + + + + + + + ) +} diff --git a/src/features/DateFilter/styled.tsx b/src/features/DateFilter/styled.tsx index 8646cd0a..22a67329 100644 --- a/src/features/DateFilter/styled.tsx +++ b/src/features/DateFilter/styled.tsx @@ -1,3 +1,145 @@ import styled from 'styled-components/macro' -export const Wrapper = styled.div`` +type Props = { + active: boolean, +} + +export const Wrapper = styled.div` + position: relative; + height: 100%; + width: 100%; + display: flex; + box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.3); + + ${({ active }) => (active + ? ` + & ${Button}, ${DateButton} { + color: #fff; + background-color: #666666; + } + ` + : '' + )}; +` + +export const DateButton = styled.button` + border: none; + outline: none; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + text-align: left; + width: 192px; + height: 100%; + border-left: 1px solid #222222; + border-right: 1px solid #222222; + cursor: pointer; + background-color: #3F3F3F; + color: #999999; + + :hover { + background-color: #484848; + } + + :active { + color: #fff; + background-color: #666666; + } +` + +export const Day = styled.span` + font-weight: bold; + font-size: 38px; + letter-spacing: -0.03em; +` + +export const Month = styled.span` + font-weight: bold; + font-size: 14px; +` + +export const MonthYearWrapper = styled.div` + height: 28px; + margin-left: 4px; + display: flex; + flex-direction: column; + justify-content: space-between; +` + +export const Year = styled.span` + font-style: normal; + font-weight: 300; + font-size: 14px; +` + +const Arrow = styled.div` + width: 16px; + height: 2px; + margin: auto; + background-color: #222222; + + &::before { + content: ''; + position: absolute; + width: 10px; + height: 10px; + border-left: 2px solid #222222; + border-bottom: 2px solid #222222; + } +` + +export const ArrowLeft = styled(Arrow)` + &:before { + transform: rotate(45deg); + top: 19px; + left: 17px; + } +` + +export const ArrowRight = styled(Arrow)` + &:before { + transform: rotate(225deg); + top: 19px; + right: 17px; + } +` + +type ButtonProps = { + borderLeftRadius?: number, + borderRightRadius?: number, +} + +export const Button = styled.button` + position: relative; + outline: none; + border: none; + width: 48px; + height: 100%; + cursor: pointer; + background-color: #3F3F3F; + ${({ borderLeftRadius }) => ( + borderLeftRadius + ? ` + border-top-left-radius: ${borderLeftRadius}px; + border-bottom-left-radius: ${borderLeftRadius}px; + ` + : '' + )} + ${({ borderRightRadius }) => ( + borderRightRadius + ? ` + border-top-right-radius: ${borderRightRadius}px; + border-bottom-right-radius: ${borderRightRadius}px; + ` + : '' + )} + + :hover { + background-color: #484848; + } + + :active { + background-color: #666666; + } +` diff --git a/src/features/Menu/styled.tsx b/src/features/Menu/styled.tsx index 0a9fc0fa..b7f70873 100644 --- a/src/features/Menu/styled.tsx +++ b/src/features/Menu/styled.tsx @@ -7,7 +7,7 @@ export const Wrapper = styled.div` display: flex; justify-content: center; align-items: center; - width: 304px; + width: 320px; height: 48px; ` export const ToggleButton = styled.button` diff --git a/src/requests/getMatches.tsx b/src/requests/getMatches.tsx new file mode 100644 index 00000000..e82b89a6 --- /dev/null +++ b/src/requests/getMatches.tsx @@ -0,0 +1,56 @@ +import { DATA_URL, PROCEDURES } from 'config' +import { callApi, getResponseData } from 'helpers' + +const proc = PROCEDURES.get_matches + +export type Item = { + id: number, + matches: Array, + name_eng: string, + name_rus: string, + sport: number, +} + +export type Match = { + date: string, + id: number, + round_id: number, + stream_status: number, + team1: Team, + team2: Team, +} + +export type Team = { + id: number, + name_eng: string, + name_rus: string, + score: number, +} + +type Args = { + date: string, + sportType: number, + tournamentId: number, +} + +export const getMatches = ({ + date, + sportType, + tournamentId, +}: Args): Promise> => { + const config = { + body: { + params: { + _p_date: date, + _p_sport: sportType, + _p_tournament_id: tournamentId, + }, + proc, + }, + } + + return callApi({ + config, + url: DATA_URL, + }).then(getResponseData(proc)) +} diff --git a/src/requests/index.tsx b/src/requests/index.tsx index 528c126b..762d8112 100644 --- a/src/requests/index.tsx +++ b/src/requests/index.tsx @@ -5,3 +5,4 @@ export * from './getCountries' export * from './getCountryCities' export * from './getLexics' export * from './getSearchItems' +export * from './getMatches'