Initial commit
@ -0,0 +1,190 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
`airbnb`,
|
||||
`airbnb/hooks`,
|
||||
`plugin:promise/recommended`,
|
||||
`plugin:sonarjs/recommended`,
|
||||
`plugin:prettier/recommended`,
|
||||
],
|
||||
plugins: [`optimize-regex`, `simple-import-sort`, `styled-components-varname`],
|
||||
ignorePatterns: [`**/dist/**/*.*`, `src/page/Game/*`, `vite.config.ts`, `dev-dist`],
|
||||
rules: {
|
||||
'react/prop-types': `off`,
|
||||
'sonarjs/prefer-single-boolean-return': `off`,
|
||||
'import/no-default-export': `off`,
|
||||
'no-param-reassign': `off`,
|
||||
'react/react-in-jsx-scope': `off`,
|
||||
'import/prefer-default-export': [`off`],
|
||||
'@typescript-eslint/no-unsafe-member-access': `off`,
|
||||
'@typescript-eslint/no-unsafe-assignment': `off`,
|
||||
'import/no-extraneous-dependencies': [`error`, { devDependencies: true }],
|
||||
'react/function-component-definition': [
|
||||
`error`,
|
||||
{
|
||||
namedComponents: `arrow-function`,
|
||||
},
|
||||
],
|
||||
'react/jsx-props-no-spreading': [`off`],
|
||||
'react/require-default-props': [`off`],
|
||||
'no-restricted-syntax': [`error`, `ForInStatement`, `LabeledStatement`, `WithStatement`],
|
||||
'no-plusplus': [`off`],
|
||||
'no-void': [
|
||||
`error`,
|
||||
{
|
||||
allowAsStatement: true,
|
||||
},
|
||||
],
|
||||
'func-names': [`warn`, `as-needed`, { generators: `never` }],
|
||||
curly: [`error`, `all`],
|
||||
'@typescript-eslint/no-use-before-define': [`error`, { functions: false, classes: false }],
|
||||
'simple-import-sort/imports': [
|
||||
`error`,
|
||||
{
|
||||
groups: [[`^react`, `^@?\\w`]],
|
||||
},
|
||||
],
|
||||
'optimize-regex/optimize-regex': `warn`,
|
||||
'@typescript-eslint/no-magic-numbers': [
|
||||
`warn`,
|
||||
{
|
||||
ignoreNumericLiteralTypes: true,
|
||||
ignoreEnums: true,
|
||||
enforceConst: true,
|
||||
ignoreReadonlyClassProperties: true,
|
||||
ignore: [-1, 0, 1],
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/quotes': [`error`, `backtick`],
|
||||
'newline-before-return': `error`,
|
||||
'react/self-closing-comp': [
|
||||
`error`,
|
||||
{
|
||||
component: true,
|
||||
html: true,
|
||||
},
|
||||
],
|
||||
'prettier/prettier': [
|
||||
`error`,
|
||||
{
|
||||
printWidth: 120,
|
||||
tabWidth: 4,
|
||||
useTabs: true,
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: `all`,
|
||||
bracketSpacing: true,
|
||||
jsxBracketSameLine: false,
|
||||
arrowParens: `avoid`,
|
||||
},
|
||||
{ usePrettierrc: false },
|
||||
],
|
||||
'styled-components-varname/varname': [
|
||||
`error`,
|
||||
{
|
||||
tagStyle: {
|
||||
suffix: `Styled`,
|
||||
},
|
||||
},
|
||||
],
|
||||
'import/no-extraneous-dependencies': [
|
||||
`error`,
|
||||
{
|
||||
devDependencies: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: [`*.ts`, `*.tsx`],
|
||||
extends: [
|
||||
`airbnb-typescript`,
|
||||
`plugin:@typescript-eslint/recommended`,
|
||||
`plugin:@typescript-eslint/recommended-requiring-type-checking`,
|
||||
`plugin:prettier/recommended`,
|
||||
],
|
||||
rules: {
|
||||
'@typescript-eslint/no-misused-promises': [
|
||||
`error`,
|
||||
{
|
||||
checksVoidReturn: {
|
||||
attributes: false,
|
||||
returns: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/explicit-function-return-type': [
|
||||
`error`,
|
||||
{
|
||||
allowExpressions: true,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-floating-promises': [
|
||||
`error`,
|
||||
{
|
||||
ignoreIIFE: true,
|
||||
ignoreVoid: true,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-unnecessary-condition': [`error`],
|
||||
'import/no-extraneous-dependencies': [
|
||||
`error`,
|
||||
{
|
||||
devDependencies: true,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/quotes': [`error`, `backtick`],
|
||||
'@typescript-eslint/no-unused-vars': [`error`],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [`*.json`],
|
||||
parser: `@typescript-eslint/parser`,
|
||||
parserOptions: {
|
||||
sourceType: `module`,
|
||||
project: false,
|
||||
},
|
||||
rules: {
|
||||
'no-unused-expressions': `off`,
|
||||
'sonarjs/no-duplicate-string': `off`,
|
||||
quotes: `off`,
|
||||
'@typescript-eslint/quotes': `off`,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [`**/*.spec.ts`, `**/*.spec.tsx`, `**/dsl/**`],
|
||||
extends: [`plugin:jest/recommended`],
|
||||
rules: {
|
||||
'jest/no-focused-tests': `error`,
|
||||
'jest/no-identical-title': `error`,
|
||||
|
||||
'no-shadow': `off`,
|
||||
'max-classes-per-file': `off`,
|
||||
|
||||
'@typescript-eslint/no-magic-numbers': `off`,
|
||||
'@typescript-eslint/indent': `off`,
|
||||
'@typescript-eslint/explicit-function-return-type': `off`,
|
||||
'@typescript-eslint/member-ordering': `off`,
|
||||
'@typescript-eslint/no-non-null-assertion': `off`,
|
||||
'@typescript-eslint/no-empty-function': `off`,
|
||||
|
||||
'sonarjs/no-duplicate-string': `off`,
|
||||
'sonarjs/no-identical-functions': `off`,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [`*.js`],
|
||||
rules: {
|
||||
'@typescript-eslint/no-var-requires': `off`,
|
||||
},
|
||||
},
|
||||
],
|
||||
parserOptions: {
|
||||
project: `tsconfig.json`,
|
||||
sourceType: `module`,
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: `detect`,
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
build
|
||||
@ -0,0 +1,9 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"useTabs": true,
|
||||
"tabWidth": 4,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
{
|
||||
"name": "ridebus-lite",
|
||||
"author": "Xtimms",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"homepage": "https://ridebus.by",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint --ext .ts,.tsx .",
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx ."
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@maptiler/sdk": "^1.0.9",
|
||||
"@reduxjs/toolkit": "^1.9.1",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/leaflet": "^1.9.3",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/react": "^18.0.27",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@types/styled-components": "^5.1.26",
|
||||
"@types/three": "^0.150.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.2",
|
||||
"@typescript-eslint/parser": "^5.48.2",
|
||||
"@vitejs/plugin-react": "^3.0.1",
|
||||
"eslint": "^8.32.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-better-styled-components": "latest",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jest": "^27.2.1",
|
||||
"eslint-plugin-json": "^3.1.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-optimize-regex": "^1.2.1",
|
||||
"eslint-plugin-prefer-arrow-functions": "^3.1.4",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-react": "^7.32.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-simple-import-sort": "^9.0.0",
|
||||
"eslint-plugin-sonarjs": "^0.18.0",
|
||||
"eslint-plugin-styled-components-varname": "^1.0.1",
|
||||
"gh-pages": "^4.0.0",
|
||||
"i18next": "^22.4.9",
|
||||
"leaflet": "^1.9.3",
|
||||
"prettier": "^2.8.3",
|
||||
"query-string": "^8.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-ga": "^3.3.1",
|
||||
"react-i18next": "^12.1.4",
|
||||
"react-inlinesvg": "^3.0.1",
|
||||
"react-qr-code": "^2.0.11",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-router-dom": "^6.7.0",
|
||||
"react-select": "^5.7.0",
|
||||
"react-spring-bottom-sheet": "^3.4.1",
|
||||
"react-yandex-metrika": "^2.6.0",
|
||||
"rollup-plugin-visualizer": "^5.9.0",
|
||||
"styled-components": "^6.0.4",
|
||||
"three": "^0.151.3",
|
||||
"typescript": "^5.1.6",
|
||||
"vite": "^4.4.4",
|
||||
"vite-plugin-checker": "^0.5.4",
|
||||
"vite-plugin-mkcert": "^1.11.0",
|
||||
"vite-plugin-pwa": "^0.14.7",
|
||||
"vite-tsconfig-paths": "^4.0.5"
|
||||
},
|
||||
"packageManager": "yarn@3.4.1"
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Single Page Apps for GitHub Pages</title>
|
||||
<script type="text/javascript">
|
||||
// Single Page Apps for GitHub Pages
|
||||
// MIT License
|
||||
// https://github.com/rafgraph/spa-github-pages
|
||||
// This script takes the current url and converts the path and query
|
||||
// string into just a query string, and then redirects the browser
|
||||
// to the new url with only a query string and hash fragment,
|
||||
// e.g. https://www.foo.tld/one/two?a=b&c=d#qwe, becomes
|
||||
// https://www.foo.tld/?/one/two&a=b~and~c=d#qwe
|
||||
// Note: this 404.html file must be at least 512 bytes for it to work
|
||||
// with Internet Explorer (it is currently > 512 bytes)
|
||||
|
||||
// If you're creating a Project Pages site and NOT using a custom domain,
|
||||
// then set pathSegmentsToKeep to 1 (enterprise users may need to set it to > 1).
|
||||
// This way the code will only replace the route part of the path, and not
|
||||
// the real directory in which the app resides, for example:
|
||||
// https://username.github.io/repo-name/one/two?a=b&c=d#qwe becomes
|
||||
// https://username.github.io/repo-name/?/one/two&a=b~and~c=d#qwe
|
||||
// Otherwise, leave pathSegmentsToKeep as 0.
|
||||
var pathSegmentsToKeep = 0;
|
||||
|
||||
var l = window.location;
|
||||
l.replace(
|
||||
l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '') +
|
||||
l.pathname.split('/').slice(0, 1 + pathSegmentsToKeep).join('/') + '/?/' +
|
||||
l.pathname.slice(1).split('/').slice(pathSegmentsToKeep).join('/').replace(/&/g, '~and~') +
|
||||
(l.search ? '&' + l.search.slice(1).replace(/&/g, '~and~') : '') +
|
||||
l.hash
|
||||
);
|
||||
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://pwa.ridebus.by</loc>
|
||||
<lastmod>2024-05-26</lastmod>
|
||||
</url>
|
||||
</urlset>
|
||||
@ -0,0 +1,43 @@
|
||||
import React from 'react'
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom'
|
||||
import { Dev } from 'page/Dev/Dev'
|
||||
import { Home } from 'page/Home'
|
||||
import { configureI18next, initGA, YM } from 'shared/lib'
|
||||
|
||||
import 'react-spring-bottom-sheet/dist/style.css'
|
||||
import 'shared/theme/styles/index.css'
|
||||
|
||||
const Game = React.lazy(() => import(`page/Game/Game`))
|
||||
const Intro = React.lazy(() => import(`page/Game/Intro/Intro`))
|
||||
|
||||
initGA()
|
||||
|
||||
configureI18next()
|
||||
|
||||
export const Root: React.FC = () => (
|
||||
<>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/dev" element={<Dev />} />
|
||||
<Route
|
||||
path="/game"
|
||||
element={
|
||||
<React.Suspense fallback={<>...</>}>
|
||||
<Intro />
|
||||
</React.Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/game/doubles"
|
||||
element={
|
||||
<React.Suspense fallback={<>...</>}>
|
||||
<Game />
|
||||
</React.Suspense>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
<YM />
|
||||
</>
|
||||
)
|
||||
@ -0,0 +1 @@
|
||||
if('serviceWorker' in navigator) navigator.serviceWorker.register('/dev-sw.js?dev-sw', { scope: '/', type: 'classic' })
|
||||
@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { Provider } from 'react-redux'
|
||||
import { store } from 'shared/store/app/configureStore'
|
||||
|
||||
import { Root } from './App'
|
||||
|
||||
const container = document.getElementById(`root`)
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const root = createRoot(container!)
|
||||
|
||||
root.render(
|
||||
<Provider store={store}>
|
||||
<Root />
|
||||
</Provider>,
|
||||
)
|
||||
@ -0,0 +1 @@
|
||||
export * from './Holiday'
|
||||
@ -0,0 +1,20 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const HolidayContainerStyled = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
margin: 12px 0 0 20px;
|
||||
color: #454545;
|
||||
font-weight: 600;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: -17px;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
background: linear-gradient(180deg, #38347a 0%, #336cff 100%);
|
||||
border-radius: 50%;
|
||||
}
|
||||
`
|
||||
|
After Width: | Height: | Size: 12 KiB |
@ -0,0 +1,2 @@
|
||||
export * from './ui/Logo'
|
||||
export * from './ui/styled'
|
||||
@ -0,0 +1,15 @@
|
||||
import SVG from 'react-inlinesvg'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ImageWrapperStyled } from 'shared/ui/ImageWrapper'
|
||||
|
||||
import LogoWithText from '../img/LogoWithText.svg'
|
||||
|
||||
export const Logo: React.FC = () => {
|
||||
return (
|
||||
<Link to="/">
|
||||
<ImageWrapperStyled $w={140} $h={27}>
|
||||
<SVG src={LogoWithText} width={140} height={27} uniquifyIDs />
|
||||
</ImageWrapperStyled>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const HeaderInnerStyled = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
padding: 11px 15px;
|
||||
max-width: 768px;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
export const HeaderContainerStyled = styled.nav`
|
||||
z-index: 2;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
export const HeaderContainerBetaStyled = styled.nav`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
export const HeaderActionsStyled = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
`
|
||||
@ -0,0 +1,13 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const TextContainerStyled = styled.p`
|
||||
opacity: 0.5;
|
||||
font-size: 18px;
|
||||
`
|
||||
|
||||
export const SelectBusStopText: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return <TextContainerStyled>{t(`Select bus stop to see schedule`)}</TextContainerStyled>
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from './SelectBusStopText'
|
||||
@ -0,0 +1,27 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import SVG from 'react-inlinesvg'
|
||||
import { TelegramButton } from 'shared/ui'
|
||||
import { CardHeaderStyled, CardStyled } from 'shared/ui/common'
|
||||
|
||||
import Write from './img/write.svg'
|
||||
import { HandWrapperStyled } from './styled'
|
||||
|
||||
export const WriteMe: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<CardStyled $isOverflow>
|
||||
<CardHeaderStyled>
|
||||
{t(`Did you see an error?`)}
|
||||
<br />
|
||||
{t(`Have a suggestion for improvement?`)}
|
||||
</CardHeaderStyled>
|
||||
|
||||
<TelegramButton />
|
||||
|
||||
<HandWrapperStyled>
|
||||
<SVG src={Write} width={109} height={100} uniquifyIDs />
|
||||
</HandWrapperStyled>
|
||||
</CardStyled>
|
||||
)
|
||||
}
|
||||
|
After Width: | Height: | Size: 26 KiB |
@ -0,0 +1 @@
|
||||
export * from './WriteMe'
|
||||
@ -0,0 +1,7 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const HandWrapperStyled = styled.div`
|
||||
position: absolute;
|
||||
right: -10px;
|
||||
bottom: 0;
|
||||
`
|
||||
@ -0,0 +1,83 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { BottomSheet, BottomSheetRef } from 'react-spring-bottom-sheet'
|
||||
|
||||
import { getSnapPoints, getSnapValue, snapBottom, snapMid, snapTop } from './helpers/getSnapPoints'
|
||||
import {
|
||||
bottomSheetPositionSelector,
|
||||
BottomSheetStates,
|
||||
maxHeightSelector,
|
||||
setBottomSheetPosition,
|
||||
setMaxHeight,
|
||||
} from './model/bottomSheetSlice'
|
||||
|
||||
interface IProps {
|
||||
children: JSX.Element
|
||||
header?: JSX.Element
|
||||
}
|
||||
export const BottomSheetCustom: React.FC<IProps> = ({ children, header }) => {
|
||||
const sheetRef = useRef<BottomSheetRef>(null)
|
||||
const [expandOnContentDrag] = useState<boolean>(true)
|
||||
const focusRef = useRef<HTMLButtonElement>(null)
|
||||
const dispatch = useDispatch()
|
||||
const bottomSheetMaxHeight = useSelector(maxHeightSelector)
|
||||
|
||||
const bottomSheetPostion = useSelector(bottomSheetPositionSelector)
|
||||
|
||||
useEffect(() => {
|
||||
if (sheetRef.current) {
|
||||
sheetRef.current.snapTo(({ maxHeight }) => getSnapValue(bottomSheetPostion, maxHeight))
|
||||
}
|
||||
}, [bottomSheetPostion])
|
||||
|
||||
const getDefaultSnap = ({ maxHeight }: { maxHeight: number }): number => {
|
||||
if (maxHeight !== bottomSheetMaxHeight) {
|
||||
dispatch(setMaxHeight(maxHeight))
|
||||
}
|
||||
|
||||
return snapMid(maxHeight)
|
||||
}
|
||||
|
||||
const handleSpringEnd = (): void => {
|
||||
const currentMaxHeight = sheetRef.current?.height
|
||||
|
||||
if (bottomSheetMaxHeight === undefined || !currentMaxHeight) return
|
||||
|
||||
if (
|
||||
currentMaxHeight >= Math.floor(snapBottom(bottomSheetMaxHeight)) &&
|
||||
currentMaxHeight < Math.floor(snapMid(bottomSheetMaxHeight))
|
||||
) {
|
||||
dispatch(setBottomSheetPosition(BottomSheetStates.BOTTOM))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
currentMaxHeight >= Math.floor(snapMid(bottomSheetMaxHeight)) &&
|
||||
currentMaxHeight < Math.floor(snapTop(bottomSheetMaxHeight))
|
||||
) {
|
||||
dispatch(setBottomSheetPosition(BottomSheetStates.MID))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
dispatch(setBottomSheetPosition(BottomSheetStates.TOP))
|
||||
}
|
||||
|
||||
return (
|
||||
<BottomSheet
|
||||
open
|
||||
skipInitialTransition
|
||||
ref={sheetRef}
|
||||
initialFocusRef={focusRef}
|
||||
defaultSnap={getDefaultSnap}
|
||||
snapPoints={getSnapPoints}
|
||||
expandOnContentDrag={expandOnContentDrag}
|
||||
blocking={false}
|
||||
onSpringEnd={handleSpringEnd}
|
||||
header={header}
|
||||
>
|
||||
{children}
|
||||
</BottomSheet>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
import { BottomSheetStates } from '../model/bottomSheetSlice'
|
||||
|
||||
export const snapTop = (maxHeight: number): number => maxHeight - maxHeight / 5
|
||||
export const snapMid = (maxHeight: number): number => maxHeight / 2
|
||||
export const snapBottom = (maxHeight: number): number => maxHeight * 0.2
|
||||
|
||||
export const getSnapPoints = ({ maxHeight }: { maxHeight: number }): number[] => {
|
||||
return [snapTop(maxHeight), snapMid(maxHeight), snapBottom(maxHeight)]
|
||||
}
|
||||
|
||||
export const getSnapValue = (snap: BottomSheetStates, maxHeight: number): number => {
|
||||
switch (snap) {
|
||||
case BottomSheetStates.TOP:
|
||||
return snapTop(maxHeight)
|
||||
case BottomSheetStates.MID:
|
||||
return snapMid(maxHeight)
|
||||
case BottomSheetStates.BOTTOM:
|
||||
return snapBottom(maxHeight)
|
||||
default:
|
||||
return snapMid(maxHeight)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
import { RootState } from 'shared/store/app/configureStore'
|
||||
|
||||
export enum BottomSheetStates {
|
||||
'TOP' = 1, // 90% open
|
||||
'MID' = 2, // 50/50
|
||||
'BOTTOM' = 3, // 30% open
|
||||
}
|
||||
export interface BottomSheetState {
|
||||
position: BottomSheetStates
|
||||
maxHeight: number | undefined
|
||||
}
|
||||
|
||||
const initialState: BottomSheetState = {
|
||||
position: BottomSheetStates.MID,
|
||||
maxHeight: undefined,
|
||||
}
|
||||
|
||||
export const bottomSheetSlice = createSlice({
|
||||
name: `bottomSheetSlice`,
|
||||
initialState,
|
||||
reducers: {
|
||||
setBottomSheetPosition: (state, action: PayloadAction<BottomSheetStates>) => {
|
||||
state.position = action.payload
|
||||
},
|
||||
setMaxHeight: (state, action: PayloadAction<number | undefined>) => {
|
||||
state.maxHeight = action.payload
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Action creators are generated for each case reducer function
|
||||
export const { setBottomSheetPosition, setMaxHeight } = bottomSheetSlice.actions
|
||||
|
||||
export const bottomSheetPositionSelector = (state: RootState): BottomSheetStates => state.bottomSheetSlice.position
|
||||
export const maxHeightSelector = (state: RootState): number | undefined => state.bottomSheetSlice.maxHeight
|
||||
|
||||
export default bottomSheetSlice.reducer
|
||||
@ -0,0 +1,54 @@
|
||||
import { calculateHowMuchIsLeft } from 'shared/lib/time/calculateHowMuchIsLeft'
|
||||
import { Directions } from 'shared/store/busStop/Stops'
|
||||
|
||||
import { ComplainType } from '../model/Complains'
|
||||
|
||||
export const getMinutesString = (amount: number, secondWord = `назад`): string => {
|
||||
if (amount === 0) return ` сейчас`
|
||||
if (amount === 1) return ` минуту ${secondWord}`
|
||||
if ([2, 3, 4].includes(amount)) return ` минуты ${secondWord}`
|
||||
|
||||
return ` минут ${secondWord}`
|
||||
}
|
||||
|
||||
export const getDirectionString = (direction: Directions): string => {
|
||||
return direction === `in` ? `в парк` : `из парка`
|
||||
}
|
||||
|
||||
export const getTypeString = (type: ComplainType): string => {
|
||||
switch (type) {
|
||||
case ComplainType.earlier:
|
||||
return `приехал раньше`
|
||||
case ComplainType.later:
|
||||
return `приехал позже`
|
||||
case ComplainType.not_arrive:
|
||||
return `не приехал`
|
||||
case ComplainType.passed_by:
|
||||
return `проехал мимо`
|
||||
default:
|
||||
return ``
|
||||
}
|
||||
}
|
||||
|
||||
export const getOnString = (on: number, type: ComplainType): string => {
|
||||
if (on === 0) return getTypeString(type)
|
||||
|
||||
const commingType = getTypeString(type)
|
||||
const minutesString = getMinutesString(on, ``)
|
||||
|
||||
return `${commingType} на ${on}${minutesString}`
|
||||
}
|
||||
|
||||
export const getHumanDate = (date: string): string => {
|
||||
const time = calculateHowMuchIsLeft(date)
|
||||
const d = new Date(date)
|
||||
const min = d.getMinutes() < 9 ? `0${d.getMinutes()}` : d.getMinutes()
|
||||
if (time.hours === null || time.hours >= 1) return `в ${d.getHours()}:${min}`
|
||||
|
||||
if (time.minutes === null) return ``
|
||||
|
||||
const timeAmountString = time.minutes === 0 ? `` : time.minutes
|
||||
const minutesString = getMinutesString(time.minutes)
|
||||
|
||||
return `${timeAmountString}${minutesString}`
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export * from './ui/Complains'
|
||||
export * from './model/Complains'
|
||||
@ -0,0 +1,6 @@
|
||||
export enum ComplainType {
|
||||
'earlier' = `earlier`,
|
||||
'later' = `later`,
|
||||
not_arrive = `not_arrive`,
|
||||
passed_by = `passed_by`,
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
import React, { createContext, useContext, useMemo } from 'react'
|
||||
|
||||
import { IComplains, IComplainsResponse, useComplains } from './useComplains'
|
||||
|
||||
const DEFAULT_PROPS = {
|
||||
complains: [],
|
||||
addComplain: () => {},
|
||||
}
|
||||
|
||||
export const ComplainsContext = createContext<ContextProps>(DEFAULT_PROPS)
|
||||
|
||||
interface ContextProps {
|
||||
complains: IComplainsResponse[]
|
||||
addComplain: (data: IComplains) => void
|
||||
}
|
||||
|
||||
interface IProviderProps {
|
||||
children: React.ReactElement
|
||||
}
|
||||
export const ComplainsProvider = ({ children }: IProviderProps): JSX.Element => {
|
||||
const { complains, addComplain } = useComplains()
|
||||
|
||||
const values = useMemo(() => ({ complains, addComplain }), [addComplain, complains])
|
||||
|
||||
return <ComplainsContext.Provider value={values}>{children}</ComplainsContext.Provider>
|
||||
}
|
||||
|
||||
export const useComplainsContext = (): ContextProps => {
|
||||
return useContext(ComplainsContext)
|
||||
}
|
||||
@ -0,0 +1,100 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { AndrewLytics } from 'shared/lib'
|
||||
import { Directions, StopKeys } from 'shared/store/busStop/Stops'
|
||||
|
||||
import { ComplainType } from './Complains'
|
||||
|
||||
export interface IComplains {
|
||||
stop: StopKeys
|
||||
direction: Directions
|
||||
date: string
|
||||
type: ComplainType
|
||||
on: number
|
||||
}
|
||||
|
||||
export interface IComplainsResponse extends IComplains {
|
||||
id: number
|
||||
}
|
||||
interface IReturns {
|
||||
complains: IComplainsResponse[]
|
||||
addComplain: (data: IComplains) => void
|
||||
}
|
||||
|
||||
export const useComplains = (): IReturns => {
|
||||
const [complains, setComplains] = useState<IComplainsResponse[]>([])
|
||||
|
||||
const fetchComplains = (): void => {
|
||||
fetch(`https://popooga.ru/graphql`, {
|
||||
headers: {
|
||||
accept: `*/*`,
|
||||
'accept-language': `ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7`,
|
||||
'content-type': `application/json`,
|
||||
'sec-fetch-dest': `empty`,
|
||||
'sec-fetch-mode': `cors`,
|
||||
'sec-fetch-site': `same-origin`,
|
||||
},
|
||||
referrer: `https://popooga.ru/graphql`,
|
||||
referrerPolicy: `strict-origin-when-cross-origin`,
|
||||
body: `{"operationName":"C","variables":{},"query":"query C {\\n findComplains {\\n id\\n stop\\n direction\\n date\\n type\\n on\\n }\\n}\\n"}`,
|
||||
method: `POST`,
|
||||
mode: `cors`,
|
||||
credentials: `omit`,
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (res?.data?.findComplains) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
|
||||
setComplains(res.data.findComplains)
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchComplains()
|
||||
const interval = setInterval(() => {
|
||||
fetchComplains()
|
||||
}, 5000)
|
||||
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const addComplain = (data: IComplains): void => {
|
||||
AndrewLytics(`addComplainMethod`)
|
||||
const body = {
|
||||
operationName: null,
|
||||
variables: {
|
||||
data,
|
||||
},
|
||||
query: `mutation Complain($data: ComplainsInputDTO!) {createComplain(data: $data) {id}
|
||||
}`,
|
||||
}
|
||||
|
||||
fetch(`https://popooga.ru/graphql`, {
|
||||
headers: {
|
||||
accept: `*/*`,
|
||||
'accept-language': `ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7`,
|
||||
'content-type': `application/json`,
|
||||
'sec-ch-ua': `" Not A;Brand";v="99", "Chromium";v="101", "Google Chrome";v="101"`,
|
||||
'sec-ch-ua-mobile': `?0`,
|
||||
'sec-ch-ua-platform': `"macOS"`,
|
||||
'sec-fetch-dest': `empty`,
|
||||
'sec-fetch-mode': `cors`,
|
||||
'sec-fetch-site': `same-origin`,
|
||||
},
|
||||
referrer: `https://popooga.ru/graphql`,
|
||||
referrerPolicy: `strict-origin-when-cross-origin`,
|
||||
body: JSON.stringify(body),
|
||||
method: `POST`,
|
||||
mode: `cors`,
|
||||
credentials: `omit`,
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
return { complains, addComplain }
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { AndrewLytics } from 'shared/lib'
|
||||
import { calculateHowMuchIsLeft } from 'shared/lib/time/calculateHowMuchIsLeft'
|
||||
import { leftSelector } from 'shared/store/timeLeft/timeLeftSlice'
|
||||
|
||||
const isToday = (date: Date): boolean => {
|
||||
const today = new Date()
|
||||
|
||||
return (
|
||||
today.getFullYear() === date.getFullYear() &&
|
||||
today.getMonth() === date.getMonth() &&
|
||||
today.getDate() === date.getDate()
|
||||
)
|
||||
}
|
||||
|
||||
const getTodayFirstVisit = (): string => {
|
||||
const item = localStorage.getItem(`TODAY_FIRST_VISIT`)
|
||||
|
||||
if (!item || !isToday(new Date(item))) {
|
||||
const today = new Date().toISOString()
|
||||
localStorage.setItem(`TODAY_FIRST_VISIT`, today)
|
||||
|
||||
return today
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
export const VISIT_TIME = getTodayFirstVisit()
|
||||
|
||||
interface IReturns {
|
||||
shouldShowFastReply: boolean
|
||||
}
|
||||
|
||||
export const useFastReplay = (): IReturns => {
|
||||
const left = useSelector(leftSelector)
|
||||
const [shouldShowFastReply, setShouldShowFastReply] = useState<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (left.hours === null) return
|
||||
|
||||
const userTimeLeft = calculateHowMuchIsLeft(VISIT_TIME)
|
||||
|
||||
if (userTimeLeft.minutes === null || (userTimeLeft.hours === 0 && userTimeLeft.minutes <= 0)) return
|
||||
|
||||
AndrewLytics(`frappears`)
|
||||
|
||||
setShouldShowFastReply(true)
|
||||
}, [left, shouldShowFastReply])
|
||||
|
||||
return {
|
||||
shouldShowFastReply,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
import React, { useMemo } from 'react'
|
||||
|
||||
import { getDirectionString, getHumanDate, getOnString } from '../helpers'
|
||||
import { IComplainsResponse } from '../model/useComplains'
|
||||
import { ComplainsDirectionStyled, ComplainsStopStyled, MessageContainerStyled, MessageDateStyled } from './styled'
|
||||
|
||||
type Props = IComplainsResponse & { isCurrentStop?: boolean }
|
||||
|
||||
export const ComplainsMessage: React.FC<Props> = ({ date, on, type, direction, stop, isCurrentStop = false }) => {
|
||||
const left = useMemo(() => getHumanDate(date), [date])
|
||||
const directionString = getDirectionString(direction)
|
||||
const onString = getOnString(on, type)
|
||||
|
||||
return (
|
||||
<MessageContainerStyled>
|
||||
<ComplainsStopStyled $isCurrentStop={isCurrentStop}>{stop}</ComplainsStopStyled>
|
||||
<ComplainsDirectionStyled>{directionString}</ComplainsDirectionStyled>
|
||||
<p>{onString}</p>
|
||||
<MessageDateStyled>{left}</MessageDateStyled>
|
||||
</MessageContainerStyled>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const ComplainsContainerStyled = styled.div`
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
export const MessageContainerStyled = styled.article`
|
||||
background: #e7edec;
|
||||
border-radius: 14px;
|
||||
padding: 11px 16px;
|
||||
text-align: left;
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
grid-template-rows: 20px 20px;
|
||||
grid-row-gap: 5px;
|
||||
|
||||
& + & {
|
||||
margin-top: 12px;
|
||||
}
|
||||
`
|
||||
|
||||
export const MessageDateStyled = styled.span`
|
||||
color: #737776;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-end;
|
||||
`
|
||||
|
||||
export const ComplainsStopStyled = styled.p<{ $isCurrentStop?: boolean }>`
|
||||
color: ${props => (props.$isCurrentStop ? `#1191FB` : `black`)};
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
export const ComplainsDirectionStyled = styled.p`
|
||||
text-align: right;
|
||||
`
|
||||
|
||||
export const ComplainsLabelStyled = styled.span`
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #a5a5a5;
|
||||
`
|
||||
|
||||
export const ComplainsBlockContainerStyled = styled.div`
|
||||
padding-left: 6px;
|
||||
`
|
||||
|
||||
export const ComplainsBlockTextStyled = styled.p`
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
`
|
||||
|
||||
export const ComplainCountStyled = styled.span`
|
||||
color: #717171;
|
||||
`
|
||||
|
||||
export const InfoTextStyled = styled.p`
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
color: #808080;
|
||||
margin-bottom: 12px;
|
||||
padding: 0 3px;
|
||||
`
|
||||
|
After Width: | Height: | Size: 46 KiB |
@ -0,0 +1 @@
|
||||
export * from './ui/DirectionChanger'
|
||||
@ -0,0 +1,44 @@
|
||||
import { MAIN_GREY } from 'shared/theme'
|
||||
import { ImageWrapperStyled } from 'shared/ui'
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const GoButtonContainerStyled = styled.div`
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
`
|
||||
|
||||
export const DirectionTextStyled = styled.p`
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
`
|
||||
|
||||
export const DirectionContainerStyled = styled.div`
|
||||
padding-left: 6px;
|
||||
`
|
||||
export const DirectionPlaceholderStyled = styled.span`
|
||||
font-size: 16px;
|
||||
margin-bottom: 2px;
|
||||
`
|
||||
export const GoButtonStyled = styled.button<{ $active?: boolean }>`
|
||||
width: 100%;
|
||||
border-radius: 13px;
|
||||
background-color: ${MAIN_GREY};
|
||||
color: #000000;
|
||||
padding: 17px 17px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
|
||||
@media (hover: hover) {
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const WebWrapper = styled(ImageWrapperStyled)`
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: -10px;
|
||||
`
|
||||
@ -0,0 +1,44 @@
|
||||
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
||||
/* eslint-disable no-console */
|
||||
/* eslint-disable promise/always-return */
|
||||
/* eslint-disable func-names */
|
||||
/* eslint-disable promise/catch-or-return */
|
||||
export const fallbackCopyTextToClipboard = (text: string): void => {
|
||||
const textArea = document.createElement(`textarea`)
|
||||
textArea.value = text
|
||||
|
||||
// Avoid scrolling to bottom
|
||||
textArea.style.top = `0`
|
||||
textArea.style.left = `0`
|
||||
textArea.style.position = `fixed`
|
||||
|
||||
document.body.appendChild(textArea)
|
||||
textArea.focus()
|
||||
textArea.select()
|
||||
|
||||
try {
|
||||
const successful = document.execCommand(`copy`)
|
||||
const msg = successful ? `successful` : `unsuccessful`
|
||||
console.log(`Fallback: Copying text command was ${msg}`)
|
||||
} catch (err) {
|
||||
console.error(`Fallback: Oops, unable to copy`, err)
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea)
|
||||
}
|
||||
|
||||
export const copyTextToClipboard = (text: string): void => {
|
||||
if (!navigator.clipboard) {
|
||||
fallbackCopyTextToClipboard(text)
|
||||
|
||||
return
|
||||
}
|
||||
navigator.clipboard.writeText(text).then(
|
||||
function () {
|
||||
console.log(`Async: Copying to clipboard was successful!`)
|
||||
},
|
||||
function (err) {
|
||||
console.error(`Async: Could not copy text: `, err)
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.23034 0.213989H2.81344C1.63555 0.213989 0.637641 0.992216 0.318594 2.05589C0.285599 2.16592 0.259844 2.279 0.241937 2.39459C0.222201 2.52215 0.211914 2.65276 0.211914 2.78573V5.26C0.211914 6.67876 1.37929 7.8325 2.81344 7.8325L4.02189 7.83273L5.23034 7.83243C6.66442 7.83243 7.83187 6.67869 7.83187 5.25993V2.78573C7.83187 1.36765 6.6645 0.213989 5.23034 0.213989ZM6.68888 5.26C6.68888 6.04821 6.03463 6.68951 5.23004 6.68951L4.02189 6.68974L2.81375 6.68943C2.00916 6.68943 1.35491 6.04814 1.35491 5.26V2.78573C1.35491 2.53953 1.41876 2.30773 1.53116 2.10519C1.63936 1.91035 1.79245 1.74278 1.97677 1.61591C2.21375 1.45277 2.50232 1.35698 2.81337 1.35698H5.23034C6.03463 1.35698 6.68888 1.9979 6.68888 2.78573V5.26Z" fill="black"/>
|
||||
<path d="M14.1854 0.213989H11.7685C10.5906 0.213989 9.59272 0.992216 9.27367 2.05589C9.24068 2.16592 9.21492 2.279 9.19701 2.39459C9.17728 2.52215 9.16699 2.65276 9.16699 2.78573V5.26C9.16699 6.67876 10.3344 7.8325 11.7685 7.8325L12.977 7.83273L14.1854 7.83243C15.6195 7.83243 16.787 6.67869 16.787 5.25993V2.78573C16.787 1.36765 15.6196 0.213989 14.1854 0.213989ZM15.644 5.26C15.644 6.04821 14.9897 6.68951 14.1851 6.68951L12.977 6.68974L11.7688 6.68943C10.9642 6.68943 10.31 6.04814 10.31 5.26V2.78573C10.31 2.53953 10.3738 2.30773 10.4862 2.10519C10.5944 1.91035 10.7475 1.74278 10.9319 1.61591C11.1688 1.45277 11.4574 1.35698 11.7684 1.35698H14.1854C14.9897 1.35698 15.644 1.9979 15.644 2.78573V5.26Z" fill="black"/>
|
||||
<path d="M5.23132 9.16687H2.81442C1.63653 9.16687 0.638618 9.9451 0.31957 11.0088C0.286576 11.1188 0.26082 11.2319 0.242913 11.3475C0.223178 11.475 0.212891 11.6056 0.212891 11.7386V14.2129C0.212891 15.6316 1.38027 16.7854 2.81442 16.7854L4.02287 16.7856L5.23132 16.7853C6.6654 16.7853 7.83285 15.6316 7.83285 14.2128V11.7386C7.83285 10.3205 6.66547 9.16687 5.23132 9.16687ZM6.68986 14.2129C6.68986 15.0011 6.03561 15.6424 5.23102 15.6424L4.02287 15.6426L2.81473 15.6423C2.01013 15.6423 1.35588 15.001 1.35588 14.2129V11.7386C1.35588 11.4924 1.41974 11.2606 1.53213 11.0581C1.64034 10.8632 1.79342 10.6957 1.97775 10.5688C2.21473 10.4056 2.5033 10.3099 2.81435 10.3099H5.23132C6.03561 10.3099 6.68986 10.9508 6.68986 11.7386V14.2129Z" fill="black"/>
|
||||
<path d="M10.4491 12.007C10.4491 12.007 10.4491 12.007 10.4491 12.007C10.0808 12.007 9.78125 11.7134 9.78125 11.3527V10.2203C9.78125 10.1077 9.81054 10.0017 9.86181 9.9089C9.91133 9.81962 9.98143 9.74289 10.0658 9.685C10.1743 9.61036 10.3065 9.56641 10.4488 9.56641H11.555C11.923 9.56641 12.2225 9.85972 12.2225 10.2203V11.3527C12.2225 11.7133 11.923 12.007 11.555 12.007H11.0019H10.4491C10.4491 12.007 10.4491 12.007 10.4491 12.007V12.007Z" fill="black"/>
|
||||
<path d="M10.4491 16.0584C10.0808 16.0584 9.78125 15.7647 9.78125 15.4041V14.2717C9.78125 14.1591 9.81054 14.0531 9.86181 13.9603C9.91133 13.871 9.98143 13.7943 10.0658 13.7364C10.1743 13.6618 10.3065 13.6178 10.4488 13.6178H11.555C11.923 13.6178 12.2225 13.9111 12.2225 14.2717V15.4041C12.2225 15.7647 11.923 16.0584 11.555 16.0584H11.0019H10.4491V16.0584Z" fill="black"/>
|
||||
<path d="M14.7128 12.0068C14.3445 12.0068 14.0449 11.7131 14.0449 11.3525V10.2201C14.0449 10.1074 14.0742 10.0014 14.1255 9.90865C14.175 9.81937 14.2451 9.74264 14.3295 9.68475C14.438 9.61012 14.5701 9.56616 14.7124 9.56616H15.8187C16.1866 9.56616 16.4862 9.85947 16.4862 10.2201V11.3525C16.4862 11.7131 16.1866 12.0067 15.8187 12.0067H15.2656H14.7128V12.0068Z" fill="black"/>
|
||||
<path d="M14.7128 16.0584C14.3445 16.0584 14.0449 15.7647 14.0449 15.4041V14.2717C14.0449 14.1591 14.0742 14.0531 14.1255 13.9603C14.175 13.871 14.2451 13.7943 14.3295 13.7364C14.438 13.6618 14.5701 13.6178 14.7124 13.6178H15.8187C16.1866 13.6178 16.4862 13.9111 16.4862 14.2717V15.4041C16.4862 15.7647 16.1866 16.0584 15.8187 16.0584H15.2656H14.7128V16.0584Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 96 960 960" width="48"><path d="M484 809q16 0 27-11t11-27q0-16-11-27t-27-11q-16 0-27 11t-11 27q0 16 11 27t27 11Zm-35-146h59q0-26 6.5-47.5T555 566q31-26 44-51t13-55q0-53-34.5-85T486 343q-49 0-86.5 24.5T345 435l53 20q11-28 33-43.5t52-15.5q34 0 55 18.5t21 47.5q0 22-13 41.5T508 544q-30 26-44.5 51.5T449 663Zm31 313q-82 0-155-31.5t-127.5-86Q143 804 111.5 731T80 576q0-83 31.5-156t86-127Q252 239 325 207.5T480 176q83 0 156 31.5T763 293q54 54 85.5 127T880 576q0 82-31.5 155T763 858.5q-54 54.5-127 86T480 976Zm0-60q142 0 241-99.5T820 576q0-142-99-241t-241-99q-141 0-240.5 99T140 576q0 141 99.5 240.5T480 916Zm0-340Z"/></svg>
|
||||
|
After Width: | Height: | Size: 680 B |
@ -0,0 +1 @@
|
||||
export * from './ui/FAQ'
|
||||
@ -0,0 +1,59 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const ShareContainerStyled = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
border-radius: 7px;
|
||||
padding: 6px;
|
||||
`
|
||||
|
||||
export const ShareTitleStyled = styled.h3`
|
||||
margin-top: 24px;
|
||||
`
|
||||
|
||||
export const CopyFieldStyled = styled.div<{ $clicked: boolean }>`
|
||||
padding: 13px 18px;
|
||||
color: ${props => (props.$clicked ? `green` : `#336cff`)};
|
||||
font-weight: 500;
|
||||
border-radius: 15px;
|
||||
background: #f2f4f4;
|
||||
text-align: left;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
export const QABlockStyled = styled.article`
|
||||
h2 {
|
||||
font-size: 19px;
|
||||
font-weight: 600;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 4px;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #336cff;
|
||||
|
||||
&:visited {
|
||||
color: #2859d5;
|
||||
}
|
||||
}
|
||||
|
||||
& + & {
|
||||
margin-top: 24px;
|
||||
}
|
||||
`
|
||||
export const ShareItemContainerStyled = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
export const ShareContentContainerStyled = styled.div`
|
||||
margin-top: 56px;
|
||||
`
|
||||
@ -0,0 +1,7 @@
|
||||
import { StopKeys } from 'shared/store/busStop/Stops'
|
||||
|
||||
export const getFavoriteBusStop = (): StopKeys[] => {
|
||||
const localStorageItem = localStorage.getItem(`favoriteStops`)
|
||||
|
||||
return localStorageItem ? (JSON.parse(localStorageItem) as StopKeys[]) : []
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
export * from './ui/FavoriteButton'
|
||||
export * from './ui/FavoriteStops'
|
||||
export * from './model/favoriteStopsSlice'
|
||||
@ -0,0 +1,31 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
import { RootState } from 'shared/store/app/configureStore'
|
||||
import { StopKeys } from 'shared/store/busStop/Stops'
|
||||
|
||||
import { getFavoriteBusStop } from '../helpers/getFavoriteBusStop'
|
||||
|
||||
export interface FavoriteStopsState {
|
||||
stops: StopKeys[]
|
||||
}
|
||||
|
||||
const initialState: FavoriteStopsState = {
|
||||
stops: getFavoriteBusStop(),
|
||||
}
|
||||
|
||||
export const favoriteStopsSlice = createSlice({
|
||||
name: `favoriteStopsSlice`,
|
||||
initialState,
|
||||
reducers: {
|
||||
saveFavoriteBusStops: (state, action: PayloadAction<StopKeys[]>) => {
|
||||
state.stops = action.payload
|
||||
localStorage.setItem(`favoriteStops`, JSON.stringify(state.stops))
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Action creators are generated for each case reducer function
|
||||
export const { saveFavoriteBusStops } = favoriteStopsSlice.actions
|
||||
|
||||
export const favoriteStopsSelector = (state: RootState): StopKeys[] => state.favoriteStops.stops
|
||||
|
||||
export default favoriteStopsSlice.reducer
|
||||
@ -0,0 +1,51 @@
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { AndrewLytics } from 'shared/lib'
|
||||
import { busStopSelector } from 'shared/store/busStop/busStopInfoSlice'
|
||||
import { StopKeys } from 'shared/store/busStop/Stops'
|
||||
import { CustomButtonStyled } from 'shared/ui'
|
||||
|
||||
import { favoriteStopsSelector, saveFavoriteBusStops } from '../model/favoriteStopsSlice'
|
||||
|
||||
export const FavoriteButton: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const busStop = useSelector(busStopSelector)
|
||||
const favoriteBusStops = useSelector(favoriteStopsSelector)
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const handleAddFavoriteStatus = useCallback(() => {
|
||||
if (!busStop) return
|
||||
|
||||
if (favoriteBusStops.includes(busStop)) return
|
||||
|
||||
const newStops: StopKeys[] = [busStop, ...favoriteBusStops]
|
||||
dispatch(saveFavoriteBusStops(newStops))
|
||||
|
||||
AndrewLytics(`addStop`)
|
||||
}, [busStop, favoriteBusStops, dispatch])
|
||||
|
||||
const handleRemoveFavoriteStatus = useCallback(() => {
|
||||
if (!busStop) return
|
||||
|
||||
if (!favoriteBusStops.includes(busStop)) return
|
||||
|
||||
const newStops: StopKeys[] = favoriteBusStops.filter(stop => stop !== busStop)
|
||||
dispatch(saveFavoriteBusStops(newStops))
|
||||
}, [busStop, favoriteBusStops, dispatch])
|
||||
|
||||
const isBusStopFavorite = useMemo(
|
||||
() => (busStop ? favoriteBusStops.includes(busStop) : false),
|
||||
[busStop, favoriteBusStops],
|
||||
)
|
||||
|
||||
return (
|
||||
<CustomButtonStyled
|
||||
$status={isBusStopFavorite ? `danger` : `primary`}
|
||||
$mt="12px"
|
||||
onClick={isBusStopFavorite ? handleRemoveFavoriteStatus : handleAddFavoriteStatus}
|
||||
>
|
||||
{isBusStopFavorite ? t(`Remove stop from favorite`) : t(`Add stop to favorite`)}
|
||||
</CustomButtonStyled>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { busStopSelector, setBusStop, stopsOptionsSelector } from 'shared/store/busStop/busStopInfoSlice'
|
||||
import { StopKeys } from 'shared/store/busStop/Stops'
|
||||
import { CardStyled, ContainerStyled } from 'shared/ui'
|
||||
import { Header } from 'shared/ui/Header'
|
||||
import { InlineOptions } from 'shared/ui/InlineOptions'
|
||||
|
||||
import { favoriteStopsSelector } from '../model/favoriteStopsSlice'
|
||||
|
||||
export const FavoriteStops: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const favoriteBusStops = useSelector(favoriteStopsSelector)
|
||||
const stopsOptions = useSelector(stopsOptionsSelector)
|
||||
const busStop = useSelector(busStopSelector)
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const favoriteList = useMemo(
|
||||
() => stopsOptions.filter(stop => stop.value && favoriteBusStops.includes(stop.value)),
|
||||
[stopsOptions, favoriteBusStops],
|
||||
)
|
||||
|
||||
const handleChangeBusStop = useCallback(
|
||||
(busStopToChange: StopKeys | null) => {
|
||||
if (busStopToChange) dispatch(setBusStop(busStopToChange))
|
||||
},
|
||||
[dispatch],
|
||||
)
|
||||
|
||||
if (favoriteList.length === 0) return null
|
||||
|
||||
const myStops = t(`My stops`)
|
||||
|
||||
return (
|
||||
<ContainerStyled>
|
||||
<CardStyled>
|
||||
<Header text={myStops} />
|
||||
<InlineOptions<StopKeys> list={favoriteList} activeId={busStop} onClick={handleChangeBusStop} />
|
||||
</CardStyled>
|
||||
</ContainerStyled>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from './ui/HeaderScore'
|
||||
@ -0,0 +1,27 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Link, useSearchParams } from 'react-router-dom'
|
||||
import { AndrewLytics } from 'shared/lib'
|
||||
|
||||
import { StyledScore } from './styled'
|
||||
|
||||
export const HeaderScore: React.FC = () => {
|
||||
const [searchParams] = useSearchParams()
|
||||
const [score, setScore] = useState<undefined | string>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
const scoreToChange = localStorage.getItem(`score`)
|
||||
if (scoreToChange && !Number.isNaN(Number(scoreToChange))) {
|
||||
setScore(scoreToChange)
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (!score) return null
|
||||
|
||||
return (
|
||||
<Link to={`/game?${searchParams.toString()}`} onClick={() => AndrewLytics(`game.headerScore`)}>
|
||||
<StyledScore $selected={false} $destroyed={false}>
|
||||
{score}
|
||||
</StyledScore>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
import { GameCell } from 'page/Game/styled'
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const StyledScore = styled(GameCell)`
|
||||
min-width: 30px;
|
||||
font-size: 12px;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
|
||||
background: linear-gradient(180.17deg, #723cb7 0.15%, #ff47e2 99.85%);
|
||||
box-shadow: 0px 2px 8px rgba(121, 61, 185, 0.15);
|
||||
|
||||
&::after {
|
||||
height: 1px;
|
||||
margin: 0 2px;
|
||||
top: 2px;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
`
|
||||
@ -0,0 +1,61 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const HowMuchLeftContainerStyled = styled.div<{ $isFancy: boolean; $defaultColor: string }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
padding: 18px 17px;
|
||||
background: ${props =>
|
||||
props.$isFancy
|
||||
? `linear-gradient(93.72deg, #5020CA 9.83%, #5020CA 9.84%, #8365E0 96.15%);`
|
||||
: props.$defaultColor};
|
||||
color: ${props => (props.$isFancy ? `white` : `black`)};
|
||||
border-radius: 13px;
|
||||
`
|
||||
|
||||
export const NextBusContainerStyled = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
export const FastReplyContainerStyled = styled.div`
|
||||
margin-top: 18px;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
export const FastReplyButtonStyled = styled.button`
|
||||
background: #e4f5d6;
|
||||
border-radius: 13px;
|
||||
color: black;
|
||||
padding: 15px;
|
||||
width: 100%;
|
||||
`
|
||||
export const TextWrapperStyled = styled.div``
|
||||
|
||||
export const HighLightedStyled = styled.span`
|
||||
font-weight: bold;
|
||||
/* animation-duration: 2s;
|
||||
animation-name: flashing;
|
||||
animation-iteration-count: infinite;
|
||||
|
||||
@keyframes flashing {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
} */
|
||||
`
|
||||
|
||||
export const BusEstimationStyled = styled.div`
|
||||
font-size: 18px;
|
||||
margin-left: 19px;
|
||||
`
|
||||
@ -0,0 +1,113 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import SVG from 'react-inlinesvg'
|
||||
import { Holiday } from 'entities/Holiday'
|
||||
import { SelectBusStopText } from 'entities/SelectBusStopText'
|
||||
import { Fastreply } from 'features/Complains/ui/Fastreply'
|
||||
import { useTypedSelector } from 'shared/lib'
|
||||
import { isHalloween } from 'shared/store/app/selectors/isHalloween'
|
||||
import { StopKeys } from 'shared/store/busStop/Stops'
|
||||
import { IHoliday } from 'shared/store/holidays/IHolidays'
|
||||
import { ITime } from 'shared/store/timeLeft/ITime'
|
||||
|
||||
import { ImageWrapperStyled } from '../../shared/ui/ImageWrapper'
|
||||
import {
|
||||
BusEstimationStyled,
|
||||
HighLightedStyled,
|
||||
HowMuchLeftContainerStyled,
|
||||
NextBusContainerStyled,
|
||||
TextWrapperStyled,
|
||||
} from './HowMuchLeft.styled'
|
||||
import Dead from './img/dead.svg'
|
||||
import EvilFace from './img/evil-face.svg'
|
||||
import NextBus from './img/next-bus.svg'
|
||||
import Pumpkin from './img/pumpkin.svg'
|
||||
|
||||
const SIZE = 45
|
||||
|
||||
export const LeftToString: React.FC<{ left: ITime; busStop: StopKeys | null }> = ({ busStop, left }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!busStop) return <SelectBusStopText />
|
||||
|
||||
if (left.hours === null && left.minutes === null)
|
||||
return (
|
||||
<TextWrapperStyled>
|
||||
{t(`Bus on stop`)} <b>{busStop}</b> {t(`today wont arrive`)}
|
||||
</TextWrapperStyled>
|
||||
)
|
||||
|
||||
const leftString = left.hours || `0`
|
||||
|
||||
return (
|
||||
<TextWrapperStyled>
|
||||
{t(`Next bus arriving in`)}
|
||||
{` `}
|
||||
<HighLightedStyled>
|
||||
{left.hours === 0 ? `` : `${leftString}ч `}
|
||||
{left.minutes}м
|
||||
</HighLightedStyled>
|
||||
</TextWrapperStyled>
|
||||
)
|
||||
}
|
||||
|
||||
interface ILeftProps {
|
||||
left: ITime
|
||||
busStopLabel: StopKeys | null
|
||||
holiday: IHoliday | null
|
||||
}
|
||||
|
||||
export const HowMuchLeft: React.FC<ILeftProps> = ({ left, busStopLabel, holiday }) => {
|
||||
const isHalloweenMode = useTypedSelector(isHalloween)
|
||||
const isFancy = isHalloweenMode || !!holiday
|
||||
const [currentIcon, setIcon] = useState(isHalloweenMode ? Pumpkin : NextBus)
|
||||
const [iconClickCounter, setIconClickCounter] = useState(0)
|
||||
|
||||
const getColorByLeftTime = (): string => {
|
||||
if (left.hours === null || left.minutes === null || left.hours >= 1) return `#e7edec`
|
||||
|
||||
if (left.minutes > 15 && left.minutes < 35) return `#E4F5D6`
|
||||
if (left.minutes <= 15) return `#FBDCDC`
|
||||
|
||||
return `#e7edec`
|
||||
}
|
||||
|
||||
const handleIconClick = (): void => {
|
||||
setIconClickCounter(prev => prev + 1)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isHalloweenMode) return
|
||||
if (iconClickCounter > 18) {
|
||||
setIcon(Dead)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (iconClickCounter % 2 === 0) {
|
||||
setIcon(Pumpkin)
|
||||
} else {
|
||||
setIcon(EvilFace)
|
||||
}
|
||||
}, [iconClickCounter, isHalloweenMode])
|
||||
|
||||
return (
|
||||
<>
|
||||
<HowMuchLeftContainerStyled $isFancy={isFancy} $defaultColor={getColorByLeftTime()}>
|
||||
<NextBusContainerStyled>
|
||||
<ImageWrapperStyled $w={SIZE} $h={SIZE}>
|
||||
<SVG src={currentIcon} width={SIZE} height={SIZE} uniquifyIDs onClick={handleIconClick} />
|
||||
</ImageWrapperStyled>
|
||||
|
||||
<BusEstimationStyled>
|
||||
<LeftToString busStop={busStopLabel} left={left} />
|
||||
</BusEstimationStyled>
|
||||
</NextBusContainerStyled>
|
||||
</HowMuchLeftContainerStyled>
|
||||
|
||||
<Fastreply />
|
||||
|
||||
{holiday && <Holiday />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 29 KiB |
@ -0,0 +1 @@
|
||||
export * from './HowMuchLeft'
|
||||
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none"><g fill="#000" opacity=".5"><rect width="2.05" height="13.37" rx="1.03" transform="matrix(-.71306 -.7011 -.69903 .71509 11.8 2.36)"/><rect width="2.05" height="13.37" rx="1.03" transform="scale(.99855 -1.00145) rotate(45 19.5 6.7)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 318 B |
@ -0,0 +1 @@
|
||||
export * from './ui/Info'
|
||||
@ -0,0 +1,20 @@
|
||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
|
||||
import { FetchInfoResponse, INFO_URL } from 'shared/api'
|
||||
|
||||
import { IInfo } from '../types/IInfo'
|
||||
|
||||
// Define a service using a base URL and expected endpoints
|
||||
export const infoApi = createApi({
|
||||
reducerPath: `infoApi`,
|
||||
baseQuery: fetchBaseQuery({ baseUrl: INFO_URL }),
|
||||
endpoints: builder => ({
|
||||
getInfo: builder.query<IInfo, void>({
|
||||
query: () => ``,
|
||||
transformResponse: (response: FetchInfoResponse) => response.fields,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
// Export hooks for usage in functional components, which are
|
||||
// auto-generated based on the defined endpoints
|
||||
export const { useGetInfoQuery } = infoApi
|
||||
@ -0,0 +1,5 @@
|
||||
export interface IInfo {
|
||||
message: string | null
|
||||
id: number | null
|
||||
link: string | null
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import SVG from 'react-inlinesvg'
|
||||
import { AndrewLytics } from 'shared/lib'
|
||||
import { ContainerStyled } from 'shared/ui/common'
|
||||
|
||||
import InfoCloseCross from '../img/infoclosecross.svg'
|
||||
import { useGetInfoQuery } from '../model/info'
|
||||
import { InfoCloseButtonStyled, InfoLinkStyled, InfoTextStyled, InfoWrapperStyled } from './styled'
|
||||
|
||||
export const Info: React.FC = () => {
|
||||
const [isInfoShow, setIsInfoShow] = useState(false)
|
||||
const { data: infoMessage } = useGetInfoQuery()
|
||||
|
||||
const onLinkClick = (): void => {
|
||||
AndrewLytics(`infoBlockLinkClick`)
|
||||
}
|
||||
|
||||
const onInfoCrossClick = (): void => {
|
||||
setIsInfoShow(false)
|
||||
if (infoMessage?.id) {
|
||||
localStorage.setItem(`infoMessageId`, String(infoMessage.id))
|
||||
}
|
||||
|
||||
AndrewLytics(`infoBlockHide`)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!infoMessage?.id) return
|
||||
|
||||
const infoMessageId = localStorage.getItem(`infoMessageId`)
|
||||
if (Number(infoMessageId) !== Number(infoMessage.id)) setIsInfoShow(true)
|
||||
}, [infoMessage])
|
||||
|
||||
if (!infoMessage || !isInfoShow) return null
|
||||
|
||||
const { message: text, link } = infoMessage
|
||||
|
||||
return (
|
||||
<ContainerStyled>
|
||||
<InfoWrapperStyled>
|
||||
{link ? (
|
||||
<InfoLinkStyled href={link} target="_blank" onClick={onLinkClick}>
|
||||
{text}
|
||||
</InfoLinkStyled>
|
||||
) : (
|
||||
<InfoTextStyled>{text}</InfoTextStyled>
|
||||
)}
|
||||
<InfoCloseButtonStyled data-testid="hide-btn" onClick={onInfoCrossClick}>
|
||||
<SVG className="closebutton" src={InfoCloseCross} />
|
||||
</InfoCloseButtonStyled>
|
||||
</InfoWrapperStyled>
|
||||
</ContainerStyled>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const InfoWrapperStyled = styled.div`
|
||||
width: 100%;
|
||||
padding: 15px 30px 15px 15px;
|
||||
|
||||
background: linear-gradient(100.09deg, #0374f9 -39.57%, #35e0ff 124.36%);
|
||||
box-shadow: 0px 1px 22px -4px rgba(14, 139, 251, 0.46);
|
||||
border-radius: 18px;
|
||||
|
||||
position: relative;
|
||||
`
|
||||
|
||||
export const InfoTextStyled = styled.p`
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-size: 16px;
|
||||
line-height: 19px;
|
||||
|
||||
color: #ffffff;
|
||||
`
|
||||
|
||||
export const InfoLinkStyled = styled.a`
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-size: 16px;
|
||||
line-height: 19px;
|
||||
|
||||
text-decoration: underline;
|
||||
color: #ffffff;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
`
|
||||
|
||||
export const InfoCloseButtonStyled = styled.div`
|
||||
position: absolute;
|
||||
top: 9px;
|
||||
right: 9px;
|
||||
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
|
||||
.closebutton {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
}
|
||||
`
|
||||
@ -0,0 +1,34 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import SVG from 'react-inlinesvg'
|
||||
import { Link, useSearchParams } from 'react-router-dom'
|
||||
import { FEEDBACK_LINK } from 'shared/common'
|
||||
import { AndrewLytics } from 'shared/lib'
|
||||
import { ContainerStyled } from 'shared/ui'
|
||||
|
||||
import Bird from './img/bird-zamanuha.svg'
|
||||
import { BirdContainerStyled, BirdWrapperStyled, FeedbackLinkStyled, FeedbackTextStyled } from './styled'
|
||||
|
||||
export const LeaveFeedbackButton: React.FC = () => {
|
||||
const [searchParams] = useSearchParams()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onFeedbackClick = (): void => {
|
||||
AndrewLytics(`FeedbackClick`)
|
||||
}
|
||||
|
||||
return (
|
||||
<ContainerStyled $doubled>
|
||||
<BirdWrapperStyled>
|
||||
<FeedbackLinkStyled href={FEEDBACK_LINK} onClick={onFeedbackClick}>
|
||||
<FeedbackTextStyled>{t(`Leave feedback`)}</FeedbackTextStyled>
|
||||
</FeedbackLinkStyled>
|
||||
|
||||
<BirdContainerStyled onClick={() => AndrewLytics(`game.birdPlay`)}>
|
||||
<Link to={`/game?${searchParams.toString()}`} onClick={() => AndrewLytics(`game.footerPlay`)}>
|
||||
<SVG src={Bird} width={32} height={48} uniquifyIDs />
|
||||
</Link>
|
||||
</BirdContainerStyled>
|
||||
</BirdWrapperStyled>
|
||||
</ContainerStyled>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="49" fill="none"><rect width="32" height="32" fill="#000" fill-opacity=".74" rx="5"/><path fill="#fff" d="M8 14.18V20H6.77v-4.65h-.03l-1.33.83V15.1l1.44-.9H8Zm3.54 5.9c-.43 0-.8-.07-1.14-.22a1.93 1.93 0 0 1-.78-.6 1.53 1.53 0 0 1-.3-.9h1.25c0 .14.05.26.14.37.08.1.2.19.34.25.15.06.3.09.49.09s.35-.04.5-.1a.81.81 0 0 0 .33-.28.7.7 0 0 0 .13-.4c0-.16-.05-.3-.13-.42a.83.83 0 0 0-.37-.28c-.16-.07-.35-.1-.57-.1h-.54v-.9h.54c.18 0 .35-.04.49-.1a.8.8 0 0 0 .34-.27.7.7 0 0 0 .11-.4.7.7 0 0 0-.4-.65 1.13 1.13 0 0 0-.89 0 .8.8 0 0 0-.32.25.67.67 0 0 0-.14.39H9.45c0-.34.1-.63.28-.89.19-.26.44-.46.75-.6.32-.14.68-.22 1.07-.22.4 0 .76.08 1.06.22.3.15.53.34.7.6.17.24.25.51.25.82 0 .33-.1.6-.3.82-.2.21-.47.35-.8.41v.05c.43.05.76.2.98.44.22.24.34.54.33.9 0 .34-.09.63-.28.89-.2.26-.46.46-.8.6-.33.16-.72.23-1.15.23Zm3.84-.76a.66.66 0 0 1-.49-.2.66.66 0 0 1-.2-.48c0-.18.07-.34.2-.48.14-.13.3-.2.49-.2.18 0 .34.07.48.2.13.14.2.3.2.48 0 .13-.03.24-.1.35a.73.73 0 0 1-.24.24c-.1.06-.22.1-.34.1Zm0-2.85a.66.66 0 0 1-.49-.2.66.66 0 0 1-.2-.48c0-.19.07-.35.2-.48.14-.13.3-.2.49-.2.18 0 .34.07.48.2.13.13.2.3.2.48 0 .12-.03.24-.1.34a.7.7 0 0 1-.24.25c-.1.06-.22.09-.34.09Zm3.79 3.61c-.4 0-.76-.07-1.08-.22a1.87 1.87 0 0 1-.74-.61 1.57 1.57 0 0 1-.29-.9h1.2c0 .23.1.41.28.55.18.14.38.21.63.21a1 1 0 0 0 .5-.12c.16-.1.27-.21.36-.36.08-.16.13-.34.13-.54s-.05-.38-.14-.54a.92.92 0 0 0-.35-.36 1.05 1.05 0 0 0-.52-.14c-.17 0-.34.04-.5.1a.9.9 0 0 0-.37.3l-1.1-.2.28-3.07h3.56v1h-2.54l-.15 1.46h.03c.1-.14.26-.26.46-.35a1.79 1.79 0 0 1 2.28.78c.16.29.24.62.24.99a1.92 1.92 0 0 1-1.04 1.76c-.32.17-.7.26-1.13.26Zm5.2 0c-.44 0-.83-.07-1.17-.21-.34-.15-.6-.34-.8-.6-.2-.24-.29-.52-.29-.84 0-.24.06-.46.17-.66a1.5 1.5 0 0 1 1.1-.77v-.04a1.3 1.3 0 0 1-1.05-1.29c0-.3.09-.57.26-.8.18-.24.42-.43.73-.56.3-.14.66-.2 1.05-.2.4 0 .74.06 1.05.2.3.13.55.32.72.56.18.23.27.5.27.8a1.3 1.3 0 0 1-1.04 1.29V17c.23.04.44.12.63.26a1.48 1.48 0 0 1 .63 1.17c0 .32-.1.6-.3.85-.19.25-.46.44-.8.59a3 3 0 0 1-1.16.2Zm0-.9c.2 0 .36-.04.51-.1a.84.84 0 0 0 .34-.3.78.78 0 0 0 .13-.44.79.79 0 0 0-.13-.45.88.88 0 0 0-.35-.3 1.08 1.08 0 0 0-.5-.12c-.19 0-.36.04-.5.12a.88.88 0 0 0-.35.3.8.8 0 0 0-.13.45c0 .16.04.3.12.44.08.12.2.22.35.3.14.06.32.1.5.1Zm0-2.6c.16 0 .3-.04.43-.1a.75.75 0 0 0 .4-.68.73.73 0 0 0-.4-.67.92.92 0 0 0-.43-.1.93.93 0 0 0-.44.1.74.74 0 0 0-.3.27.74.74 0 0 0-.1.4c0 .15.03.28.1.4.08.12.18.21.3.28.13.06.28.1.44.1Z"/><path fill="#fff" fill-opacity=".14" d="M3 2.84c0-.46.38-.84.84-.84h23.58a.84.84 0 0 1 0 1.68H3.84A.84.84 0 0 1 3 2.84Z"/><path stroke="#000" d="M24 32v11.5m0 0c-2.5.5-2.5 1-4 1.5-.85.28 1.27-.95 4-1.5Zm0 0c-2 1.5-2.14 4.55-2 4 .5-2 .5-2 2-4Zm0 0c1.84 1.4 1.2 1.2 2 2 .5.5-.02-.5-2-2ZM9 32v8.5l-.5 3m0 0c-2.83.17-4.5.5-6 1-.5.17 1-.7 6-1Zm0 0C6 44.5 3.43 48 4 48c.5 0 .5-2 4.5-4.5Zm0 0c2 1.5 2 3 2 3.5s1-1-2-3.5Z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@ -0,0 +1 @@
|
||||
export * from './LeaveFeedbackButton'
|
||||
@ -0,0 +1,38 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const FeedbackLinkStyled = styled.a`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
padding: 23px 32px;
|
||||
width: 100%;
|
||||
|
||||
background: #caeaff;
|
||||
border-radius: 15px;
|
||||
|
||||
@media (hover: hover) {
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const FeedbackTextStyled = styled.div`
|
||||
font-weight: 600;
|
||||
font-size: 17px;
|
||||
line-height: 19px;
|
||||
|
||||
color: #000000;
|
||||
`
|
||||
|
||||
export const BirdWrapperStyled = styled.div`
|
||||
position: relative;
|
||||
`
|
||||
|
||||
export const BirdContainerStyled = styled.div`
|
||||
position: absolute;
|
||||
top: -44px;
|
||||
right: 9px;
|
||||
`
|
||||
@ -0,0 +1,69 @@
|
||||
import SVG from 'react-inlinesvg'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { getLocation } from './helpers/getLocation'
|
||||
import LocationSVG from './img/location.svg'
|
||||
import { setLocation, setLocationError, setNoGeolocation } from './model/myLocationSlice'
|
||||
|
||||
export const GeoLocationStyled = styled.div`
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
z-index: 3;
|
||||
|
||||
box-shadow: 0px 1px 13px rgba(0, 0, 0, 0.08);
|
||||
`
|
||||
const SIZE = 16
|
||||
export const MyLocation: React.FC = () => {
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const onSuccess = (position: GeolocationPosition): void => {
|
||||
dispatch(
|
||||
setLocation({
|
||||
coords: {
|
||||
accuracy: position.coords.accuracy,
|
||||
altitude: position.coords.altitude,
|
||||
altitudeAccuracy: position.coords.altitudeAccuracy,
|
||||
heading: position.coords.heading,
|
||||
latitude: position.coords.latitude,
|
||||
longitude: position.coords.longitude,
|
||||
speed: position.coords.speed,
|
||||
},
|
||||
timestamp: position.timestamp,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const onError = (data: GeolocationPositionError): void => {
|
||||
dispatch(
|
||||
setLocationError({
|
||||
code: data.code,
|
||||
message: data.message,
|
||||
PERMISSION_DENIED: data.PERMISSION_DENIED,
|
||||
POSITION_UNAVAILABLE: data.POSITION_UNAVAILABLE,
|
||||
TIMEOUT: data.TIMEOUT,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const onNoLocation = (): void => {
|
||||
dispatch(setNoGeolocation(true))
|
||||
}
|
||||
|
||||
const handleClick = (): void => {
|
||||
getLocation(onSuccess, onError, onNoLocation)
|
||||
}
|
||||
|
||||
return (
|
||||
<GeoLocationStyled onClick={handleClick}>
|
||||
<SVG src={LocationSVG} width={SIZE} height={SIZE} uniquifyIDs />
|
||||
</GeoLocationStyled>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
export const getLocation = (
|
||||
successCallback: PositionCallback,
|
||||
errorCallback: PositionErrorCallback | null | undefined,
|
||||
noLocatonCb: () => void,
|
||||
options?: PositionOptions | undefined,
|
||||
): void => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (navigator.geolocation) {
|
||||
navigator.geolocation.getCurrentPosition(successCallback, errorCallback, options)
|
||||
} else {
|
||||
noLocatonCb()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.1584 16.9749L8.00154 10.467C8.00057 10.4641 7.99829 10.4618 7.99537 10.4607L1.02394 8.00842C1.01538 8.00541 1.01494 7.99346 1.02325 7.98983L16.9769 1.01011C16.9852 1.00648 16.9936 1.01487 16.9901 1.0232L10.177 16.9757C10.1734 16.9842 10.1613 16.9837 10.1584 16.9749Z" stroke="black" stroke-width="1.9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 420 B |
@ -0,0 +1 @@
|
||||
export * from './MyLocation'
|
||||
@ -0,0 +1,38 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
import { RootState } from 'shared/store/app/configureStore'
|
||||
|
||||
export interface MyLocationState {
|
||||
location: GeolocationPosition | undefined
|
||||
error: GeolocationPositionError | undefined
|
||||
noGeolocation: boolean
|
||||
}
|
||||
|
||||
const initialState: MyLocationState = {
|
||||
location: undefined,
|
||||
error: undefined,
|
||||
noGeolocation: false,
|
||||
}
|
||||
|
||||
export const myLocationSlice = createSlice({
|
||||
name: `myLocationSlice`,
|
||||
initialState,
|
||||
reducers: {
|
||||
setLocation: (state, action: PayloadAction<GeolocationPosition>) => {
|
||||
state.location = action.payload
|
||||
},
|
||||
setLocationError: (state, action: PayloadAction<GeolocationPositionError>) => {
|
||||
state.error = action.payload
|
||||
},
|
||||
setNoGeolocation: (state, action: PayloadAction<boolean>) => {
|
||||
state.noGeolocation = action.payload
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Action creators are generated for each case reducer function
|
||||
export const { setLocation, setLocationError, setNoGeolocation } = myLocationSlice.actions
|
||||
|
||||
export const userLocationSelector = (state: RootState): GeolocationPosition | undefined =>
|
||||
state.myLocationSlice.location
|
||||
|
||||
export default myLocationSlice.reducer
|
||||
@ -0,0 +1,36 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const NearestStopStyled = styled.article`
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
padding: 9px;
|
||||
background-color: #fff;
|
||||
border-radius: 19px;
|
||||
background: radial-gradient(50% 50% at 50% 50%, #fff 0%, rgba(255, 255, 255, 0) 100%), #f2f4f4;
|
||||
|
||||
& + & {
|
||||
margin-left: 9px;
|
||||
}
|
||||
`
|
||||
|
||||
export const NearestStopsStyled = styled.div`
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
background-color: #fff;
|
||||
`
|
||||
|
||||
export const NearestStopsLabelStyled = styled.span`
|
||||
display: -webkit-box;
|
||||
max-width: 115px;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
margin-bottom: 9px;
|
||||
`
|
||||
|
||||
export const NearestStopsDirectionStyled = styled.span`
|
||||
opacity: 0.66;
|
||||
`
|
||||
@ -0,0 +1,71 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { getDirectionString } from 'features/Complains/helpers'
|
||||
import { userLocationSelector } from 'features/MyLocation/model/myLocationSlice'
|
||||
import L from 'leaflet'
|
||||
import { setBusStopNew } from 'shared/store/busStop/busStopInfoSlice'
|
||||
import { STOPS } from 'shared/store/busStop/const/stops'
|
||||
import { DirectionsNew, IStops } from 'shared/store/busStop/Stops'
|
||||
import { CardStyled, ContainerStyled } from 'shared/ui'
|
||||
|
||||
import {
|
||||
NearestStopsDirectionStyled,
|
||||
NearestStopsLabelStyled,
|
||||
NearestStopsStyled,
|
||||
NearestStopStyled,
|
||||
} from './NearestStops.styled'
|
||||
|
||||
const MAX_DISTANCE = 300
|
||||
|
||||
interface INearestStopProps extends IStops<DirectionsNew> {
|
||||
onClick: (id: string) => void
|
||||
}
|
||||
|
||||
const NearestStop: React.FC<INearestStopProps> = ({ direction, label, id, onClick }) => {
|
||||
const handleClick = (): void => onClick(id)
|
||||
|
||||
return (
|
||||
<NearestStopStyled onClick={handleClick}>
|
||||
<NearestStopsLabelStyled>{label}</NearestStopsLabelStyled>
|
||||
<NearestStopsDirectionStyled>{getDirectionString(direction)}</NearestStopsDirectionStyled>
|
||||
</NearestStopStyled>
|
||||
)
|
||||
}
|
||||
export const NearestStops: React.FC = () => {
|
||||
const userLocation = useSelector(userLocationSelector)
|
||||
const dispatch = useDispatch()
|
||||
const [stops, setStops] = useState<IStops<DirectionsNew>[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (userLocation) {
|
||||
const nearestStops = STOPS.filter(
|
||||
stop =>
|
||||
L.latLng([userLocation.coords.latitude, userLocation.coords.longitude]).distanceTo(stop.latLon) <
|
||||
MAX_DISTANCE,
|
||||
)
|
||||
|
||||
setStops(nearestStops)
|
||||
}
|
||||
}, [userLocation])
|
||||
|
||||
const onStopClick = useCallback(
|
||||
(id: string) => {
|
||||
dispatch(setBusStopNew(id))
|
||||
},
|
||||
[dispatch],
|
||||
)
|
||||
|
||||
if (stops.length === 0) return null
|
||||
|
||||
return (
|
||||
<ContainerStyled>
|
||||
<CardStyled>
|
||||
<NearestStopsStyled>
|
||||
{stops.map(stop => (
|
||||
<NearestStop {...stop} onClick={onStopClick} key={stop.id} />
|
||||
))}
|
||||
</NearestStopsStyled>
|
||||
</CardStyled>
|
||||
</ContainerStyled>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from './NearestStops'
|
||||
@ -0,0 +1,77 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import Select from 'react-select'
|
||||
import { AndrewLytics } from 'shared/lib'
|
||||
import { busStopSelector, directionSelector } from 'shared/store/busStop/busStopInfoSlice'
|
||||
import { IOption } from 'shared/store/busStop/Stops'
|
||||
import { nextDaySelector, scheduleSelector } from 'shared/store/schedule/scheduleSlice'
|
||||
import { CardStyled, ContainerStyled } from 'shared/ui'
|
||||
import { Header } from 'shared/ui/Header/Header'
|
||||
import { OtherTimeStyled } from 'shared/ui/OtherTime'
|
||||
import { selectStyles } from 'shared/ui/SelectStyles'
|
||||
import { TimeStampStyled } from 'shared/ui/TimeStamp'
|
||||
|
||||
import { SelectBusStopText } from '../../entities/SelectBusStopText'
|
||||
|
||||
export const OtherTimeBusses: React.FC = () => {
|
||||
const busStop = useSelector(busStopSelector)
|
||||
const nextDay = useSelector(nextDaySelector)
|
||||
const SCHEDULE = useSelector(scheduleSelector)
|
||||
const direction = useSelector(directionSelector)
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const DaysOptions = [
|
||||
{
|
||||
label: t(`Tomorrow`),
|
||||
value: nextDay,
|
||||
},
|
||||
{
|
||||
label: t(`Workday`),
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: t(`Weekday`),
|
||||
value: 2,
|
||||
},
|
||||
]
|
||||
|
||||
const [busOption, setBusOption] = useState<IOption<number>>(DaysOptions[0])
|
||||
|
||||
const handleChange = (e: IOption<number> | null): void => {
|
||||
if (!e) return
|
||||
AndrewLytics(`otherSchedule`)
|
||||
setBusOption(e)
|
||||
}
|
||||
|
||||
const renderOtherTimeContent = useMemo(() => {
|
||||
return busStop ? (
|
||||
SCHEDULE[direction][busOption.value][busStop].map(timeKeys => (
|
||||
<TimeStampStyled key={`${timeKeys}`}>{timeKeys}</TimeStampStyled>
|
||||
))
|
||||
) : (
|
||||
<SelectBusStopText />
|
||||
)
|
||||
}, [busStop, SCHEDULE, direction, busOption])
|
||||
|
||||
return (
|
||||
<ContainerStyled>
|
||||
<CardStyled>
|
||||
<Header text={t(`Buses for`)}>
|
||||
<Select
|
||||
isSearchable={false}
|
||||
styles={selectStyles}
|
||||
options={DaysOptions}
|
||||
onChange={handleChange}
|
||||
value={busOption}
|
||||
defaultValue={DaysOptions[0]}
|
||||
/>
|
||||
</Header>
|
||||
|
||||
<OtherTimeStyled>{renderOtherTimeContent}</OtherTimeStyled>
|
||||
</CardStyled>
|
||||
</ContainerStyled>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from './OtherTimeBuses'
|
||||
@ -0,0 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
declare module '*.svg' {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const DevStyled = styled.div``;
|
||||
@ -0,0 +1,70 @@
|
||||
import React from 'react'
|
||||
|
||||
import { LeftToString } from 'features/HowMuchLeft'
|
||||
import { GameLink, GameUIContainer, Title } from 'page/Game/common'
|
||||
import RecordTable from 'page/Game/RecordTable/RecordTable'
|
||||
|
||||
import { HeaderContainer } from './styled'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { busStopSelector } from 'shared/store/busStop/busStopInfoSlice'
|
||||
import { leftSelector } from 'shared/store/timeLeft/timeLeftSlice'
|
||||
|
||||
interface IGameHeaderProps {
|
||||
score: number
|
||||
miss: number
|
||||
level: number
|
||||
timeLeft: { seconds: number } | null
|
||||
title?: string | null
|
||||
isGameOver?: boolean
|
||||
plusNumber?: string
|
||||
bestScore: number
|
||||
}
|
||||
|
||||
const Header: React.FC<IGameHeaderProps> = ({
|
||||
score,
|
||||
level,
|
||||
plusNumber = '',
|
||||
title = null,
|
||||
isGameOver = false,
|
||||
bestScore,
|
||||
}) => {
|
||||
const left = useSelector(leftSelector)
|
||||
const busStop = useSelector(busStopSelector)
|
||||
|
||||
return (
|
||||
<HeaderContainer fullHeight={isGameOver}>
|
||||
{busStop ? (
|
||||
<GameUIContainer>
|
||||
<GameLink to="/" lowLight={!isGameOver}>
|
||||
<LeftToString busStop={'Швейная фабрика'} left={left} />
|
||||
</GameLink>
|
||||
</GameUIContainer>
|
||||
) : null}
|
||||
|
||||
<GameUIContainer>
|
||||
{title && isGameOver ? (
|
||||
<GameUIContainer>
|
||||
<Title>{title}</Title>
|
||||
</GameUIContainer>
|
||||
) : null}
|
||||
|
||||
<GameUIContainer>
|
||||
<RecordTable
|
||||
plusNumber={plusNumber}
|
||||
score={score}
|
||||
bestScore={bestScore}
|
||||
level={level}
|
||||
isGameOver={isGameOver}
|
||||
/>
|
||||
</GameUIContainer>
|
||||
</GameUIContainer>
|
||||
{title && !isGameOver ? (
|
||||
<GameUIContainer>
|
||||
<Title>{title}</Title>
|
||||
</GameUIContainer>
|
||||
) : null}
|
||||
</HeaderContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const HeaderContainer = styled.div<{ fullHeight: boolean }>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
/* height: ${props => (props.fullHeight ? '100%' : 'unset')}; */
|
||||
`
|
||||
@ -0,0 +1,11 @@
|
||||
import * as P from './styled'
|
||||
|
||||
const ProgressBar: React.FC<{ bgcolor: string; completed: number }> = ({ bgcolor, completed }) => {
|
||||
return (
|
||||
<P.Cotainer>
|
||||
<P.Filler bgcolor={bgcolor} style={{ width: `${completed}%` }}></P.Filler>
|
||||
</P.Cotainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProgressBar
|
||||
@ -0,0 +1,29 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const Cotainer = styled.div`
|
||||
height: 5px;
|
||||
width: 100%;
|
||||
background: rgba(0, 0, 0, 0.19);
|
||||
border-radius: 50;
|
||||
margin: 50;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
`
|
||||
|
||||
export const Filler = styled.div<{ bgcolor: string }>`
|
||||
height: 100%;
|
||||
width: 0;
|
||||
background-color: ${props => props.bgcolor};
|
||||
border-radius: inherit;
|
||||
text-align: right;
|
||||
|
||||
transition: width 0.3s ease-in-out;
|
||||
`
|
||||
|
||||
export const Label = styled.span`
|
||||
padding: 5;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
`
|
||||