Add scroll, languages, settings
@@ -1,20 +1,41 @@
|
||||
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 { 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'
|
||||
|
||||
const delay = () => new Promise(resolve => setTimeout(resolve, 800))
|
||||
|
||||
export default function App() {
|
||||
const {settings} = useSettings()
|
||||
const [isSettingsShown, setSettingsShown] = useState(false)
|
||||
const [isLoading, setLoading] = useState(false)
|
||||
const [isLeftPanelShown, setLeftPanelShown] = useState(false)
|
||||
const [isBottomPanelShown, setBottomPanelShown] = useState(false)
|
||||
const [activePlace, setActivePlace] = 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(() => {
|
||||
setBottomPanelShown(false)
|
||||
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 (
|
||||
<div className='main'>
|
||||
<div
|
||||
ref={app}
|
||||
onKeyDown={handleOpenSettings}
|
||||
tabIndex={0}
|
||||
className='main'
|
||||
>
|
||||
<ViewComponent src={places[activePlace].view[activeView]}/>
|
||||
<PanelComponent
|
||||
openSound={panelOpenSound}
|
||||
closeSound={panelCloseSound}
|
||||
itemsCount={places.length || 0}
|
||||
orientation='left'
|
||||
isShown={isLeftPanelShown}
|
||||
setShown={handleHideLeftPanel}
|
||||
@@ -59,6 +124,9 @@ export default function App() {
|
||||
))}
|
||||
</PanelComponent>
|
||||
<PanelComponent
|
||||
openSound={panelOpenSound}
|
||||
closeSound={panelCloseSound}
|
||||
itemsCount={places[activePlace].preview.length || 0}
|
||||
orientation='bottom'
|
||||
isShown={isBottomPanelShown}
|
||||
setShown={handleHideBottomPanel}
|
||||
@@ -73,6 +141,14 @@ export default function App() {
|
||||
/>
|
||||
))}
|
||||
</PanelComponent>
|
||||
{isSettingsShown && (
|
||||
<SettingsComponent
|
||||
closeSettings={openCloseSettings}
|
||||
checkboxOnSound={checkboxOnSound}
|
||||
checkboxOffSound={checkboxOffSound}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</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 StormwindPark4 from './stormwind-park-4.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 [
|
||||
StormwindPark1,
|
||||
StormwindPark2,
|
||||
StormwindPark3,
|
||||
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 StormwindPark4 from './stormwind-park-4.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 [
|
||||
StormwindPark1,
|
||||
StormwindPark2,
|
||||
StormwindPark3,
|
||||
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 { SelectComponent } from './select-component/select-component'
|
||||
export { PanelComponent } from './panel-component/panel-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";
|
||||
|
||||
$panelWidth: $previewWidth + 40px;
|
||||
$panelHeight: $previewHeight + 40px;
|
||||
|
||||
$panelBorderSize: 8px;
|
||||
|
||||
.panel {
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
border: none;
|
||||
position: absolute;
|
||||
height: 220px;
|
||||
width: 360px;
|
||||
height: $panelHeight;
|
||||
width: $panelWidth;
|
||||
background-image: $panelBackground;
|
||||
background-repeat: repeat;
|
||||
|
||||
&-content {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
button {
|
||||
z-index: 3;
|
||||
cursor: pointer;
|
||||
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: -220px;
|
||||
bottom: -$panelHeight + $panelBorderSize;
|
||||
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 {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
top: -10px;
|
||||
min-width: 100%;
|
||||
background-image: $borderRight;
|
||||
background-repeat: repeat;
|
||||
height: 16px;
|
||||
.panel-content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
button {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
top: -10px;
|
||||
width: 50px;
|
||||
top: -$buttonHeight - 5px;
|
||||
}
|
||||
|
||||
&--shown {
|
||||
bottom: 0;
|
||||
|
||||
button {
|
||||
height: 25px;
|
||||
top: -25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--left {
|
||||
left: -360px;
|
||||
flex-direction: column;
|
||||
left: -$panelWidth + $panelBorderSize;
|
||||
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 {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
right: -8px;
|
||||
min-height: 100%;
|
||||
background-image: $borderTop;
|
||||
background-repeat: repeat;
|
||||
width: 16px;
|
||||
.panel-content {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: -10px;
|
||||
height: 50px;
|
||||
transform: translateY(-50%) rotate(90deg);
|
||||
right: -80px;
|
||||
}
|
||||
|
||||
&--shown {
|
||||
left: 0;
|
||||
|
||||
button {
|
||||
width: 25px;
|
||||
right: -25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,124 @@
|
||||
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'
|
||||
|
||||
interface Props {
|
||||
orientation: 'bottom' | 'left'
|
||||
isShown: boolean
|
||||
itemsCount: number
|
||||
setShown: () => void
|
||||
openSound: UIfx
|
||||
closeSound: UIfx
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const PanelComponent = ({orientation, isShown, setShown, children}: Props) => {
|
||||
const [isRendered, setRendered] = useState(false)
|
||||
export const PanelComponent = ({
|
||||
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(() => {
|
||||
setRendered(isShown)
|
||||
if (!isShown && panel.current) {
|
||||
panel.current.style.transform = 'unset'
|
||||
setLastPosition(0)
|
||||
}
|
||||
}, [isShown])
|
||||
|
||||
const handleClick = useCallback((event) => {
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
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 (
|
||||
<div
|
||||
onMouseDown={handlePress}
|
||||
onMouseUp={handleFree}
|
||||
onMouseMove={handleDragScroll}
|
||||
onMouseLeave={handleFree}
|
||||
onWheel={handleScroll}
|
||||
onBlur={handleFree}
|
||||
className={`panel panel--${orientation} ${isShown ? `panel--${orientation}--shown` : ''}`}
|
||||
>
|
||||
<div className='panel-border'/>
|
||||
{isRendered && children}
|
||||
<button onClick={handleClick}/>
|
||||
<div
|
||||
ref={panel}
|
||||
className='panel-content'
|
||||
>
|
||||
{isShown && children}
|
||||
</div>
|
||||
<button onClick={handleClick}>
|
||||
{orientation === 'bottom' ? language['ui.button.views'] : language['ui.button.places']}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,33 @@
|
||||
@import "../../app/style";
|
||||
|
||||
$previewBorderWidth: 10px;
|
||||
$plugSize: 90px;
|
||||
|
||||
.preview {
|
||||
z-index: 4;
|
||||
cursor: pointer;
|
||||
min-width: 320px;
|
||||
min-height: 180px;
|
||||
cursor: $cursorInteract, auto;
|
||||
min-width: $previewWidth;
|
||||
min-height: $previewHeight;
|
||||
margin: 5px;
|
||||
border: 10px double var(--foreground);
|
||||
border: $previewBorderWidth double $fontColor;
|
||||
border-image: $border 12 12 11 12;
|
||||
border-image-width: 10px;
|
||||
border-image-outset: 5px;
|
||||
border-image-outset: $previewBorderWidth / 2;
|
||||
border-image-repeat: stretch stretch;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
background-size: $previewWidth $previewHeight;
|
||||
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 { useMemo, useState } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { Plug } from '../../assets'
|
||||
import './preview-component.scss'
|
||||
@@ -15,27 +15,39 @@ export const PreviewComponent = ({src, value, handleChange, isLoading}: Props) =
|
||||
const [isLoaded, setLoaded] = useState(false)
|
||||
const image = useMemo(() => {
|
||||
setLoaded(false)
|
||||
const img = new Image(320, 180)
|
||||
const img = new Image()
|
||||
img.src = src
|
||||
img.onload = () => {
|
||||
setLoaded(true)
|
||||
}
|
||||
return img
|
||||
}, [src])
|
||||
|
||||
const handleClick = () => {
|
||||
useEffect(() => {
|
||||
image.onload = () => {
|
||||
setLoaded(true)
|
||||
}
|
||||
return () => {
|
||||
image.onload = null
|
||||
}
|
||||
}, [image])
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
handleChange(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onContextMenu={handleClick}
|
||||
onClick={handleClick}
|
||||
style={{
|
||||
backgroundImage: `url(${isLoaded ? image.src : Plug})`,
|
||||
backgroundSize: `${isLoaded ? '320px 180px' : '90px 90px'}`,
|
||||
opacity: `${isLoading ? '0.4' : '1'}`
|
||||
backgroundImage: `url(${isLoaded ? image.src : Plug})`
|
||||
}}
|
||||
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 {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
@@ -6,7 +8,8 @@
|
||||
min-width: 100vw;
|
||||
min-height: 100vh;
|
||||
background-position: center;
|
||||
transition: background-image 500ms ease-in;
|
||||
background-repeat: no-repeat;
|
||||
transition: background-image $transitionDuration $transitionType;
|
||||
animation: pulse 10s infinite;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import * as React from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { Background } from '../../assets'
|
||||
import { ANIMATION_DURATION } from '../../utils'
|
||||
|
||||
import './view-component.scss'
|
||||
|
||||
@@ -11,25 +12,25 @@ interface Props {
|
||||
|
||||
export const ViewComponent = ({src}: Props) => {
|
||||
const [imageSrc, setImageSrc] = useState(Background)
|
||||
const isImage = useMemo(() => imageSrc !== Background, [imageSrc])
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
const timer = setTimeout(() => {
|
||||
const image = new Image()
|
||||
image.src = src
|
||||
image.onload = () => {
|
||||
setImageSrc(src)
|
||||
}
|
||||
}, 500)
|
||||
}, ANIMATION_DURATION)
|
||||
return () => {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}, [src])
|
||||
|
||||
return (
|
||||
<div
|
||||
className='view'
|
||||
style={{
|
||||
backgroundImage: `url(${imageSrc})`,
|
||||
backgroundRepeat: `${isImage ? 'no-repeat' : 'repeat'}`,
|
||||
backgroundSize: `${isImage ? 'cover' : '256px'}`
|
||||
backgroundImage: `url(${imageSrc})`
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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 ReactDom from 'react-dom'
|
||||
|
||||
import { Settings, SettingsProvider } from './settings-context'
|
||||
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))
|
||||