From c7e64af0f8a82de500893adee354ceca8c5b00ce Mon Sep 17 00:00:00 2001 From: "a.muhin" Date: Sun, 20 Feb 2022 23:50:41 +0300 Subject: [PATCH 1/6] changeable keybindings --- src/App.tsx | 6 +- src/controls/Keyboard.ts | 57 +++-------- src/store.ts | 148 ++++++++++++++++++++++++++++- src/styles.css | 46 ++++++++- src/ui/Help.tsx | 200 ++++++++++++++++++++++++++++++++------- 5 files changed, 374 insertions(+), 83 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 45bb0aef..ee3ead79 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -58,9 +58,9 @@ export function App() { - - - + void} rotation={[0, 0.55, 0]} position={[-27, 1, 180]} /> + void} rotation={[0, -1.2, 0]} position={[-104, 1, -189]} /> + void} rotation={[0, -0.5, 0]} position={[-50, 1, -5]} /> diff --git a/src/controls/Keyboard.ts b/src/controls/Keyboard.ts index 74a75590..5351e9f7 100644 --- a/src/controls/Keyboard.ts +++ b/src/controls/Keyboard.ts @@ -1,34 +1,25 @@ import { useEffect } from 'react' -import { cameras, useStore } from '../store' - -interface KeyConfig extends KeyMap { - keys?: string[] -} - -interface KeyMap { - fn: (pressed: boolean) => void - up?: boolean - pressed?: boolean -} +import { useStore } from '../store' +import type { KeyConfig, KeyMap } from '../store' function useKeys(keyConfig: KeyConfig[]) { useEffect(() => { - const keyMap = keyConfig.reduce<{ [key: string]: KeyMap }>((out, { keys, fn, up = true }) => { - keys && keys.forEach((key) => (out[key] = { fn, pressed: false, up })) + const codeMap = keyConfig.reduce<{ [key: string]: KeyMap }>((out, { keys, fn, up = true }) => { + keys && keys.forEach((key) => key.values.forEach((value) => (out[value] = { fn, pressed: false, up }))) return out }, {}) - const downHandler = ({ key, target }: KeyboardEvent) => { - if (!keyMap[key] || (target as HTMLElement).nodeName === 'INPUT') return - const { fn, pressed, up } = keyMap[key] - keyMap[key].pressed = true + const downHandler = ({ code, target }: KeyboardEvent) => { + if (!codeMap[code] || (target as HTMLElement).nodeName === 'INPUT') return + const { fn, pressed, up } = codeMap[code] + codeMap[code].pressed = true if (up || !pressed) fn(true) } - const upHandler = ({ key, target }: KeyboardEvent) => { - if (!keyMap[key] || (target as HTMLElement).nodeName === 'INPUT') return - const { fn, up } = keyMap[key] - keyMap[key].pressed = false + const upHandler = ({ code, target }: KeyboardEvent) => { + if (!codeMap[code] || (target as HTMLElement).nodeName === 'INPUT') return + const { fn, up } = codeMap[code] + codeMap[code].pressed = false if (up) fn(false) } @@ -43,27 +34,7 @@ function useKeys(keyConfig: KeyConfig[]) { } export function Keyboard() { - const { reset, set } = useStore(({ actions: { reset }, set }) => ({ reset, set })) - useKeys([ - { keys: ['ArrowUp', 'w', 'W', 'z', 'Z'], fn: (forward) => set((state) => ({ controls: { ...state.controls, forward } })) }, - { keys: ['ArrowDown', 's', 'S'], fn: (backward) => set((state) => ({ controls: { ...state.controls, backward } })) }, - { keys: ['ArrowLeft', 'a', 'A', 'q', 'Q'], fn: (left) => set((state) => ({ controls: { ...state.controls, left } })) }, - { keys: ['ArrowRight', 'd', 'D'], fn: (right) => set((state) => ({ controls: { ...state.controls, right } })) }, - { keys: [' '], fn: (brake) => set((state) => ({ controls: { ...state.controls, brake } })) }, - { keys: ['h', 'H'], fn: (honk) => set((state) => ({ controls: { ...state.controls, honk } })) }, - { keys: ['Shift'], fn: (boost) => set((state) => ({ controls: { ...state.controls, boost } })) }, - { keys: ['r', 'R'], fn: reset, up: false }, - { keys: ['.'], fn: () => set((state) => ({ editor: !state.editor })), up: false }, - { keys: ['i', 'I'], fn: () => set((state) => ({ help: !state.help, leaderboard: false, pickcolor: false })), up: false }, - { keys: ['l', 'L'], fn: () => set((state) => ({ help: false, leaderboard: !state.leaderboard, pickcolor: false })), up: false }, - { keys: ['m', 'M'], fn: () => set((state) => ({ map: !state.map })), up: false }, - { keys: ['p', 'P'], fn: () => set((state) => ({ help: false, pickcolor: !state.pickcolor, leaderboard: false })), up: false }, - { keys: ['u', 'U'], fn: () => set((state) => ({ sound: !state.sound })), up: false }, - { - keys: ['c', 'C'], - fn: () => set((state) => ({ camera: cameras[(cameras.indexOf(state.camera) + 1) % cameras.length] })), - up: false, - }, - ]) + const [keyboardBindings] = useStore((state) => [state.keyboardBindings]) + useKeys(keyboardBindings) return null } diff --git a/src/store.ts b/src/store.ts index 21095b0d..0a659810 100644 --- a/src/store.ts +++ b/src/store.ts @@ -69,7 +69,7 @@ export const wheelInfo: WheelInfo = { useCustomSlidingRotationalSpeed: true, } -const actionNames = ['onCheckpoint', 'onFinish', 'onStart', 'reset'] as const +const actionNames = ['onCheckpoint', 'onFinish', 'onStart', 'reset', 'addKeyBinding', 'removeKeyBinding'] as const export type ActionNames = typeof actionNames[number] export type Camera = typeof cameras[number] @@ -87,8 +87,24 @@ type BaseState = { [K in Booleans]: boolean } +export interface Key { + name: string + values: string[] +} + +export interface KeyConfig extends KeyMap { + keys: Key[] + action: string +} + +export interface KeyMap { + fn: (pressed: boolean) => void + up?: boolean + pressed?: boolean +} + export interface IState extends BaseState { - actions: Record void> + actions: Record void) | ((action: string, newKey: Key) => void) | ((action: string, name: string) => void)> api: PublicApi | null bestCheckpoint: number camera: Camera @@ -96,6 +112,8 @@ export interface IState extends BaseState { checkpoint: number color: string controls: Controls + keyboardBindings: KeyConfig[] + keyBindingsWithError: number[] dpr: number finished: number get: Getter @@ -106,6 +124,28 @@ export interface IState extends BaseState { vehicleConfig: VehicleConfig wheelInfo: WheelInfo wheels: [RefObject, RefObject, RefObject, RefObject] + keyInput: string | null +} + +function deduplicateKeys(newKey: Key, keysList: KeyConfig[]): KeyConfig[] { + const deduplicatedList = keysList.map((key) => { + const index = key.keys.findIndex((keyCode) => keyCode.name === newKey.name) + if (index !== -1) { + key.keys.splice(index, 1) + } + + return key + }) + return deduplicatedList +} + +function checkKeybindings(keysList: KeyConfig[]): number[] { + return keysList.reduce((akk, value, index) => { + if (value.keys.length < 1) { + akk.push(index) + } + return akk + }, []) } const useStoreImpl = create((set: SetState, get: GetState) => { @@ -138,8 +178,109 @@ const useStoreImpl = create((set: SetState, get: GetState { + set((state) => { + const index = state.keyboardBindings.findIndex(({ action: stateAction }) => action === stateAction) + + const keyboardBindingsCopy = deduplicateKeys(newKey, state.keyboardBindings) + + keyboardBindingsCopy[index].keys.push(newKey) + + const keyBindingsWithError = checkKeybindings(keyboardBindingsCopy) + + return { ...state, keyboardBindings: keyboardBindingsCopy, keyBindingsWithError } + }) + }, + removeKeyBinding: (action: string, name: string) => { + set((state) => { + const index = state.keyboardBindings.findIndex(({ action: stateAction }) => action === stateAction) + + const keyIndex = state.keyboardBindings[index].keys.findIndex(({ name: stateName }) => name === stateName) + + const keyboardBindingsCopy = [...state.keyboardBindings] + + keyboardBindingsCopy[index].keys.splice(keyIndex, 1) + + const keyBindingsWithError = checkKeybindings(keyboardBindingsCopy) + + return { ...state, keyboardBindings: keyboardBindingsCopy, keyBindingsWithError } + }) + }, } + const keyboardBindings: KeyConfig[] = [ + { + action: 'Forward', + keys: [ + { name: '↑', values: ['ArrowUp'] }, + { name: 'W', values: ['KeyW'] }, + { name: 'Z', values: ['KeyZ'] }, + ], + + fn: (forward) => set((state) => ({ controls: { ...state.controls, forward } })), + }, + { + action: 'Backward', + keys: [ + { name: '↓', values: ['ArrowDown'] }, + { name: 'S', values: ['KeyS'] }, + ], + fn: (backward) => set((state) => ({ controls: { ...state.controls, backward } })), + }, + { + action: 'Left', + keys: [ + { name: '←', values: ['ArrowLeft'] }, + { name: 'A', values: ['KeyA'] }, + { name: 'Q', values: ['KeyQ'] }, + ], + fn: (left) => set((state) => ({ controls: { ...state.controls, left } })), + }, + { + action: 'Right', + keys: [ + { name: '→', values: ['ArrowRight'] }, + { name: 'D', values: ['KeyD'] }, + ], + fn: (right) => set((state) => ({ controls: { ...state.controls, right } })), + }, + { action: 'Drift', keys: [{ name: 'Space ␣', values: ['Space'] }], fn: (brake) => set((state) => ({ controls: { ...state.controls, brake } })) }, + { action: 'Honk', keys: [{ name: 'H', values: ['KeyH'] }], fn: (honk) => set((state) => ({ controls: { ...state.controls, honk } })) }, + { + action: 'Turbo Boost', + keys: [{ name: 'Shift ⇧', values: ['ShiftLeft', 'ShiftRight'] }], + fn: (boost) => set((state) => ({ controls: { ...state.controls, boost } })), + }, + { action: 'Reset', keys: [{ name: 'R', values: ['KeyR'] }], fn: actions.reset, up: false }, + { action: 'Editor', keys: [{ name: '.', values: ['Period'] }], fn: () => set((state) => ({ editor: !state.editor })), up: false }, + { + action: 'Help', + keys: [{ name: 'I', values: ['KeyI'] }], + fn: () => set((state) => ({ help: !state.help, leaderboard: false, pickcolor: false })), + up: false, + }, + { + action: 'Leaderboards', + keys: [{ name: 'L', values: ['KeyL'] }], + fn: () => set((state) => ({ help: false, leaderboard: !state.leaderboard, pickcolor: false })), + up: false, + }, + { action: 'Map', keys: [{ name: 'M', values: ['KeyM'] }], fn: () => set((state) => ({ map: !state.map })), up: false }, + { + action: 'Pick Car Color', + keys: [{ name: 'P', values: ['KeyP'] }], + fn: () => set((state) => ({ help: false, pickcolor: !state.pickcolor, leaderboard: false })), + up: false, + }, + { action: 'Toggle Mute', keys: [{ name: 'U', values: ['KeyU'] }], fn: () => set((state) => ({ sound: !state.sound })), up: false }, + { + action: 'Toggle Camera', + keys: [{ name: 'C', values: ['KeyC'] }], + fn: () => set((state) => ({ camera: cameras[(cameras.indexOf(state.camera) + 1) % cameras.length] })), + up: false, + }, + ] + return { actions, api: null, @@ -149,11 +290,14 @@ const useStoreImpl = create((set: SetState, get: GetState(), diff --git a/src/styles.css b/src/styles.css index cb04e514..ee53a3e8 100644 --- a/src/styles.css +++ b/src/styles.css @@ -41,11 +41,11 @@ body { } .checkpoint .green { - color: green + color: green; } .checkpoint .red { - color: red + color: red; } /* --------------- AUTH --------------- */ @@ -278,10 +278,20 @@ body { .popup-item { display: flex; justify-content: space-between; - width: 250px; + width: 274px; + height: 25px; + padding-right: 24px; margin: 0em 0; } +.with-error { + color: rgb(112, 7, 7); +} + +.popup-item.hovered { + padding-right: 0; +} + .popup-item-keys { display: flex; align-items: center; @@ -289,6 +299,8 @@ body { } .popup-item-key { + position: relative; + min-height: auto; display: inline-block; font-size: 0.7em; min-width: 24px; @@ -305,6 +317,20 @@ body { justify-content: center; } +.popup-item-key_hovered:before { + width: 100%; + content: '-'; + position: absolute; + background-color: #fff; +} + +.hovered-item { + cursor: pointer; + background: transparent; + color: #999; + border: 1px dashed #999; +} + button { min-width: 32px; min-height: 32px; @@ -440,6 +466,20 @@ button { animation: hideSoundIcon 3s forwards; } +.key-input { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 300px; + height: 100px; + background-color: #101010; + border: 1px solid #a0a0a0; + display: flex; + justify-content: center; + align-items: center; +} + @keyframes hideSoundIcon { from { opacity: 1; diff --git a/src/ui/Help.tsx b/src/ui/Help.tsx index b7a2a634..31e0ad20 100644 --- a/src/ui/Help.tsx +++ b/src/ui/Help.tsx @@ -1,27 +1,13 @@ +import { useState, useCallback, useEffect } from 'react' import type { HTMLAttributes } from 'react' -import { useStore } from '../store' +import type { Key } from '../store' -const controlOptions = [ - { keys: ['↑', 'W', 'Z'], action: 'Forward' }, - { keys: ['←', 'A', 'Q'], action: 'Left' }, - { keys: ['→', 'D'], action: 'Right' }, - { keys: ['↓', 'S'], action: 'Backward' }, - { keys: ['Space'], action: 'Drift' }, - { keys: ['Shift'], action: 'Turbo Boost' }, - { keys: ['H'], action: 'Honk' }, - { keys: ['M'], action: 'Map' }, - { keys: ['C'], action: 'Toggle Camera' }, - { keys: ['R'], action: 'Reset' }, - { keys: ['.'], action: 'Editor' }, - { keys: ['U'], action: 'Toggle Mute' }, - { keys: ['I'], action: 'Help' }, - { keys: ['L'], action: 'Leaderboards' }, - { keys: ['P'], action: 'Pick Car Color' }, -] +import { useStore } from '../store' export function Help(): JSX.Element { const [set, help, sound] = useStore((state) => [state.set, state.help, state.sound]) + return ( <>
@@ -40,21 +26,171 @@ export function Help(): JSX.Element { ) } -export function Keys(props: HTMLAttributes): JSX.Element { +function Button({ value, onClick }: { value: string; onClick: () => void }): JSX.Element { + const [isHovered, setIsHovered] = useState(false) return ( -
- {controlOptions.map(({ keys, action }) => ( -
-
{action}
-
- {keys.map((key) => ( - - {key} - - ))} -
-
- ))} + + ) +} + +function Row({ + action, + keys, + hasError, + onRemove, + onAdd, +}: { + action: string + keys: Key[] + hasError: boolean + onAdd: (action: string) => void + onRemove: (action: string, name: string) => void +}): JSX.Element { + const [isHovered, setIsHovered] = useState(false) + return ( +
{ + setIsHovered(true) + }} + onMouseLeave={() => { + setIsHovered(false) + }} + > +
{action}
+
+ {keys.map(({ name }) => ( + + )} +
) } + +function Rows({ onSelect }: { onSelect: (action: string) => void }) { + const [keyboardBindingsList, removeKeyBinding, keyBindingsWithError] = useStore((state) => [ + state.keyboardBindings, + state.actions.removeKeyBinding as (action: string, name: string) => void, + state.keyBindingsWithError, + ]) + + const addClickHandler = useCallback((action) => { + onSelect(action) + }, []) + + return ( + <> + {keyboardBindingsList.map(({ keys, action }, index) => ( + + ))} + + ) +} + +const keyMap: Record = { + ArrowUp: '↑', + ArrowDown: '↓', + ArrowLeft: '←', + ArrowRight: '→', + Tab: 'Tab ⇥', + CapsLock: 'CapsLock ⇪', + ShiftLeft: 'Shift ⇧', + ShiftRight: 'Shift ⇧', + ControlLeft: 'Control ⌃', + ControlRight: 'Control ⌃', + AltLeft: 'Alt ⌥', + AltRight: 'Alt ⌥', + MetaLeft: 'Meta ⌘', + MetaRight: 'Meta ⌘', + Space: 'Space ␣', + Enter: 'Enter ↵', + Backspace: 'Backspace ⌫', + Escape: 'Esc ⎋', +} + +const codesAlternativesList: Record = { + ShiftLeft: ['ShiftLeft', 'ShiftRight'], + ShiftRight: ['ShiftLeft', 'ShiftRight'], + ControlLeft: ['ControlLeft', 'ControlRight'], + ControlRight: ['ControlLeft', 'ControlRight'], + AltLeft: ['AltLeft', 'AltRight'], + AltRight: ['AltLeft', 'AltRight'], +} + +function populateCodes(code: string): string[] { + const codes = codesAlternativesList[code] + return codes ? codes : [code] +} + +function getKeyName(key: string, code: string): string { + const value = keyMap[code] + return value ? value : key.toUpperCase() +} + +function produceKeyFromCode(key: string, code: string): Key { + return { name: getKeyName(key, code), values: populateCodes(code) } +} + +function Modal({ onSelect }: { onSelect: (event: KeyboardEvent) => void }) { + useEffect(() => { + window.addEventListener('keydown', onSelect, { passive: true }) + + return () => { + window.removeEventListener('keydown', onSelect) + } + }, [onSelect]) + + return
Press new key
+} + +type KeyProps = HTMLAttributes + +export function Keys(props: KeyProps): JSX.Element { + const [addKeyBinding] = useStore((state) => [state.actions.addKeyBinding as (action: string, newKey: Key) => void]) + const [selectedAction, setSelectedAction] = useState(null) + + const selectActionHandler = useCallback((action) => { + setSelectedAction(action) + }, []) + + const selectKeyHandler = useCallback( + (event: KeyboardEvent) => { + const { key, code } = event + addKeyBinding(selectedAction!, produceKeyFromCode(key, code)) + setSelectedAction(null) + }, + [selectedAction], + ) + + return ( + <> +
+ +
+ {selectedAction && } + + ) +} From a89ff5a1ee71a2b7fdc281ab1ced146965777212 Mon Sep 17 00:00:00 2001 From: "a.muhin" Date: Mon, 21 Mar 2022 00:06:28 +0400 Subject: [PATCH 2/6] review issues fix --- src/controls/Keyboard.ts | 16 ++-- src/store.ts | 32 +++---- src/styles.css | 9 +- src/ui/Help.tsx | 176 +------------------------------------- src/ui/Intro.tsx | 2 +- src/ui/Keys.tsx | 180 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 211 insertions(+), 204 deletions(-) create mode 100644 src/ui/Keys.tsx diff --git a/src/controls/Keyboard.ts b/src/controls/Keyboard.ts index 5351e9f7..f4074c8b 100644 --- a/src/controls/Keyboard.ts +++ b/src/controls/Keyboard.ts @@ -4,22 +4,22 @@ import type { KeyConfig, KeyMap } from '../store' function useKeys(keyConfig: KeyConfig[]) { useEffect(() => { - const codeMap = keyConfig.reduce<{ [key: string]: KeyMap }>((out, { keys, fn, up = true }) => { + const keyMap = keyConfig.reduce<{ [key: string]: KeyMap }>((out, { keys, fn, up = true }) => { keys && keys.forEach((key) => key.values.forEach((value) => (out[value] = { fn, pressed: false, up }))) return out }, {}) const downHandler = ({ code, target }: KeyboardEvent) => { - if (!codeMap[code] || (target as HTMLElement).nodeName === 'INPUT') return - const { fn, pressed, up } = codeMap[code] - codeMap[code].pressed = true + if (!keyMap[code] || (target as HTMLElement).nodeName === 'INPUT') return + const { fn, pressed, up } = keyMap[code] + keyMap[code].pressed = true if (up || !pressed) fn(true) } const upHandler = ({ code, target }: KeyboardEvent) => { - if (!codeMap[code] || (target as HTMLElement).nodeName === 'INPUT') return - const { fn, up } = codeMap[code] - codeMap[code].pressed = false + if (!keyMap[code] || (target as HTMLElement).nodeName === 'INPUT') return + const { fn, up } = keyMap[code] + keyMap[code].pressed = false if (up) fn(false) } @@ -34,7 +34,7 @@ function useKeys(keyConfig: KeyConfig[]) { } export function Keyboard() { - const [keyboardBindings] = useStore((state) => [state.keyboardBindings]) + const keyboardBindings = useStore((state) => state.keyboardBindings) useKeys(keyboardBindings) return null } diff --git a/src/store.ts b/src/store.ts index 0a659810..48ae5581 100644 --- a/src/store.ts +++ b/src/store.ts @@ -92,17 +92,17 @@ export interface Key { values: string[] } -export interface KeyConfig extends KeyMap { - keys: Key[] - action: string -} - export interface KeyMap { fn: (pressed: boolean) => void up?: boolean pressed?: boolean } +export interface KeyConfig extends KeyMap { + keys: Key[] + action: string +} + export interface IState extends BaseState { actions: Record void) | ((action: string, newKey: Key) => void) | ((action: string, name: string) => void)> api: PublicApi | null @@ -128,15 +128,7 @@ export interface IState extends BaseState { } function deduplicateKeys(newKey: Key, keysList: KeyConfig[]): KeyConfig[] { - const deduplicatedList = keysList.map((key) => { - const index = key.keys.findIndex((keyCode) => keyCode.name === newKey.name) - if (index !== -1) { - key.keys.splice(index, 1) - } - - return key - }) - return deduplicatedList + return keysList.map((key) => ({ ...key, keys: [...key.keys.filter((keyCode) => keyCode.name !== newKey.name)] })) } function checkKeybindings(keysList: KeyConfig[]): number[] { @@ -184,7 +176,9 @@ const useStoreImpl = create((set: SetState, get: GetState((set: SetState, get: GetState((set: SetState, get: GetState((set: SetState, get: GetState [state.set, state.help, state.sound]) @@ -25,172 +22,3 @@ export function Help(): JSX.Element { ) } - -function Button({ value, onClick }: { value: string; onClick: () => void }): JSX.Element { - const [isHovered, setIsHovered] = useState(false) - return ( - - ) -} - -function Row({ - action, - keys, - hasError, - onRemove, - onAdd, -}: { - action: string - keys: Key[] - hasError: boolean - onAdd: (action: string) => void - onRemove: (action: string, name: string) => void -}): JSX.Element { - const [isHovered, setIsHovered] = useState(false) - return ( -
{ - setIsHovered(true) - }} - onMouseLeave={() => { - setIsHovered(false) - }} - > -
{action}
-
- {keys.map(({ name }) => ( - - )} -
-
- ) -} - -function Rows({ onSelect }: { onSelect: (action: string) => void }) { - const [keyboardBindingsList, removeKeyBinding, keyBindingsWithError] = useStore((state) => [ - state.keyboardBindings, - state.actions.removeKeyBinding as (action: string, name: string) => void, - state.keyBindingsWithError, - ]) - - const addClickHandler = useCallback((action) => { - onSelect(action) - }, []) - - return ( - <> - {keyboardBindingsList.map(({ keys, action }, index) => ( - - ))} - - ) -} - -const keyMap: Record = { - ArrowUp: '↑', - ArrowDown: '↓', - ArrowLeft: '←', - ArrowRight: '→', - Tab: 'Tab ⇥', - CapsLock: 'CapsLock ⇪', - ShiftLeft: 'Shift ⇧', - ShiftRight: 'Shift ⇧', - ControlLeft: 'Control ⌃', - ControlRight: 'Control ⌃', - AltLeft: 'Alt ⌥', - AltRight: 'Alt ⌥', - MetaLeft: 'Meta ⌘', - MetaRight: 'Meta ⌘', - Space: 'Space ␣', - Enter: 'Enter ↵', - Backspace: 'Backspace ⌫', - Escape: 'Esc ⎋', -} - -const codesAlternativesList: Record = { - ShiftLeft: ['ShiftLeft', 'ShiftRight'], - ShiftRight: ['ShiftLeft', 'ShiftRight'], - ControlLeft: ['ControlLeft', 'ControlRight'], - ControlRight: ['ControlLeft', 'ControlRight'], - AltLeft: ['AltLeft', 'AltRight'], - AltRight: ['AltLeft', 'AltRight'], -} - -function populateCodes(code: string): string[] { - const codes = codesAlternativesList[code] - return codes ? codes : [code] -} - -function getKeyName(key: string, code: string): string { - const value = keyMap[code] - return value ? value : key.toUpperCase() -} - -function produceKeyFromCode(key: string, code: string): Key { - return { name: getKeyName(key, code), values: populateCodes(code) } -} - -function Modal({ onSelect }: { onSelect: (event: KeyboardEvent) => void }) { - useEffect(() => { - window.addEventListener('keydown', onSelect, { passive: true }) - - return () => { - window.removeEventListener('keydown', onSelect) - } - }, [onSelect]) - - return
Press new key
-} - -type KeyProps = HTMLAttributes - -export function Keys(props: KeyProps): JSX.Element { - const [addKeyBinding] = useStore((state) => [state.actions.addKeyBinding as (action: string, newKey: Key) => void]) - const [selectedAction, setSelectedAction] = useState(null) - - const selectActionHandler = useCallback((action) => { - setSelectedAction(action) - }, []) - - const selectKeyHandler = useCallback( - (event: KeyboardEvent) => { - const { key, code } = event - addKeyBinding(selectedAction!, produceKeyFromCode(key, code)) - setSelectedAction(null) - }, - [selectedAction], - ) - - return ( - <> -
- -
- {selectedAction && } - - ) -} diff --git a/src/ui/Intro.tsx b/src/ui/Intro.tsx index 4b8231c4..3b7d5f37 100644 --- a/src/ui/Intro.tsx +++ b/src/ui/Intro.tsx @@ -6,7 +6,7 @@ import type { FC } from 'react' import { useStore } from '../store' import { setupSession, unAuthenticateUser } from '../data' -import { Keys } from './Help' +import { Keys } from './Keys' import { Auth } from './Auth' export const Intro: FC = ({ children }) => { diff --git a/src/ui/Keys.tsx b/src/ui/Keys.tsx new file mode 100644 index 00000000..13c17495 --- /dev/null +++ b/src/ui/Keys.tsx @@ -0,0 +1,180 @@ +import { useState, useCallback, useEffect } from 'react' + +import type { HTMLAttributes } from 'react' + +import type { Key } from '../store' + +import { useStore } from '../store' + +function Button({ value, onClick }: { value: string; onClick: () => void }): JSX.Element { + const [isHovered, setIsHovered] = useState(false) + return ( + + ) +} + +function Row({ + action, + keys, + hasError, + onRemove, + onAdd, +}: { + action: string + keys: Key[] + hasError: boolean + onAdd: (action: string) => void + onRemove: (action: string, name: string) => void +}): JSX.Element { + const [isHovered, setIsHovered] = useState(false) + return ( +
{ + setIsHovered(true) + }} + onMouseLeave={() => { + setIsHovered(false) + }} + > +
{action}
+
+ {keys.map(({ name }) => ( + + )} +
+
+ ) +} + +function Rows({ onSelect }: { onSelect: (action: string) => void }) { + const [keyboardBindingsList, removeKeyBinding, keyBindingsWithError] = useStore((state) => [ + state.keyboardBindings, + state.actions.removeKeyBinding as (action: string, name: string) => void, + state.keyBindingsWithError, + ]) + + const addClickHandler = useCallback((action) => { + onSelect(action) + }, []) + + return ( + <> + {keyboardBindingsList.map(({ keys, action }, index) => ( + + ))} + + ) +} + +const keyMap: Record = { + ArrowUp: '↑', + ArrowDown: '↓', + ArrowLeft: '←', + ArrowRight: '→', + Tab: 'Tab ⇥', + CapsLock: 'CapsLock ⇪', + ShiftLeft: 'Shift ⇧', + ShiftRight: 'Shift ⇧', + ControlLeft: 'Control ⌃', + ControlRight: 'Control ⌃', + AltLeft: 'Alt ⌥', + AltRight: 'Alt ⌥', + MetaLeft: 'Meta ⌘', + MetaRight: 'Meta ⌘', + Space: 'Space ␣', + Enter: 'Enter ↵', + Backspace: 'Backspace ⌫', + Escape: 'Esc ⎋', +} + +const codesAlternativesList: Record = { + ShiftLeft: ['ShiftLeft', 'ShiftRight'], + ShiftRight: ['ShiftLeft', 'ShiftRight'], + ControlLeft: ['ControlLeft', 'ControlRight'], + ControlRight: ['ControlLeft', 'ControlRight'], + AltLeft: ['AltLeft', 'AltRight'], + AltRight: ['AltLeft', 'AltRight'], +} + +function populateCodes(code: string): string[] { + const codes = codesAlternativesList[code] + return codes ? codes : [code] +} + +function getKeyName(key: string, code: string): string { + const value = keyMap[code] + return value ? value : key.toUpperCase() +} + +function produceKeyFromCode(key: string, code: string): Key { + return { name: getKeyName(key, code), values: populateCodes(code) } +} + +function Modal({ onSelect }: { onSelect: (event: KeyboardEvent) => void }) { + useEffect(() => { + window.addEventListener('keydown', onSelect, { passive: true }) + + return () => { + window.removeEventListener('keydown', onSelect) + } + }, [onSelect]) + + return ( +
+
Press new key
+
+ ) +} + +type KeyProps = HTMLAttributes + +export function Keys(props: KeyProps): JSX.Element { + const [addKeyBinding] = useStore((state) => [state.actions.addKeyBinding as (action: string, newKey: Key) => void]) + const [selectedAction, setSelectedAction] = useState(null) + + const selectActionHandler = useCallback((action) => { + setSelectedAction(action) + }, []) + + const selectKeyHandler = useCallback( + (event: KeyboardEvent) => { + const { key, code } = event + addKeyBinding(selectedAction!, produceKeyFromCode(key, code)) + setSelectedAction(null) + }, + [selectedAction], + ) + + return ( + <> +
+ +
+ {selectedAction && } + + ) +} From e68b91e60cc9a859ea5cce1c3504d967ffab944d Mon Sep 17 00:00:00 2001 From: "a.muhin" Date: Sun, 27 Mar 2022 01:00:23 +0400 Subject: [PATCH 3/6] review fixes --- src/store.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/store.ts b/src/store.ts index 48ae5581..5a946a3b 100644 --- a/src/store.ts +++ b/src/store.ts @@ -128,7 +128,7 @@ export interface IState extends BaseState { } function deduplicateKeys(newKey: Key, keysList: KeyConfig[]): KeyConfig[] { - return keysList.map((key) => ({ ...key, keys: [...key.keys.filter((keyCode) => keyCode.name !== newKey.name)] })) + return keysList.map((key) => ({ ...key, keys: key.keys.filter((keyCode) => keyCode.name !== newKey.name) })) } function checkKeybindings(keysList: KeyConfig[]): number[] { @@ -174,11 +174,13 @@ const useStoreImpl = create((set: SetState, get: GetState { const index = state.keyboardBindings.findIndex(({ action: stateAction }) => action === stateAction) + if (index === -1) { + return state + } + const keyboardBindingsCopy = deduplicateKeys(newKey, state.keyboardBindings) - if (index !== -1) { - keyboardBindingsCopy[index].keys.push(newKey) - } + keyboardBindingsCopy[index].keys.push(newKey) const keyBindingsWithError = checkKeybindings(keyboardBindingsCopy) @@ -189,13 +191,15 @@ const useStoreImpl = create((set: SetState, get: GetState { const index = state.keyboardBindings.findIndex(({ action: stateAction }) => action === stateAction) + if (index === -1) { + return state + } + const keyIndex = state.keyboardBindings[index].keys.findIndex(({ name: stateName }) => name === stateName) const keyboardBindingsCopy = [...state.keyboardBindings] - if (index !== -1) { - keyboardBindingsCopy[index].keys.splice(keyIndex, 1) - } + keyboardBindingsCopy[index].keys.splice(keyIndex, 1) const keyBindingsWithError = checkKeybindings(keyboardBindingsCopy) From c473e2140c67bbd067f9ca1d20f9a62c56ed1ef1 Mon Sep 17 00:00:00 2001 From: "a.muhin" Date: Sun, 10 Apr 2022 23:16:32 +0400 Subject: [PATCH 4/6] add button visibility fix --- src/styles.css | 20 +++++++++++++------- src/ui/Keys.tsx | 39 +++++++++------------------------------ 2 files changed, 22 insertions(+), 37 deletions(-) diff --git a/src/styles.css b/src/styles.css index 147274f0..269e8441 100644 --- a/src/styles.css +++ b/src/styles.css @@ -284,12 +284,20 @@ body { margin: 0em 0; } -.with-error { - color: rgb(112, 7, 7); +.popup-item:hover { + padding-right: 0; } -.popup-item.hovered { - padding-right: 0; +.keys-row:hover .add-button { + display: flex; +} + +.keys-row .add-button { + display: none; +} + +.with-error { + color: rgb(112, 7, 7); } .popup-item-keys { @@ -301,7 +309,6 @@ body { .popup-item-key { position: relative; min-height: auto; - display: inline-block; font-size: 0.7em; min-width: 24px; border: 1px solid transparent; @@ -309,7 +316,6 @@ body { border-radius: 3px; padding: 2px 5px; margin: 2px; - background: white; color: black; display: flex; @@ -317,7 +323,7 @@ body { justify-content: center; } -.popup-item-key_hovered:before { +.key-button:hover:before { width: 100%; content: '-'; position: absolute; diff --git a/src/ui/Keys.tsx b/src/ui/Keys.tsx index 13c17495..0a3e4328 100644 --- a/src/ui/Keys.tsx +++ b/src/ui/Keys.tsx @@ -6,20 +6,6 @@ import type { Key } from '../store' import { useStore } from '../store' -function Button({ value, onClick }: { value: string; onClick: () => void }): JSX.Element { - const [isHovered, setIsHovered] = useState(false) - return ( - - ) -} - function Row({ action, keys, @@ -33,38 +19,31 @@ function Row({ onAdd: (action: string) => void onRemove: (action: string, name: string) => void }): JSX.Element { - const [isHovered, setIsHovered] = useState(false) return ( -
{ - setIsHovered(true) - }} - onMouseLeave={() => { - setIsHovered(false) - }} - > +
{action}
{keys.map(({ name }) => ( - ))} - {isHovered && ( + { - )} + }
) From b6ce0ba6f9101f4c058bd8a96260f56ab3e28c89 Mon Sep 17 00:00:00 2001 From: "a.muhin" Date: Tue, 12 Apr 2022 12:00:13 +0400 Subject: [PATCH 5/6] review fix --- src/ui/Keys.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/ui/Keys.tsx b/src/ui/Keys.tsx index 0a3e4328..34709d4d 100644 --- a/src/ui/Keys.tsx +++ b/src/ui/Keys.tsx @@ -34,16 +34,14 @@ function Row({ {name} ))} - { - - } +
) From ce13d324c03ed85ceed1c659173e1fa5dded3077 Mon Sep 17 00:00:00 2001 From: "a.muhin" Date: Tue, 12 Apr 2022 18:39:43 +0400 Subject: [PATCH 6/6] build fixes --- src/models/BoundingBox.tsx | 2 +- src/ui/Finished.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/BoundingBox.tsx b/src/models/BoundingBox.tsx index 936b0e79..d3ce84b2 100644 --- a/src/models/BoundingBox.tsx +++ b/src/models/BoundingBox.tsx @@ -11,7 +11,7 @@ type Props = { } export const BoundingBox = ({ depth, height, position: [x, y, z], width }: Props) => { - const { reset: onCollide } = useStore(({ actions: { reset } }) => ({ reset })) + const { reset: onCollide } = useStore(({ actions: { reset } }) => ({ reset: reset as () => void })) const sharedProps = { isTrigger: true, diff --git a/src/ui/Finished.tsx b/src/ui/Finished.tsx index 17ef73d5..603735cd 100644 --- a/src/ui/Finished.tsx +++ b/src/ui/Finished.tsx @@ -6,7 +6,7 @@ import { Auth } from './Auth' import type { SavedScore } from '../data' export const Finished = (): JSX.Element => { - const { finished: time, reset, session } = useStore(({ actions: { reset }, finished, session }) => ({ finished, reset, session })) + const { finished: time, reset, session } = useStore(({ actions: { reset }, finished, session }) => ({ finished, reset: reset as () => void, session })) const [scoreId, setScoreId] = useState('') const [scores, setScores] = useState([]) const [position, setPosition] = useState(0)