From 403dec39dddf4e59634eaf44ee9d718914be85f4 Mon Sep 17 00:00:00 2001 From: Suhyun Park Date: Wed, 17 Jul 2024 18:36:05 +0900 Subject: [PATCH] feat: score input --- index.html | 6 + src/components/commons/Autocomplete.tsx | 222 ++++++++++++++++++ src/components/commons/ListBox.tsx | 67 ++++++ src/components/commons/Option.tsx | 34 +-- src/components/commons/Select.tsx | 76 +----- src/components/commons/TextField.tsx | 13 + src/components/form/GameTypeSelect.tsx | 29 ++- src/components/form/JyanshiSelect.tsx | 84 +++++++ .../form/gameRecord/GameRecordInput.tsx | 103 ++++++++ .../gameRecord/GameRecordSingleUserInput.tsx | 123 ++++++++++ .../form/gameRecord/GameScoreInput.tsx | 130 ++++++++++ src/components/form/gameRecord/types.ts | 9 + src/contexts/AuthContext.tsx | 1 + src/contexts/GlobalsContext.tsx | 29 +++ src/main.tsx | 9 +- src/pages/create/CreateLoggedIn.tsx | 66 +++++- src/styles/commons.ts | 21 ++ src/styles/fonts.ts | 6 + src/styles/option.ts | 58 +++++ src/utils/wind.ts | 33 +++ 20 files changed, 1003 insertions(+), 116 deletions(-) create mode 100644 src/components/commons/Autocomplete.tsx create mode 100644 src/components/commons/ListBox.tsx create mode 100644 src/components/commons/TextField.tsx create mode 100644 src/components/form/JyanshiSelect.tsx create mode 100644 src/components/form/gameRecord/GameRecordInput.tsx create mode 100644 src/components/form/gameRecord/GameRecordSingleUserInput.tsx create mode 100644 src/components/form/gameRecord/GameScoreInput.tsx create mode 100644 src/components/form/gameRecord/types.ts create mode 100644 src/contexts/GlobalsContext.tsx create mode 100644 src/styles/option.ts create mode 100644 src/utils/wind.ts diff --git a/index.html b/index.html index 7fe7383..56181c0 100644 --- a/index.html +++ b/index.html @@ -6,6 +6,12 @@ 개척단 훈련소 + + + diff --git a/src/components/commons/Autocomplete.tsx b/src/components/commons/Autocomplete.tsx new file mode 100644 index 0000000..cea1525 --- /dev/null +++ b/src/components/commons/Autocomplete.tsx @@ -0,0 +1,222 @@ +import styled from "@emotion/styled"; +import { Button } from "@mui/base/Button"; +import { Popper } from "@mui/base/Popper"; +import { + useAutocomplete, + UseAutocompleteProps, +} from "@mui/base/useAutocomplete"; +import { unstable_useForkRef as useForkRef } from "@mui/utils"; +import { IconChevronDown, IconX } from "@tabler/icons-react"; +import * as React from "react"; +import { forwardRef } from "react"; +import { color } from "../../styles/colors"; +import { commons } from "../../styles/commons"; +import { option } from "../../styles/option"; +import { ListBoxWrapper } from "./ListBox"; + +const StyledAutocompleteRoot = styled.div` + ${commons.textField} + display: inline-flex; + align-items: center; + gap: 8px; +`; + +const StyledInput = styled.input` + ${commons.textFieldBase} + flex: 1 0 0; + min-width: 0; + background-color: transparent; + height: 100%; + border: none; + + &:focus, + &:hover { + border: none; + outline: none; + } +`; + +const AutocompleteOption = styled.li` + ${option.base} + + &:hover { + ${option.hover} + } + + &[aria-selected="true"] { + ${option.selected} + } + + &.Mui-focused, + &.Mui-focusVisible { + ${option.highlighted} + } + + &.Mui-focusVisible { + ${option.focusVisible} + } + + &[aria-selected="true"].Mui-focused, + &[aria-selected="true"].Mui-focusVisible { + ${option.highlightedSelected} + } +`; + +const StyledPopper = styled.div` + position: relative; + z-index: 1001; + width: 320px; +`; + +const IconContainer = styled(Button)` + color: ${color.selectButtonLight}; + display: inline-flex; + align-self: center; + align-items: center; + justify-content: center; + + &:hover { + cursor: pointer; + } +`; + +const StyledNoOptions = styled.li` + list-style: none; + padding: 8px; + cursor: default; + color: ${color.silkBlueSecondaryText}; +`; + +interface Props< + Value = string, + Multiple extends boolean | undefined = false, + DisableClearable extends boolean | undefined = false, + FreeSolo extends boolean | undefined = false +> extends UseAutocompleteProps { + slotProps?: { + root?: React.HTMLAttributes; + }; +} + +interface AutocompleteType { + < + Value = string, + Multiple extends boolean | undefined = false, + DisableClearable extends boolean | undefined = false, + FreeSolo extends boolean | undefined = false + >( + props: Props, + ref: React.ForwardedRef + ): JSX.Element | null; + propTypes?: unknown; + displayName?: string | undefined; +} + +const Autocomplete = forwardRef( + < + Value = string, + Multiple extends boolean | undefined = false, + DisableClearable extends boolean | undefined = false, + FreeSolo extends boolean | undefined = false + >( + props: Props, + ref: React.ForwardedRef + ) => { + const { + disableClearable = false, + disabled = false, + readOnly = false, + slotProps = {}, + ...other + } = props; + + const { + getRootProps, + getInputProps, + getPopupIndicatorProps, + getClearProps, + getListboxProps, + getOptionProps, + dirty, + id, + popupOpen, + focused, + anchorEl, + setAnchorEl, + groupedOptions, + } = useAutocomplete({ + ...props, + componentName: "BaseAutocomplete", + }); + + const hasClearIcon = !disableClearable && !disabled && dirty && !readOnly; + + const rootRef = useForkRef(ref, setAnchorEl); + + return ( + + + + {hasClearIcon && ( + + + + )} + + + + + {anchorEl ? ( + + + {groupedOptions.map((option, index) => { + const isGroup = + typeof option === "object" && option && "group" in option; + if (!isGroup) { + const optionProps = getOptionProps({ option, index }); + + return ( + + {props.getOptionLabel?.(option)} + + ); + } else { + return Test; + } + })} + + {groupedOptions.length === 0 && ( + 결과 없음 + )} + + + ) : null} + + ); + } +) as AutocompleteType; + +export default Autocomplete; diff --git a/src/components/commons/ListBox.tsx b/src/components/commons/ListBox.tsx new file mode 100644 index 0000000..c5e90e9 --- /dev/null +++ b/src/components/commons/ListBox.tsx @@ -0,0 +1,67 @@ +import styled from "@emotion/styled"; +import { CssTransition, PopupContext } from "@mui/base"; +import { forwardRef, useContext } from "react"; +import { color } from "../../styles/colors"; + +export const ListBoxWrapper = styled.ul` + background-color: ${color.silkBlue}; + color: white; + padding: 16px; + margin: 12px 0; + width: 320px; + max-width: calc(100vw - 64px); + border-radius: 4px; + + .closed & { + opacity: 0; + transform: scale(0.95, 0.8); + transition: opacity 200ms ease-in, transform 200ms ease-in; + } + + .open & { + opacity: 1; + transform: scale(1, 1); + transition: opacity 100ms ease-out, + transform 100ms cubic-bezier(0.43, 0.29, 0.37, 1.48); + } + + .placement-top & { + transform-origin: bottom; + } + + .placement-bottom & { + transform-origin: top; + } +`; + +interface Props { + ownerState?: unknown; +} + +const ListBox = forwardRef( + (props: Props, ref: React.ForwardedRef) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { ownerState, ...other } = props; + const popupContext = useContext(PopupContext); + + if (popupContext == null) { + throw new Error( + "The `AnimatedListbox` component cannot be rendered outside a `Popup` component" + ); + } + + const verticalPlacement = popupContext.placement.split("-")[0]; + + return ( + + + + ); + } +); + +export default ListBox; diff --git a/src/components/commons/Option.tsx b/src/components/commons/Option.tsx index b93eb04..16d6aa9 100644 --- a/src/components/commons/Option.tsx +++ b/src/components/commons/Option.tsx @@ -1,50 +1,32 @@ import styled from "@emotion/styled"; import { Option as MuiOption, optionClasses } from "@mui/base"; -import { color } from "../../styles/colors"; +import { option } from "../../styles/option"; const Option = styled(MuiOption)` - list-style: none; - padding: 8px; - cursor: default; - - &:last-of-type { - border-bottom: none; - } + ${option.base} &.${optionClasses.selected} { - background-image: linear-gradient( - to right, - ${color.goldLight}, - ${color.silkBlue} - ); - color: ${color.silkBlue}; + ${option.selected} } &.${optionClasses.highlighted} { - background-color: ${color.silkBlueLight}; - outline: 1px solid ${color.goldLight}; - color: white; + ${option.highlighted} } &:focus-visible { - outline: 1px solid ${color.goldLight}; + ${option.focusVisible} } &.${optionClasses.highlighted}.${optionClasses.selected} { - background-image: linear-gradient( - to right, - ${color.goldLight}, - ${color.silkBlueLight} - ); - color: ${color.silkBlue}; + ${option.highlightedSelected} } &.${optionClasses.disabled} { - color: ${color.silkBlueSecondaryText}; + ${option.disabled} } &:hover:not(.${optionClasses.disabled}) { - background-color: ${color.silkBlueLight}; + ${option.hover} } `; diff --git a/src/components/commons/Select.tsx b/src/components/commons/Select.tsx index ae0cdb6..c40fefd 100644 --- a/src/components/commons/Select.tsx +++ b/src/components/commons/Select.tsx @@ -1,15 +1,13 @@ import styled from "@emotion/styled"; import { - CssTransition, - Select as MuiSelect, - PopupContext, - SelectListboxSlotProps, - SelectRootSlotProps, - SelectType, + Select as MuiSelect, + SelectRootSlotProps, + SelectType, } from "@mui/base"; -import { forwardRef, useContext } from "react"; -import { color } from "../../styles/colors"; import { IconChevronDown } from "@tabler/icons-react"; +import { forwardRef } from "react"; +import { color } from "../../styles/colors"; +import ListBox from "./ListBox"; const SelectButtonContainer = styled.button` background-image: linear-gradient( @@ -49,72 +47,12 @@ const SelectButton = forwardRef( } ); -const ListBox = styled.ul` - background-color: ${color.silkBlue}; - color: white; - padding: 16px; - margin: 12px 0; - width: 320px; - max-width: calc(100vw - 64px); - border-radius: 4px; - - .closed & { - opacity: 0; - transform: scale(0.95, 0.8); - transition: opacity 200ms ease-in, transform 200ms ease-in; - } - - .open & { - opacity: 1; - transform: scale(1, 1); - transition: opacity 100ms ease-out, - transform 100ms cubic-bezier(0.43, 0.29, 0.37, 1.48); - } - - .placement-top & { - transform-origin: bottom; - } - - .placement-bottom & { - transform-origin: top; - } -`; - -const AnimatedListBox = forwardRef( - ( - props: SelectListboxSlotProps, - ref: React.ForwardedRef - ) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { ownerState, ...other } = props; - const popupContext = useContext(PopupContext); - - if (popupContext == null) { - throw new Error( - "The `AnimatedListbox` component cannot be rendered outside a `Popup` component" - ); - } - - const verticalPlacement = popupContext.placement.split("-")[0]; - - return ( - - - - ); - } -); - const Select: SelectType = ({ slots, ...rest }) => { return ( { + return ; +}; + +export default TextField; diff --git a/src/components/form/GameTypeSelect.tsx b/src/components/form/GameTypeSelect.tsx index 6f39ca9..271e3ba 100644 --- a/src/components/form/GameTypeSelect.tsx +++ b/src/components/form/GameTypeSelect.tsx @@ -6,12 +6,12 @@ import { IconTriangleInverted, TablerIconsProps, } from "@tabler/icons-react"; -import useGameTypes from "../../api/useGameTypes"; -import Option from "../commons/Option"; -import Select from "../commons/Select"; import { useEffect } from "react"; -import { GameTypeResponse } from "../../types/GameTypeResponse"; +import { useGlobals } from "../../contexts/GlobalsContext"; import { color } from "../../styles/colors"; +import { GameTypeResponse } from "../../types/GameTypeResponse"; +import Option from "../commons/Option"; +import Select from "../commons/Select"; const GameOption = styled.div` display: flex; @@ -20,8 +20,8 @@ const GameOption = styled.div` `; interface Props { - value: string | null; - onChange?: (value: string) => void; + value: GameTypeResponse | null; + onChange?: (value: GameTypeResponse) => void; setToDefault?: boolean; } @@ -39,11 +39,11 @@ const Icon = ({ }; const GameTypeSelect = ({ value, onChange, setToDefault }: Props) => { - const gameTypes = useGameTypes(); + const { gameTypes } = useGlobals(); useEffect(() => { - if (!value && gameTypes && onChange && setToDefault) { - onChange(gameTypes[0].type); + if (!value && gameTypes?.length && onChange && setToDefault) { + onChange(gameTypes[0]); } }, [gameTypes, onChange, setToDefault, value]); @@ -58,23 +58,22 @@ const GameTypeSelect = ({ value, onChange, setToDefault }: Props) => { if (v) onChange?.(v); }} renderValue={(v) => { - const gameType = gameTypes.find((gt) => gt.type === v?.value); - return gameType ? ( + return v ? ( - {gameType.displayName.ko} + {v.value.displayName.ko} ) : null; }} - getOptionAsString={(v) => v.value} + getOptionAsString={(v) => v.value.displayName.ko} > {gameTypes.map((gameType) => ( -