Add main menu; add panel fixes
This commit is contained in:
+1
-3
@@ -29,9 +29,7 @@
|
||||
"since 2010"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
"since 2010"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+19
-1
@@ -1,7 +1,15 @@
|
||||
import * as React from 'react'
|
||||
import { KeyboardEvent, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { MusicComponent, PanelComponent, PreviewComponent, SettingsComponent, ViewComponent } from '../components'
|
||||
import {
|
||||
MainMenuComponent,
|
||||
MenuItemComponent,
|
||||
MusicComponent,
|
||||
PanelComponent,
|
||||
PreviewComponent,
|
||||
SettingsComponent,
|
||||
ViewComponent
|
||||
} from '../components'
|
||||
import places from '../assets'
|
||||
import { delay, soundLoad, UI_SOUND_VOLUME } from '../utils'
|
||||
import { useSettings } from '../hooks'
|
||||
@@ -131,6 +139,16 @@ export default function App() {
|
||||
className='main'
|
||||
>
|
||||
<ViewComponent src={places[activePlace].view[activeView]}/>
|
||||
<MainMenuComponent>
|
||||
<div className='author'>
|
||||
<a href="https://github.com/obergodmar">obergodmar</a>
|
||||
<span>v1.2.0</span>
|
||||
</div>
|
||||
<MenuItemComponent
|
||||
isActive={isSettingsShown}
|
||||
handleClick={openCloseSettings}
|
||||
/>
|
||||
</MainMenuComponent>
|
||||
<PanelComponent
|
||||
openSoundPlay={panelOpenSound.playSound}
|
||||
closeSoundPlay={panelCloseSound.playSound}
|
||||
|
||||
@@ -33,6 +33,30 @@ body {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.author {
|
||||
padding: 0 10px;
|
||||
width: 130px;
|
||||
font-family: $font;
|
||||
text-shadow: $fontShadow;
|
||||
color: $fontColor;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
a {
|
||||
cursor: $cursorPointer, auto;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: $hoverBox;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
|
||||
@@ -25,3 +25,5 @@ $radio: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAQCAYAAACm53kpA
|
||||
$rangeBorder: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIoAAAAQCAYAAADJeudBAAAABmJLR0QAKAAqADbykokVAAABBUlEQVRo3u3asW2DQBiG4RfkghG49qp4hLjGA3BI3gNdEUWurYjCyh5I2APAHqQxc9CRBmySULhJxfdUCKh+vbqTThfw8AYkiDw0QAGwmUfic69Q5O78eZ4ei808kt3rjuyQdRqRVGVlgWQWC7XP/VCV1QDcgFpjWr0auFVlNfjcD0AdAhhjppWkA/aa0+rtgS47ZJ0xBoBw4QeRPy2Emoc8Q6HI86G0batJyKKpDa0ooq1H/i8UnaHIYgshQBzHuNRZwCoWGRuwLnU2jmMAAsYj/OP7MWm/Wi7Xi47wBZc6u33Zcvo4NUATjO9/xNL3vSa1YlEUMY8EKILZd10zkN/u1wy+AWM3S3inoVC2AAAAAElFTkSuQmCC');
|
||||
$range: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAB+ElEQVR4nO2VP2gTURzHP6kOJ1nurR1vcwghOaHERRuuoygGOjQIFeUQddOhq5OtZBShXdO0EIl26ImQs+Io2kBSsVtwu3a6Gxo5muEc8g4zONy7kizmA8e993jv9/vy4/cHZsz437l0gbcCmJfrcNoCDOAWYMq1Lz9l5lI6twCz862zLkVY8nziAkTsfOPlhg3QarbsMRFiGgJE7DxGihBpBKjmgAGY7ic3zGQyxpK1RBAEVJYrLqMc6AOeikGVCAjA1PSSWFldsx49eUbh2lVCNBpNxwJdgG6SIgpJMYH1ldW1qPX+Y6Tppbaml9qgtxtNJ2o0nQj0OCkTo1wFlds3qd5/4QLW3Ts3rIXSglVdrrqqdlILGOfd3peLPAfgsuJ98cFxCINjCsU8AOHgBAjwfh0DwcQECGSj6XR6vH5Ts54+fi7DHlB7VbO6va6yc4BMwntGNpu1B4OBD7pRKObtBw/vARCeBXR7Xerb9bfAIeDKfyIS94HhcOgBPmjzJ95p6HmnRrGY5/D7V+rbdZdR/QvpPHEvSCog/Gs0NMqL1+0r2hzh7zPOh+fkcjnj6MeRC+wDP1GYjqpV4AP+weeDrfHDnd2dLUZdsE/KqaiCAdjAZnmxHAGbcp9qGiZNwn+JiKefzyjx+iltpSYuzYn1/hkzZkyFP182nWrkPhvCAAAAAElFTkSuQmCC');
|
||||
$rangeBackground: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAFUlEQVR4nGNkYGDoYMADmPBJDh8FAJNoAJjpM54wAAAAAElFTkSuQmCC');
|
||||
$helpButton: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAApCAMAAABjq9sOAAABp1BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAACAgIDAgLDQsODQsODwsOEA4TDQsTDw4WFhMWFxYYGBgYHBgZGBYbGRYbGxYbHBsbHRsbHxshGBAhHxshIBghICEhJCEkIRskIx4kIyEmEwsmGxYmHRsmIx4mJRsmJSEpGBgpICEpJCEpKCEpKCkpLCksKyYxFBAxGBAxHBgxJBgxLCkxLSw3GBA3MzE3NzQ5GBM5KCE5LCk5OzlCQTlHRkJKExBKQUJKRUJKSUJKTUpNDxBNEAhNHQ1STUpSUUpSU1JSVVJVExBYV1JdEAtdFBBjBAhjCABjCwhjEAhlY2BoFQ1oZ2hrDAhrDQhrFAhrFBBraWNwJRtwOClzCAhzKBBzb257DAh7EAh7dWt7dXN7eXt7fXuEAACEfXuEgnuJDAWJDAiMCACMKh6RIBaUjoyXKBuaLB6cmpyfKxinKyGtpqWwEwuwJx6wWUG1BAC1CAC4NSO9bUrIRTTLLSHLQS/OXUrTSjneSTHee2XvKCHveVrvmIH3EAj3MCn3XUr/WUL/aVL/tpzPGTKNAAAACHRSTlMAESIzVYjM7umjQSUAAAIfSURBVDjLhdT7W9JQGMDxMZw7k25aUWqCsATXSior7aYIRasIAiIw5NK9GBia3TAvtQGK/tG9Z7MaZwO/vwDP++GMhz17KQqy0XaLaBt1EN3HWNZH/533s5b16wLm6Eup9AZ6C72GPkCrq891YYPvl0rIIk3YKJph3+UQmr9LdNPh+PSRZWjKzrCPEMrN3yN6BuIJy9h1kEP8KSKHwwh4vid4zE0dHz4NnYNeQlch3+AgkkwgnU5rIG8CwxjcSn9ttdbWfu01vufzPp8ZvIJ5C+Z7jUYjf8cAQigwMjI9Lcs7ULVQ/dncbyrF8wMDDztAXP4B88/VQqGqNPeVByYgx+XNzZ1iAcC2FYjLslyEMFAUZbvjEtz1sbGnUCYTFa8tLantdrt2e+IEChvBfQ2IIszV9lYtNUGCcUgUxeg3VVVrqVQ3EM3AXE0RQMIA7hd/IVip1Ov12AvAl84e/fdHaUDAoFKplMvl2HgXIAQPAXwwm83GYrErJAjDzZrCvyEDLULwVjABQRAALK/v7m4s4nmXE5Z/64DnzSfw+ITM+sbKiukSIS4wGvBOeieT75PJSCSS8Hv8/otnjnEhAxj1QhgkEokZD2QNkhjMuDwulxm43V636yA4oOMSEnfD6XQTXR4aQv8B53SeJIK5DuDpnl3gOO4IEcznQvjpxvthAQsimM9p+0HbQLOSFIZCkAThV/ho2EE9d9ThW673nvwDLWrF2ZF5DfoAAAAASUVORK5CYII=');
|
||||
$helpButtonClick: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAApCAMAAABjq9sOAAABPlBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAwMIAAALAAAOAAAQAAAQBAAYAAAZAwAbAAAeHh4hAAAhICEpAAAsCQUsEw4xBAMxBwMxMDE0MjE3AAA3BAM3MzE8OzxCAABCBABCBQVCFhBCQTlEEAtKBABKBQNKCABKCANKGBBKQUJKRTlKRUJKSUJKTUpMGBNPEwtSAABSCABSGBBSTUpSUUpSU1JSVVJYJRtYV1JaWVpbDQVdDQhjCAVjCAhoFw5oZ2hrCAhuCAVuIxlwGxNzHBBzKyFzb257dWt7dXN7eXt7fXuBIBaBMSaDCwWDDAWEFAiEHxiEOCmEfXuEgnuHKRuJPjGPHRWUDAiUIBiXW02cNimclpScmpylDAilPCmlQTGlWUqtIBitpqW1STm1TTm9EAi9MCG9fWsZTHqDAAAACHRSTlMAESIzVYjM7umjQSUAAAGuSURBVDjLjdTpUoJgFIBhRBGiyLIyK8nIlKgosGjPirJodWmx1FIxt/u/gQ4EDX4i9f4RPA84MsPBMMiH+13CfZgVHiBcC+D2PEi6FvwRMKe+NO0WuoNuoGfo8/P8R/jgek2jXDKFD8MJ8v6Icu3jhSRwzE+Qe9SQDknCb4FweBqpHxzF4/NICw6wP+K4LQ3Zx4oLYJNJD8Cy7EUSOph1BROrF7Va7aHb7T5ZwgaiccIwfK4JlXRdr1TW1ozvdvsAz3dgnt3eLuk9EAOA53MAsgAu9UpvEIQA5N6aOzsASj0EbMBx7BSSZZlWVbXdarUkA6w4QHoZikRoAG0AkgFCThAzAU3LarVafZSkEHoH6zHLV0W1qMK8Dyi/jzqaee908kLKOhVREM1k3vN5QaCGgLEMJAjCuDWfQsEMAkZtsGmBWApKMAxj/8QWAiZNkEh4gLPXeqMxHKRTZ/V643oQiBZYOjkplwuFYxr9mw7AcXNzHoDjDED/AehBoAx79f4H4O1eFN3n66Lxdhv7QQQxggTzdXM/mBtoUVE2IRFSIOMTTh07yHNH/b3lvPfkN1HZaz2zDt62AAAAAElFTkSuQmCC');
|
||||
|
||||
@@ -7,3 +7,5 @@ export { SettingsComponent } from './settings-component/settings-component'
|
||||
export { CheckBoxComponent } from './checkbox-component/checkbox-component'
|
||||
export { BorderedHeader } from './bordered-header/bordered-header'
|
||||
export { MusicComponent } from './music-component/music-component'
|
||||
export { MainMenuComponent } from './main-menu-component/main-menu-component'
|
||||
export { MenuItemComponent } from './menu-item-component/menu-item-component'
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
@import "../../app/style";
|
||||
|
||||
$menuHeight: 42px;
|
||||
$menuWidth: 290px;
|
||||
|
||||
.main-menu {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
border: 1px double $fontColor;
|
||||
border-image: $rangeBorder 5 5 5 5;
|
||||
background: $panelBackground center repeat;
|
||||
border-image-width: 5px;
|
||||
border-image-repeat: round round;
|
||||
border-radius: 6px;
|
||||
padding: 2px 3px;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import './main-menu-component.scss'
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const MainMenuComponent = ({children}: Props) => {
|
||||
return (
|
||||
<div className='main-menu'>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
MainMenuComponent.displayName = 'MainMenuComponent'
|
||||
@@ -0,0 +1,31 @@
|
||||
@import '../../app/style';
|
||||
|
||||
$menuItemWidth: 32px;
|
||||
$menuItemHeight: 41px;
|
||||
|
||||
.menu-item {
|
||||
width: $menuItemWidth - 4px;
|
||||
height: $menuItemHeight - 4px;
|
||||
border-radius: 6px;
|
||||
background-image: $helpButton;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: $menuItemWidth $menuItemHeight;
|
||||
|
||||
&--active {
|
||||
background-image: $helpButtonClick;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: $hoverBox;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: $hoverBox;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-image: $helpButtonClick;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import * as React from 'react'
|
||||
import { KeyboardEvent } from 'react'
|
||||
|
||||
import './menu-item-component.scss'
|
||||
|
||||
interface Props {
|
||||
isActive: boolean
|
||||
handleClick: () => void
|
||||
}
|
||||
|
||||
export const MenuItemComponent = ({isActive, handleClick}: Props) => {
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.keyCode !== 13 && e.keyCode !== 32) {
|
||||
return
|
||||
}
|
||||
handleClick()
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
tabIndex={0}
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={`menu-item ${isActive ? 'menu-item--active' : ''}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
MenuItemComponent.displayName = 'MainMenuComponent'
|
||||
@@ -57,7 +57,7 @@ export const PanelComponent = ({
|
||||
}
|
||||
}
|
||||
|
||||
const resizePanel = useCallback((animate = true) => {
|
||||
const resetPanel = useCallback((animate = true) => {
|
||||
if (!panel.current) {
|
||||
return
|
||||
}
|
||||
@@ -73,7 +73,7 @@ export const PanelComponent = ({
|
||||
useEffect(() => {
|
||||
let timeout: NodeJS.Timeout
|
||||
const handleResize = debounce(() => {
|
||||
resizePanel()
|
||||
resetPanel()
|
||||
timeout = setTimeout(() => {
|
||||
if (!panel || !panel.current) {
|
||||
return
|
||||
@@ -88,33 +88,48 @@ export const PanelComponent = ({
|
||||
}
|
||||
window.removeEventListener('resize', handleResize)
|
||||
}
|
||||
}, [panel, resizePanel])
|
||||
}, [panel, resetPanel])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isShown) {
|
||||
resizePanel(false)
|
||||
resetPanel(false)
|
||||
}
|
||||
}, [isShown, resizePanel])
|
||||
}, [isShown, resetPanel])
|
||||
|
||||
const handleDragScroll = (e: MouseEvent) => {
|
||||
if (!isDrag) {
|
||||
return
|
||||
}
|
||||
const overflow = windowOverflow()
|
||||
if (!overflow) {
|
||||
return
|
||||
}
|
||||
|
||||
const {clientX, clientY} = e
|
||||
const value = isBottom ? clientX : clientY
|
||||
const diff = limiter(value, overflow)
|
||||
setPosition(diff)
|
||||
changePosition()
|
||||
}
|
||||
|
||||
const windowOverflow = () => {
|
||||
const {innerWidth, innerHeight} = window
|
||||
const windowSize = isBottom ? innerWidth : innerHeight
|
||||
const containerSize = itemsCount * ((isBottom ? PREVIEW_WIDTH : PREVIEW_HEIGHT) + 15)
|
||||
if (!(containerSize > windowSize)) {
|
||||
return
|
||||
return 0
|
||||
}
|
||||
const overflow = Math.abs(containerSize - windowSize)
|
||||
const {clientX, clientY} = e
|
||||
const value = isBottom ? clientX : clientY
|
||||
const diff = trackMouse - value + lastPosition
|
||||
if (Math.abs(diff) > overflow + 40 || diff < 0) {
|
||||
return
|
||||
return Math.abs(containerSize - windowSize)
|
||||
}
|
||||
|
||||
const limiter = (value: number, overflow: number, isWheel: boolean = false) => {
|
||||
let diff = (!isWheel ? trackMouse : 0) - value + lastPosition
|
||||
if (Math.abs(diff) > overflow + 40) {
|
||||
diff = overflow + 40
|
||||
} else if (diff < 0) {
|
||||
diff = 0
|
||||
}
|
||||
setPosition(diff)
|
||||
changePosition()
|
||||
return diff
|
||||
}
|
||||
|
||||
const changePosition = () => {
|
||||
@@ -125,8 +140,9 @@ export const PanelComponent = ({
|
||||
}
|
||||
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
const {clientX, clientY} = e
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
setTrackMouse(isBottom ? e.clientX : e.clientY)
|
||||
setTrackMouse(isBottom ? clientX : clientY)
|
||||
setDrag(true)
|
||||
}
|
||||
|
||||
@@ -139,19 +155,14 @@ export const PanelComponent = ({
|
||||
|
||||
const handleTouchMove = (e: TouchEvent) => {
|
||||
const {touches} = e
|
||||
const {innerWidth, innerHeight} = window
|
||||
const windowSize = isBottom ? innerWidth : innerHeight
|
||||
const containerSize = itemsCount * ((isBottom ? PREVIEW_WIDTH : PREVIEW_HEIGHT) + 15)
|
||||
if (!(containerSize > windowSize)) {
|
||||
return
|
||||
}
|
||||
const overflow = Math.abs(containerSize - windowSize)
|
||||
const {clientX, clientY} = touches[0]
|
||||
const value = isBottom ? clientX : clientY
|
||||
const diff = trackMouse - value + lastPosition
|
||||
if (Math.abs(diff) > overflow + 40 || diff < 0) {
|
||||
const overflow = windowOverflow()
|
||||
if (!overflow) {
|
||||
return
|
||||
}
|
||||
|
||||
const value = isBottom ? clientX : clientY
|
||||
const diff = limiter(value, overflow)
|
||||
setPosition(diff)
|
||||
changePosition()
|
||||
}
|
||||
@@ -164,17 +175,13 @@ export const PanelComponent = ({
|
||||
|
||||
const handleScroll = (e: WheelEvent) => {
|
||||
const {deltaY} = e
|
||||
const {innerWidth, innerHeight} = window
|
||||
const windowSize = isBottom ? innerWidth : innerHeight
|
||||
const containerSize = itemsCount * ((isBottom ? PREVIEW_WIDTH : PREVIEW_HEIGHT) + 15)
|
||||
if (!(containerSize > windowSize)) {
|
||||
return
|
||||
}
|
||||
const overflow = Math.abs(containerSize - windowSize)
|
||||
const diff = (deltaY > 0 ? 80 : -80) + lastPosition
|
||||
if (Math.abs(diff) > overflow + 40 || diff < 0) {
|
||||
const overflow = windowOverflow()
|
||||
if (!overflow) {
|
||||
return
|
||||
}
|
||||
|
||||
const value = deltaY > 0 ? 80 : -80
|
||||
const diff = limiter(value, overflow, true)
|
||||
setPosition(diff)
|
||||
changePosition()
|
||||
setLastPosition(position)
|
||||
|
||||
@@ -56,7 +56,7 @@ export const RangeComponent = ({handleChange, defaultValue}: Props) => {
|
||||
}
|
||||
const {width, left} = (stick.current.parentNode as HTMLDivElement).getBoundingClientRect()
|
||||
const {clientX} = e
|
||||
const diff = clientX - left
|
||||
const diff = clientX - left - 20
|
||||
if (diff > width - 35 || diff < 0) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -25,34 +25,6 @@
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&-author {
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
width: 110px;
|
||||
font-family: $font;
|
||||
text-shadow: $fontShadow;
|
||||
color: $fontColor;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
opacity: .3;
|
||||
|
||||
a {
|
||||
cursor: $cursorPointer, auto;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: $hoverBox;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
@@ -37,10 +37,6 @@ export const ViewComponent = ({src}: Props) => {
|
||||
}}
|
||||
>
|
||||
<div className={`view-background ${isLoaded ? 'view-background--loaded' : ''}`}/>
|
||||
<div className='view-author'>
|
||||
<a href="https://github.com/obergodmar">obergodmar</a>
|
||||
<span>1.1.0</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+4
-7
@@ -20,15 +20,12 @@ export const randomNumber = (min: number, max: number) => (
|
||||
Math.floor(Math.random() * (max - min)) + min
|
||||
)
|
||||
|
||||
export const debounce = (fn: () => any, ms: number) => {
|
||||
let timer: NodeJS.Timeout | null
|
||||
return () => {
|
||||
export function debounce(fn: (args: any) => any, ms: number) {
|
||||
let timer: NodeJS.Timeout
|
||||
return (...args: any) => {
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
timer = setTimeout(() => {
|
||||
timer = null
|
||||
fn()
|
||||
}, ms)
|
||||
timer = setTimeout(() => fn.apply(this, args), ms)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"downlevelIteration": true,
|
||||
"noImplicitThis": false,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
|
||||
Reference in New Issue
Block a user