Add scroll, languages, settings

This commit is contained in:
obergodmar
2020-06-25 03:20:33 +03:00
parent 98a307f2cc
commit 8647adc66c
47 changed files with 946 additions and 120 deletions
+6
View File
@@ -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",
+3 -2
View File
@@ -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",
+27 -2
View File
File diff suppressed because one or more lines are too long
+81 -5
View File
@@ -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>
) )
} }
+56 -16
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

+1 -1
View File
File diff suppressed because one or more lines are too long
+9 -1
View File
@@ -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
] ]
Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

+9 -1
View File
@@ -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
] ]
Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

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'
+3
View File
@@ -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'}`
}} }}
/> />
) )
+1
View File
@@ -0,0 +1 @@
export { useSettings } from './use-settings'
+4
View File
@@ -0,0 +1,4 @@
import { useContext } from 'react'
import SettingsContext from '../settings-context'
export const useSettings = () => useContext(SettingsContext)
+17 -1
View File
@@ -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')
)
+9
View File
@@ -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"
}
+9
View File
@@ -0,0 +1,9 @@
{
"ui.button.places": "Места",
"ui.button.views": "Виды",
"ui.button.close": "Закрыть",
"ui.main-menu": "Главное меню",
"ui.uiSound": "Звуки интерфейса",
"ui.uiLanguage": "Язык",
"ui.language": "Русский"
}
+50
View File
@@ -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
+5
View File
@@ -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
+10
View File
@@ -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))