Add scroll, languages, settings
@@ -13065,6 +13065,12 @@
|
|||||||
"integrity": "sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ==",
|
"integrity": "sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"uifx": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/uifx/-/uifx-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-tnPwdYe1dDmsxWJeU84CjDN/rcWOzOcG6tL1bsi5bUXw5nJaq+c4ThsbShMkedX2dAQ5gq1Q5CxQGsxwa5wxfw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"unicode-canonical-property-names-ecmascript": {
|
"unicode-canonical-property-names-ecmascript": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
|
||||||
|
|||||||
@@ -10,10 +10,11 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^16.9.38",
|
"@types/react": "^16.9.38",
|
||||||
"@types/react-dom": "^16.9.8",
|
"@types/react-dom": "^16.9.8",
|
||||||
|
"autoprefixer": "^9.8.2",
|
||||||
|
"node-sass": "^4.14.1",
|
||||||
"tslint": "^6.1.2",
|
"tslint": "^6.1.2",
|
||||||
"typescript": "^3.9.5",
|
"typescript": "^3.9.5",
|
||||||
"node-sass": "^4.14.1",
|
"uifx": "^2.0.7"
|
||||||
"autoprefixer": "^9.8.2"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
|
|||||||
@@ -1,20 +1,41 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import UIfx from 'uifx'
|
||||||
|
|
||||||
import { PanelComponent, PreviewComponent, ViewComponent } from '../components'
|
import { PanelComponent, PreviewComponent, SettingsComponent, ViewComponent } from '../components'
|
||||||
import places from '../assets'
|
import places from '../assets'
|
||||||
|
import { delay, UI_SOUND_VOLUME } from '../utils'
|
||||||
|
import { useSettings } from '../hooks'
|
||||||
|
|
||||||
|
import PanelOpenAudio from '../assets/audio/sound/panel-open.ogg'
|
||||||
|
import PanelCloseAudio from '../assets/audio/sound/panel-close.ogg'
|
||||||
|
|
||||||
|
import SettingsOpenAudio from '../assets/audio/sound/menu-open.ogg'
|
||||||
|
import SettingsCloseAudio from '../assets/audio/sound/menu-close.ogg'
|
||||||
|
|
||||||
|
import CheckBoxOnAudio from '../assets/audio/sound/check-box-on.ogg'
|
||||||
|
import CheckBoxOffAudio from '../assets/audio/sound/check-box-off.ogg'
|
||||||
|
|
||||||
import './style.scss'
|
import './style.scss'
|
||||||
|
|
||||||
const delay = () => new Promise(resolve => setTimeout(resolve, 800))
|
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const {settings} = useSettings()
|
||||||
|
const [isSettingsShown, setSettingsShown] = useState(false)
|
||||||
const [isLoading, setLoading] = useState(false)
|
const [isLoading, setLoading] = useState(false)
|
||||||
const [isLeftPanelShown, setLeftPanelShown] = useState(false)
|
const [isLeftPanelShown, setLeftPanelShown] = useState(false)
|
||||||
const [isBottomPanelShown, setBottomPanelShown] = useState(false)
|
const [isBottomPanelShown, setBottomPanelShown] = useState(false)
|
||||||
const [activePlace, setActivePlace] = useState(0)
|
const [activePlace, setActivePlace] = useState(0)
|
||||||
const [activeView, setActiveView] = useState(0)
|
const [activeView, setActiveView] = useState(0)
|
||||||
|
|
||||||
|
const panelOpenSound = useMemo(() => new UIfx(PanelOpenAudio), [])
|
||||||
|
const panelCloseSound = useMemo(() => new UIfx(PanelCloseAudio), [])
|
||||||
|
const settingsOpenSound = useMemo(() => new UIfx(SettingsOpenAudio), [])
|
||||||
|
const settingsCloseSound = useMemo(() => new UIfx(SettingsCloseAudio), [])
|
||||||
|
const checkboxOnSound = useMemo(() => new UIfx(CheckBoxOnAudio), [])
|
||||||
|
const checkboxOffSound = useMemo(() => new UIfx(CheckBoxOffAudio), [])
|
||||||
|
|
||||||
|
const app = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const handleHideLeftPanel = useCallback(() => {
|
const handleHideLeftPanel = useCallback(() => {
|
||||||
setBottomPanelShown(false)
|
setBottomPanelShown(false)
|
||||||
setLeftPanelShown(!isLeftPanelShown)
|
setLeftPanelShown(!isLeftPanelShown)
|
||||||
@@ -40,10 +61,54 @@ export default function App() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
appFocus()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const appFocus = () => {
|
||||||
|
if (app && app.current) {
|
||||||
|
app.current.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openCloseSettings = () => {
|
||||||
|
setSettingsShown(!isSettingsShown)
|
||||||
|
appFocus()
|
||||||
|
if (!settings.uiSound) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isSettingsShown) {
|
||||||
|
settingsCloseSound.play(UI_SOUND_VOLUME)
|
||||||
|
} else {
|
||||||
|
settingsOpenSound.play(UI_SOUND_VOLUME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOpenSettings = (e: React.KeyboardEvent) => {
|
||||||
|
switch (e.keyCode) {
|
||||||
|
case 27:
|
||||||
|
if (isLeftPanelShown || isBottomPanelShown) {
|
||||||
|
setLeftPanelShown(false)
|
||||||
|
setBottomPanelShown(false)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
openCloseSettings()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='main'>
|
<div
|
||||||
|
ref={app}
|
||||||
|
onKeyDown={handleOpenSettings}
|
||||||
|
tabIndex={0}
|
||||||
|
className='main'
|
||||||
|
>
|
||||||
<ViewComponent src={places[activePlace].view[activeView]}/>
|
<ViewComponent src={places[activePlace].view[activeView]}/>
|
||||||
<PanelComponent
|
<PanelComponent
|
||||||
|
openSound={panelOpenSound}
|
||||||
|
closeSound={panelCloseSound}
|
||||||
|
itemsCount={places.length || 0}
|
||||||
orientation='left'
|
orientation='left'
|
||||||
isShown={isLeftPanelShown}
|
isShown={isLeftPanelShown}
|
||||||
setShown={handleHideLeftPanel}
|
setShown={handleHideLeftPanel}
|
||||||
@@ -59,6 +124,9 @@ export default function App() {
|
|||||||
))}
|
))}
|
||||||
</PanelComponent>
|
</PanelComponent>
|
||||||
<PanelComponent
|
<PanelComponent
|
||||||
|
openSound={panelOpenSound}
|
||||||
|
closeSound={panelCloseSound}
|
||||||
|
itemsCount={places[activePlace].preview.length || 0}
|
||||||
orientation='bottom'
|
orientation='bottom'
|
||||||
isShown={isBottomPanelShown}
|
isShown={isBottomPanelShown}
|
||||||
setShown={handleHideBottomPanel}
|
setShown={handleHideBottomPanel}
|
||||||
@@ -73,6 +141,14 @@ export default function App() {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</PanelComponent>
|
</PanelComponent>
|
||||||
|
{isSettingsShown && (
|
||||||
|
<SettingsComponent
|
||||||
|
closeSettings={openCloseSettings}
|
||||||
|
checkboxOnSound={checkboxOnSound}
|
||||||
|
checkboxOffSound={checkboxOffSound}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 8.4 KiB |
@@ -3,11 +3,19 @@ import StormwindPark2 from './stormwind-park-2.jpg'
|
|||||||
import StormwindPark3 from './stormwind-park-3.jpg'
|
import StormwindPark3 from './stormwind-park-3.jpg'
|
||||||
import StormwindPark4 from './stormwind-park-4.jpg'
|
import StormwindPark4 from './stormwind-park-4.jpg'
|
||||||
import StormwindPark5 from './stormwind-park-5.jpg'
|
import StormwindPark5 from './stormwind-park-5.jpg'
|
||||||
|
import StormwindPark6 from './stormwind-park-6.jpg'
|
||||||
|
import StormwindPark7 from './stormwind-park-7.jpg'
|
||||||
|
import StormwindPark8 from './stormwind-park-8.jpg'
|
||||||
|
import StormwindPark9 from './stormwind-park-9.jpg'
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
StormwindPark1,
|
StormwindPark1,
|
||||||
StormwindPark2,
|
StormwindPark2,
|
||||||
StormwindPark3,
|
StormwindPark3,
|
||||||
StormwindPark4,
|
StormwindPark4,
|
||||||
StormwindPark5
|
StormwindPark5,
|
||||||
|
StormwindPark6,
|
||||||
|
StormwindPark7,
|
||||||
|
StormwindPark8,
|
||||||
|
StormwindPark9
|
||||||
]
|
]
|
||||||
|
|||||||
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 44 KiB |
@@ -3,11 +3,19 @@ import StormwindPark2 from './stormwind-park-2.jpg'
|
|||||||
import StormwindPark3 from './stormwind-park-3.jpg'
|
import StormwindPark3 from './stormwind-park-3.jpg'
|
||||||
import StormwindPark4 from './stormwind-park-4.jpg'
|
import StormwindPark4 from './stormwind-park-4.jpg'
|
||||||
import StormwindPark5 from './stormwind-park-5.jpg'
|
import StormwindPark5 from './stormwind-park-5.jpg'
|
||||||
|
import StormwindPark6 from './stormwind-park-6.jpg'
|
||||||
|
import StormwindPark7 from './stormwind-park-7.jpg'
|
||||||
|
import StormwindPark8 from './stormwind-park-8.jpg'
|
||||||
|
import StormwindPark9 from './stormwind-park-9.jpg'
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
StormwindPark1,
|
StormwindPark1,
|
||||||
StormwindPark2,
|
StormwindPark2,
|
||||||
StormwindPark3,
|
StormwindPark3,
|
||||||
StormwindPark4,
|
StormwindPark4,
|
||||||
StormwindPark5
|
StormwindPark5,
|
||||||
|
StormwindPark6,
|
||||||
|
StormwindPark7,
|
||||||
|
StormwindPark8,
|
||||||
|
StormwindPark9
|
||||||
]
|
]
|
||||||
|
|||||||
|
After Width: | Height: | Size: 360 KiB |
|
After Width: | Height: | Size: 284 KiB |
|
After Width: | Height: | Size: 325 KiB |
|
After Width: | Height: | Size: 297 KiB |
@@ -0,0 +1,17 @@
|
|||||||
|
@import "../../app/style";
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-image: $checkbox;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: inset $hoverColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--checked {
|
||||||
|
background-image: $checkboxCheck, $checkbox;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import { KeyboardEvent } from 'react'
|
||||||
|
|
||||||
|
import './checkbox-component.scss'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
handleClick: (option: any) => void
|
||||||
|
optionName: any
|
||||||
|
value: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CheckBoxComponent = ({handleClick, optionName, value}: Props) => {
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent, option: any) => {
|
||||||
|
if (e.keyCode !== 13 && e.keyCode !== 32) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handleClick(option)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => handleClick(optionName)}
|
||||||
|
onKeyDown={(e) => handleKeyDown(e, optionName)}
|
||||||
|
className={`checkbox ${value ? 'checkbox--checked' : ''}`}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckBoxComponent.displayName = 'CheckBoxComponent'
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
export { ViewComponent } from './view-component/view-component'
|
export { ViewComponent } from './view-component/view-component'
|
||||||
|
export { SelectComponent } from './select-component/select-component'
|
||||||
export { PanelComponent } from './panel-component/panel-component'
|
export { PanelComponent } from './panel-component/panel-component'
|
||||||
export { PreviewComponent } from './preview-component/preview-component'
|
export { PreviewComponent } from './preview-component/preview-component'
|
||||||
|
export { SettingsComponent } from './settings-component/settings-component'
|
||||||
|
export { CheckBoxComponent } from './checkbox-component/checkbox-component'
|
||||||
|
|||||||
@@ -1,98 +1,78 @@
|
|||||||
@import "../../app/style";
|
@import "../../app/style";
|
||||||
|
|
||||||
|
$panelWidth: $previewWidth + 40px;
|
||||||
|
$panelHeight: $previewHeight + 40px;
|
||||||
|
|
||||||
|
$panelBorderSize: 8px;
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
display: flex;
|
padding: 20px;
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
border: none;
|
border: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 220px;
|
height: $panelHeight;
|
||||||
width: 360px;
|
width: $panelWidth;
|
||||||
background-image: $panelBackground;
|
background-image: $panelBackground;
|
||||||
background-repeat: repeat;
|
background-repeat: repeat;
|
||||||
|
|
||||||
|
&-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
cursor: pointer;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
border: 2px solid var(--foreground);
|
|
||||||
background-color: unset;
|
|
||||||
transition: width 0.2s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&--bottom {
|
&--bottom {
|
||||||
bottom: -220px;
|
bottom: -$panelHeight + $panelBorderSize;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
transition: bottom 0.5s;
|
transition: bottom $transitionDuration $transitionType;
|
||||||
|
border-top: $panelBorderSize double $fontColor;
|
||||||
|
border-image: $borderTop 16 32 16 32;
|
||||||
|
border-image-outset: $panelBorderSize - 2px 0 0 0;
|
||||||
|
border-image-width: $panelBorderSize*2 0 0 100%;
|
||||||
|
border-image-repeat: round round;
|
||||||
|
|
||||||
.panel-border {
|
.panel-content {
|
||||||
position: absolute;
|
height: 100%;
|
||||||
z-index: 3;
|
|
||||||
top: -10px;
|
|
||||||
min-width: 100%;
|
|
||||||
background-image: $borderRight;
|
|
||||||
background-repeat: repeat;
|
|
||||||
height: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
top: -10px;
|
top: -$buttonHeight - 5px;
|
||||||
width: 50px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&--shown {
|
&--shown {
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|
||||||
button {
|
|
||||||
height: 25px;
|
|
||||||
top: -25px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--left {
|
&--left {
|
||||||
left: -360px;
|
left: -$panelWidth + $panelBorderSize;
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
transition: left 0.5s;
|
border-right: $panelBorderSize double $fontColor;
|
||||||
|
border-image: $borderRight 32 16 0 0;
|
||||||
|
border-image-outset: 0 $panelBorderSize - 2px 0 0;
|
||||||
|
border-image-width: 100% $panelBorderSize*2 0 0;
|
||||||
|
transition: left $transitionDuration $transitionType;
|
||||||
|
|
||||||
.panel-border {
|
.panel-content {
|
||||||
position: absolute;
|
flex-direction: column;
|
||||||
z-index: 3;
|
width: 100%;
|
||||||
right: -8px;
|
|
||||||
min-height: 100%;
|
|
||||||
background-image: $borderTop;
|
|
||||||
background-repeat: repeat;
|
|
||||||
width: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%) rotate(90deg);
|
||||||
right: -10px;
|
right: -80px;
|
||||||
height: 50px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&--shown {
|
&--shown {
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
||||||
button {
|
|
||||||
width: 25px;
|
|
||||||
right: -25px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,124 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { FocusEvent, MouseEvent, useEffect, useMemo, useRef, useState, WheelEvent } from 'react'
|
||||||
|
import UIfx from 'uifx'
|
||||||
|
|
||||||
|
import { PREVIEW_HEIGHT, PREVIEW_WIDTH, UI_SOUND_VOLUME } from '../../utils'
|
||||||
|
import { useSettings } from '../../hooks'
|
||||||
|
|
||||||
import './panel-component.scss'
|
import './panel-component.scss'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
orientation: 'bottom' | 'left'
|
orientation: 'bottom' | 'left'
|
||||||
isShown: boolean
|
isShown: boolean
|
||||||
|
itemsCount: number
|
||||||
setShown: () => void
|
setShown: () => void
|
||||||
|
openSound: UIfx
|
||||||
|
closeSound: UIfx
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PanelComponent = ({orientation, isShown, setShown, children}: Props) => {
|
export const PanelComponent = ({
|
||||||
const [isRendered, setRendered] = useState(false)
|
isShown,
|
||||||
|
setShown,
|
||||||
|
children,
|
||||||
|
openSound,
|
||||||
|
closeSound,
|
||||||
|
itemsCount,
|
||||||
|
orientation
|
||||||
|
}: Props) => {
|
||||||
|
const {settings: {language, uiSound}} = useSettings()
|
||||||
|
const [isDrag, setDrag] = useState(false)
|
||||||
|
const [trackMouse, setTrackMouse] = useState(0)
|
||||||
|
const [lastPosition, setLastPosition] = useState(0)
|
||||||
|
|
||||||
|
const panel = useRef<HTMLInputElement>(null)
|
||||||
|
let position = 0
|
||||||
|
const isBottom = useMemo(() => orientation === 'bottom', [orientation])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRendered(isShown)
|
if (!isShown && panel.current) {
|
||||||
|
panel.current.style.transform = 'unset'
|
||||||
|
setLastPosition(0)
|
||||||
|
}
|
||||||
}, [isShown])
|
}, [isShown])
|
||||||
|
|
||||||
const handleClick = useCallback((event) => {
|
const handleClick = (event: MouseEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
setShown()
|
setShown()
|
||||||
}, [setShown])
|
if (!uiSound) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isShown) {
|
||||||
|
openSound.play(UI_SOUND_VOLUME)
|
||||||
|
} else {
|
||||||
|
closeSound.play(UI_SOUND_VOLUME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDragScroll = (e: MouseEvent) => {
|
||||||
|
if (!isDrag) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const {clientX, clientY} = e
|
||||||
|
const value = isBottom ? clientX : clientY
|
||||||
|
position = trackMouse - value + lastPosition
|
||||||
|
changePosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
const changePosition = () => {
|
||||||
|
if (!panel.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const {innerHeight, innerWidth} = window
|
||||||
|
const overflowWindow = isBottom ? innerWidth : innerHeight
|
||||||
|
const overflowContainer = itemsCount * ((isBottom ? PREVIEW_WIDTH : PREVIEW_HEIGHT) + 10)
|
||||||
|
const overflow = overflowContainer > overflowWindow ? overflowContainer : overflowWindow
|
||||||
|
if (Math.abs(position) > overflow) {
|
||||||
|
position = -position
|
||||||
|
}
|
||||||
|
panel.current.style.transform = `translate${isBottom ? 'X' : 'Y'}(${-position}px)`
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePress = (e: MouseEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setTrackMouse(isBottom ? e.clientX : e.clientY)
|
||||||
|
setDrag(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFree = (e: MouseEvent | FocusEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setDrag(false)
|
||||||
|
setLastPosition(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleScroll = (e: WheelEvent) => {
|
||||||
|
const {deltaY} = e
|
||||||
|
const value = deltaY > 0 ? 50 : -50
|
||||||
|
position = value + lastPosition
|
||||||
|
changePosition()
|
||||||
|
setLastPosition(position)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
onMouseDown={handlePress}
|
||||||
|
onMouseUp={handleFree}
|
||||||
|
onMouseMove={handleDragScroll}
|
||||||
|
onMouseLeave={handleFree}
|
||||||
|
onWheel={handleScroll}
|
||||||
|
onBlur={handleFree}
|
||||||
className={`panel panel--${orientation} ${isShown ? `panel--${orientation}--shown` : ''}`}
|
className={`panel panel--${orientation} ${isShown ? `panel--${orientation}--shown` : ''}`}
|
||||||
>
|
>
|
||||||
<div className='panel-border'/>
|
<div
|
||||||
{isRendered && children}
|
ref={panel}
|
||||||
<button onClick={handleClick}/>
|
className='panel-content'
|
||||||
|
>
|
||||||
|
{isShown && children}
|
||||||
|
</div>
|
||||||
|
<button onClick={handleClick}>
|
||||||
|
{orientation === 'bottom' ? language['ui.button.views'] : language['ui.button.places']}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,33 @@
|
|||||||
@import "../../app/style";
|
@import "../../app/style";
|
||||||
|
|
||||||
|
$previewBorderWidth: 10px;
|
||||||
|
$plugSize: 90px;
|
||||||
|
|
||||||
.preview {
|
.preview {
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
cursor: pointer;
|
cursor: $cursorInteract, auto;
|
||||||
min-width: 320px;
|
min-width: $previewWidth;
|
||||||
min-height: 180px;
|
min-height: $previewHeight;
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
border: 10px double var(--foreground);
|
border: $previewBorderWidth double $fontColor;
|
||||||
border-image: $border 12 12 11 12;
|
border-image: $border 12 12 11 12;
|
||||||
border-image-width: 10px;
|
border-image-outset: $previewBorderWidth / 2;
|
||||||
border-image-outset: 5px;
|
|
||||||
border-image-repeat: stretch stretch;
|
border-image-repeat: stretch stretch;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: contain;
|
background-size: $previewWidth $previewHeight;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
transition: opacity 800ms ease-in-out;
|
opacity: 1;
|
||||||
|
transition: opacity $transitionDuration $transitionType;
|
||||||
|
|
||||||
|
&--not-loaded {
|
||||||
|
background-size: $plugSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--loading {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: $hoverColor;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
|
||||||
import { Plug } from '../../assets'
|
import { Plug } from '../../assets'
|
||||||
import './preview-component.scss'
|
import './preview-component.scss'
|
||||||
@@ -15,27 +15,39 @@ export const PreviewComponent = ({src, value, handleChange, isLoading}: Props) =
|
|||||||
const [isLoaded, setLoaded] = useState(false)
|
const [isLoaded, setLoaded] = useState(false)
|
||||||
const image = useMemo(() => {
|
const image = useMemo(() => {
|
||||||
setLoaded(false)
|
setLoaded(false)
|
||||||
const img = new Image(320, 180)
|
const img = new Image()
|
||||||
img.src = src
|
img.src = src
|
||||||
img.onload = () => {
|
|
||||||
setLoaded(true)
|
|
||||||
}
|
|
||||||
return img
|
return img
|
||||||
}, [src])
|
}, [src])
|
||||||
|
|
||||||
const handleClick = () => {
|
useEffect(() => {
|
||||||
|
image.onload = () => {
|
||||||
|
setLoaded(true)
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
image.onload = null
|
||||||
|
}
|
||||||
|
}, [image])
|
||||||
|
|
||||||
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
handleChange(value)
|
handleChange(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
onContextMenu={handleClick}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url(${isLoaded ? image.src : Plug})`,
|
backgroundImage: `url(${isLoaded ? image.src : Plug})`
|
||||||
backgroundSize: `${isLoaded ? '320px 180px' : '90px 90px'}`,
|
|
||||||
opacity: `${isLoading ? '0.4' : '1'}`
|
|
||||||
}}
|
}}
|
||||||
className='preview'
|
className={
|
||||||
|
`preview ${!isLoaded
|
||||||
|
? 'preview--not-loaded' :
|
||||||
|
''} ${isLoading
|
||||||
|
? 'preview--loading'
|
||||||
|
: ''}`
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
@import "../../app/style";
|
||||||
|
|
||||||
|
$selectWidth: 90px;
|
||||||
|
$selectHeight: 30px;
|
||||||
|
|
||||||
|
$selectBackgroundWidth: $selectWidth - 5px;
|
||||||
|
$selectBackgroundHeight: $selectHeight + 15px;
|
||||||
|
|
||||||
|
$DropDownBorder: 10px;
|
||||||
|
$dropDownOutset: $DropDownBorder / 2;
|
||||||
|
|
||||||
|
.select {
|
||||||
|
color: $fontColorWhite;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 6px;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-radius: 8px;
|
||||||
|
align-items: center;
|
||||||
|
width: $selectWidth;
|
||||||
|
height: $selectHeight;
|
||||||
|
background-image: $selectBorder;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: $selectBackgroundWidth $selectBackgroundHeight;
|
||||||
|
background-position: center top;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
z-index: -1;
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
left: 2px;
|
||||||
|
width: 75px;
|
||||||
|
height: 18px;
|
||||||
|
background-image: $panelBackground;
|
||||||
|
background-repeat: repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
.select-arrow {
|
||||||
|
background-image: $selectArrowClick;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: inset $hoverColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-arrow {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background-image: $selectArrow;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-drop-down {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: $selectWidth + 10px;
|
||||||
|
top: $selectHeight + 10px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
border: $dropDownOutset double $fontColor;
|
||||||
|
border-image: $border 13 13 13 13;
|
||||||
|
border-image-width: $DropDownBorder;
|
||||||
|
border-image-outset: $dropDownOutset;
|
||||||
|
border-image-repeat: stretch stretch;
|
||||||
|
background-color: $settingsBackground;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-item {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 17px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
rgba(173, 154, 32, 0.1) 0%,
|
||||||
|
rgba(173, 154, 32, 0.5) 25%,
|
||||||
|
rgba(173, 154, 32, 0.5) 75%,
|
||||||
|
rgba(173, 154, 32, 0.1) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
rgba(173, 154, 32, 0.1) 0%,
|
||||||
|
rgba(173, 154, 32, 0.5) 25%,
|
||||||
|
rgba(173, 154, 32, 0.5) 75%,
|
||||||
|
rgba(173, 154, 32, 0.1) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: $radio;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--selected {
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: $radio 48px, $radio;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import { FocusEvent, KeyboardEvent, MouseEvent, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
import './select-component.scss'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: React.ReactNode
|
||||||
|
options: any[]
|
||||||
|
current: any
|
||||||
|
handleChange: (value: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SelectComponent = ({children, options, current, handleChange}: Props) => {
|
||||||
|
const [isSelectShown, setSelectShown] = useState(false)
|
||||||
|
const dropDownRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const handleSelectClick = (e: MouseEvent) => {
|
||||||
|
if (dropDownRef && dropDownRef.current && dropDownRef.current.contains(e.target as Node)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setSelectShown(!isSelectShown)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBlur = (e: FocusEvent) => {
|
||||||
|
if (!dropDownRef || !dropDownRef.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (dropDownRef.current.contains(e.relatedTarget as Node) ||
|
||||||
|
e.currentTarget === e.relatedTarget) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setSelectShown(!isSelectShown)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.keyCode !== 13 && e.keyCode !== 32) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (dropDownRef && dropDownRef.current && dropDownRef.current.contains(e.target as Node)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setSelectShown(!isSelectShown)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onItemClick = (itemValue: any) => handleChange(itemValue)
|
||||||
|
|
||||||
|
const onItemKeyDown = (e: KeyboardEvent, itemValue: any) => {
|
||||||
|
if (e.keyCode !== 13 && e.keyCode !== 32) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handleChange(itemValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={handleSelectClick}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
tabIndex={0}
|
||||||
|
className='select'
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<div className='select-arrow'/>
|
||||||
|
{isSelectShown && (
|
||||||
|
<div
|
||||||
|
ref={dropDownRef}
|
||||||
|
className='select-drop-down'
|
||||||
|
>
|
||||||
|
{options.map((item, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => onItemClick(item)}
|
||||||
|
onKeyDown={(e) => onItemKeyDown(e, item)}
|
||||||
|
className={`select-item ${item === current
|
||||||
|
?
|
||||||
|
'select-item--selected'
|
||||||
|
:
|
||||||
|
''}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectComponent.displayName = 'SelectComponent'
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
@import "../../app/style";
|
||||||
|
|
||||||
|
$settingsWidth: 182px;
|
||||||
|
$settingsHeight: 315px;
|
||||||
|
$settingsBorderWidth: 12px;
|
||||||
|
$settingsBorderOutset: $settingsBorderWidth - 4px;
|
||||||
|
|
||||||
|
$settingsHeaderWidth: 132px;
|
||||||
|
$settingsHeaderHeight: 25px;
|
||||||
|
|
||||||
|
.settings {
|
||||||
|
z-index: 5;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
border: $settingsBorderWidth double $fontColor;
|
||||||
|
border-image: $border 15 15 15 15 fill;
|
||||||
|
border-image-outset: $settingsBorderOutset;
|
||||||
|
border-image-repeat: stretch stretch;
|
||||||
|
font-family: $font;
|
||||||
|
font-size: $fontSize;
|
||||||
|
text-shadow: $fontShadow;
|
||||||
|
letter-spacing: $fontSpacing;
|
||||||
|
width: $settingsWidth;
|
||||||
|
height: $settingsHeight;
|
||||||
|
background-color: $settingsBackground;
|
||||||
|
|
||||||
|
&-header {
|
||||||
|
z-index: 6;
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
color: $fontColor;
|
||||||
|
top: -$settingsHeaderHeight;
|
||||||
|
width: $settingsHeaderWidth;
|
||||||
|
height: $settingsHeaderHeight;
|
||||||
|
border: $settingsBorderWidth double $fontColor;
|
||||||
|
border-image: $border 15 15 15 15 fill;
|
||||||
|
border-image-outset: $settingsBorderOutset;
|
||||||
|
border-image-repeat: stretch stretch;
|
||||||
|
background-image: $backgroundTexture;
|
||||||
|
background-repeat: repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-content {
|
||||||
|
color: $fontColor;
|
||||||
|
margin-top: 10px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
grid-gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-option {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-button {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import UIfx from 'uifx'
|
||||||
|
|
||||||
|
import { useSettings } from '../../hooks'
|
||||||
|
|
||||||
|
import ru from '../../locales/ru.json'
|
||||||
|
import en from '../../locales/en.json'
|
||||||
|
|
||||||
|
import './settings-component.scss'
|
||||||
|
import { Settings } from '../../settings-context'
|
||||||
|
import { UI_SOUND_VOLUME } from '../../utils'
|
||||||
|
import { CheckBoxComponent, SelectComponent } from '..'
|
||||||
|
|
||||||
|
type languageValue = keyof typeof ru;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
closeSettings: () => void
|
||||||
|
checkboxOnSound: UIfx
|
||||||
|
checkboxOffSound: UIfx
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SettingsComponent = ({closeSettings, checkboxOnSound, checkboxOffSound}: Props) => {
|
||||||
|
const {settings, saveSettings} = useSettings()
|
||||||
|
|
||||||
|
const handleCheckboxClick = (option: keyof Settings) => {
|
||||||
|
saveSettings!({...settings, [option]: !settings[option]})
|
||||||
|
if (!settings.uiSound) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (settings[option]) {
|
||||||
|
checkboxOffSound.play(UI_SOUND_VOLUME)
|
||||||
|
} else {
|
||||||
|
checkboxOnSound.play(UI_SOUND_VOLUME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChangeLanguage = (nextLanguage: string) => {
|
||||||
|
if (settings.currentLanguage === nextLanguage) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (nextLanguage === ru['ui.language']) {
|
||||||
|
saveSettings!({...settings, language: ru, currentLanguage: nextLanguage})
|
||||||
|
} else {
|
||||||
|
saveSettings!({...settings, language: en, currentLanguage: nextLanguage})
|
||||||
|
}
|
||||||
|
if (!settings.uiSound) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
checkboxOnSound.play(UI_SOUND_VOLUME)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderOption = (option: keyof Settings, value: boolean | string[] = []) => {
|
||||||
|
const {language} = settings
|
||||||
|
const valueName = `ui.${option}` as languageValue
|
||||||
|
return (
|
||||||
|
<div className='settings-option'>
|
||||||
|
<div className='settings-option-name'>
|
||||||
|
{language[valueName]}
|
||||||
|
</div>
|
||||||
|
{typeof value !== 'boolean' ? (
|
||||||
|
<SelectComponent
|
||||||
|
handleChange={handleChangeLanguage}
|
||||||
|
current={language['ui.language']}
|
||||||
|
options={settings[option] as []}
|
||||||
|
>
|
||||||
|
{language['ui.language']}
|
||||||
|
</SelectComponent>
|
||||||
|
) : (
|
||||||
|
<CheckBoxComponent
|
||||||
|
handleClick={handleCheckboxClick}
|
||||||
|
optionName={option}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='settings'>
|
||||||
|
<div className='settings-header'>
|
||||||
|
{settings.language['ui.main-menu']}
|
||||||
|
</div>
|
||||||
|
<div className='settings-content'>
|
||||||
|
{renderOption('uiSound', settings.uiSound)}
|
||||||
|
{renderOption('uiLanguage', settings.uiLanguage)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className='settings-button'
|
||||||
|
onClick={closeSettings}
|
||||||
|
>
|
||||||
|
{settings.language['ui.button.close']}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsComponent.displayName = 'SettingsComponent'
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@import "../../app/style";
|
||||||
|
|
||||||
.view {
|
.view {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -6,7 +8,8 @@
|
|||||||
min-width: 100vw;
|
min-width: 100vw;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
transition: background-image 500ms ease-in;
|
background-repeat: no-repeat;
|
||||||
|
transition: background-image $transitionDuration $transitionType;
|
||||||
animation: pulse 10s infinite;
|
animation: pulse 10s infinite;
|
||||||
animation-direction: alternate;
|
animation-direction: alternate;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { Background } from '../../assets'
|
import { Background } from '../../assets'
|
||||||
|
import { ANIMATION_DURATION } from '../../utils'
|
||||||
|
|
||||||
import './view-component.scss'
|
import './view-component.scss'
|
||||||
|
|
||||||
@@ -11,25 +12,25 @@ interface Props {
|
|||||||
|
|
||||||
export const ViewComponent = ({src}: Props) => {
|
export const ViewComponent = ({src}: Props) => {
|
||||||
const [imageSrc, setImageSrc] = useState(Background)
|
const [imageSrc, setImageSrc] = useState(Background)
|
||||||
const isImage = useMemo(() => imageSrc !== Background, [imageSrc])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
const image = new Image()
|
const image = new Image()
|
||||||
image.src = src
|
image.src = src
|
||||||
image.onload = () => {
|
image.onload = () => {
|
||||||
setImageSrc(src)
|
setImageSrc(src)
|
||||||
}
|
}
|
||||||
}, 500)
|
}, ANIMATION_DURATION)
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer)
|
||||||
|
}
|
||||||
}, [src])
|
}, [src])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='view'
|
className='view'
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url(${imageSrc})`,
|
backgroundImage: `url(${imageSrc})`
|
||||||
backgroundRepeat: `${isImage ? 'no-repeat' : 'repeat'}`,
|
|
||||||
backgroundSize: `${isImage ? 'cover' : '256px'}`
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { useSettings } from './use-settings'
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { useContext } from 'react'
|
||||||
|
import SettingsContext from '../settings-context'
|
||||||
|
|
||||||
|
export const useSettings = () => useContext(SettingsContext)
|
||||||
@@ -1,6 +1,22 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import ReactDom from 'react-dom'
|
import ReactDom from 'react-dom'
|
||||||
|
|
||||||
|
import { Settings, SettingsProvider } from './settings-context'
|
||||||
import App from './app'
|
import App from './app'
|
||||||
|
|
||||||
ReactDom.render(<App/>, document.getElementById('root'))
|
import ru from './locales/ru.json'
|
||||||
|
import en from './locales/en.json'
|
||||||
|
|
||||||
|
const defaultSettings: Settings = {
|
||||||
|
language: ru,
|
||||||
|
currentLanguage: ru['ui.language'],
|
||||||
|
uiLanguage: [ru['ui.language'], en['ui.language']],
|
||||||
|
uiSound: true
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactDom.render(
|
||||||
|
<SettingsProvider settings={defaultSettings}>
|
||||||
|
<App/>
|
||||||
|
</SettingsProvider>,
|
||||||
|
document.getElementById('root')
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ui.button.places": "Places",
|
||||||
|
"ui.button.views": "View",
|
||||||
|
"ui.button.close": "Close",
|
||||||
|
"ui.main-menu": "Main Menu",
|
||||||
|
"ui.uiSound": "UI Sound",
|
||||||
|
"ui.uiLanguage": "Language",
|
||||||
|
"ui.language": "English"
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ui.button.places": "Места",
|
||||||
|
"ui.button.views": "Виды",
|
||||||
|
"ui.button.close": "Закрыть",
|
||||||
|
"ui.main-menu": "Главное меню",
|
||||||
|
"ui.uiSound": "Звуки интерфейса",
|
||||||
|
"ui.uiLanguage": "Язык",
|
||||||
|
"ui.language": "Русский"
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import { createContext, useState } from 'react'
|
||||||
|
|
||||||
|
import ru from './locales/ru.json'
|
||||||
|
import en from './locales/en.json'
|
||||||
|
|
||||||
|
export interface Settings {
|
||||||
|
language: typeof ru | typeof en
|
||||||
|
currentLanguage: string
|
||||||
|
uiLanguage: string[]
|
||||||
|
uiSound: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingsContextType {
|
||||||
|
settings: Settings
|
||||||
|
saveSettings?: (value: Settings) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: React.ReactNode
|
||||||
|
settings: Settings
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultSettings: SettingsContextType = {
|
||||||
|
settings: {
|
||||||
|
language: ru,
|
||||||
|
currentLanguage: ru['ui.language'],
|
||||||
|
uiLanguage: [ru['ui.language'], en['ui.language']],
|
||||||
|
uiSound: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const SettingsContext = createContext(defaultSettings)
|
||||||
|
|
||||||
|
export const SettingsProvider = ({children, settings}: Props) => {
|
||||||
|
const [currentSettings, setCurrentSettings] = useState(settings || defaultSettings)
|
||||||
|
|
||||||
|
const saveSettings = (value: Settings) => {
|
||||||
|
setCurrentSettings(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsContext.Provider
|
||||||
|
value={{settings: currentSettings, saveSettings}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SettingsContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SettingsContext
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export const ANIMATION_DURATION = 500
|
||||||
|
export const LOADING_DURATION = 800
|
||||||
|
export const PREVIEW_WIDTH = 320
|
||||||
|
export const PREVIEW_HEIGHT = 180
|
||||||
|
export const UI_SOUND_VOLUME = 0.2
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { LOADING_DURATION } from './constants'
|
||||||
|
|
||||||
|
export {
|
||||||
|
PREVIEW_WIDTH,
|
||||||
|
PREVIEW_HEIGHT,
|
||||||
|
UI_SOUND_VOLUME,
|
||||||
|
LOADING_DURATION,
|
||||||
|
ANIMATION_DURATION
|
||||||
|
} from './constants'
|
||||||
|
export const delay = () => new Promise(resolve => setTimeout(resolve, LOADING_DURATION))
|
||||||