diff --git a/.eslintrc b/.eslintrc index 34c0f8e..551173c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -12,9 +12,11 @@ "project": ["./tsconfig.json"] }, "rules": { + "no-param-reassign": "off", "no-console": "off", "react/react-in-jsx-scope": "off", "react/prop-types": "off", + "react/jsx-props-no-spreading": "off", "@typescript-eslint/no-unused-vars": "warn", "react/jsx-no-useless-fragment": "off", "import/prefer-default-export": "off", diff --git a/.pnp.cjs b/.pnp.cjs index 315381e..0f22e1a 100644 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -56,7 +56,8 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["react-scripts", "virtual:572974bfd16fba63746e564b946fcd0d5481e3958a2d86b2339f8f9624fc9f4abd3898c4444c731638d6001d3f88ca414f2c05308d6789925602f170e2d31abc#npm:5.0.1"],\ ["styled-components", "virtual:572974bfd16fba63746e564b946fcd0d5481e3958a2d86b2339f8f9624fc9f4abd3898c4444c731638d6001d3f88ca414f2c05308d6789925602f170e2d31abc#npm:6.0.7"],\ ["typescript", "patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=289587"],\ - ["web-vitals", "npm:2.1.4"]\ + ["web-vitals", "npm:2.1.4"],\ + ["zustand", "virtual:572974bfd16fba63746e564b946fcd0d5481e3958a2d86b2339f8f9624fc9f4abd3898c4444c731638d6001d3f88ca414f2c05308d6789925602f170e2d31abc#npm:4.4.1"]\ ],\ "linkType": "SOFT"\ }]\ @@ -17625,7 +17626,8 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["react-scripts", "virtual:572974bfd16fba63746e564b946fcd0d5481e3958a2d86b2339f8f9624fc9f4abd3898c4444c731638d6001d3f88ca414f2c05308d6789925602f170e2d31abc#npm:5.0.1"],\ ["styled-components", "virtual:572974bfd16fba63746e564b946fcd0d5481e3958a2d86b2339f8f9624fc9f4abd3898c4444c731638d6001d3f88ca414f2c05308d6789925602f170e2d31abc#npm:6.0.7"],\ ["typescript", "patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=289587"],\ - ["web-vitals", "npm:2.1.4"]\ + ["web-vitals", "npm:2.1.4"],\ + ["zustand", "virtual:572974bfd16fba63746e564b946fcd0d5481e3958a2d86b2339f8f9624fc9f4abd3898c4444c731638d6001d3f88ca414f2c05308d6789925602f170e2d31abc#npm:4.4.1"]\ ],\ "linkType": "SOFT"\ }]\ @@ -18186,6 +18188,28 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["use-sync-external-store", [\ + ["npm:1.2.0", {\ + "packageLocation": "./.yarn/cache/use-sync-external-store-npm-1.2.0-44f75d2564-5c639e0f8d.zip/node_modules/use-sync-external-store/",\ + "packageDependencies": [\ + ["use-sync-external-store", "npm:1.2.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:82eb135ac4e260fc91b07d128601dc104e2434498905227060bdfe7df89a42ce24f60543dd62a897728b2ba4af60e9da1f2f6083ec0ed2c8bd14ffa3edccd4b8#npm:1.2.0", {\ + "packageLocation": "./.yarn/__virtual__/use-sync-external-store-virtual-0b2e2b9dc4/0/cache/use-sync-external-store-npm-1.2.0-44f75d2564-5c639e0f8d.zip/node_modules/use-sync-external-store/",\ + "packageDependencies": [\ + ["use-sync-external-store", "virtual:82eb135ac4e260fc91b07d128601dc104e2434498905227060bdfe7df89a42ce24f60543dd62a897728b2ba4af60e9da1f2f6083ec0ed2c8bd14ffa3edccd4b8#npm:1.2.0"],\ + ["@types/react", "npm:18.2.21"],\ + ["react", "npm:18.2.0"]\ + ],\ + "packagePeers": [\ + "@types/react",\ + "react"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["util-deprecate", [\ ["npm:1.0.2", {\ "packageLocation": "./.yarn/cache/util-deprecate-npm-1.0.2-e3fe1a219c-474acf1146.zip/node_modules/util-deprecate/",\ @@ -19101,6 +19125,33 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ],\ "linkType": "HARD"\ }]\ + ]],\ + ["zustand", [\ + ["npm:4.4.1", {\ + "packageLocation": "./.yarn/cache/zustand-npm-4.4.1-f6868a7326-80acd0fbf6.zip/node_modules/zustand/",\ + "packageDependencies": [\ + ["zustand", "npm:4.4.1"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:572974bfd16fba63746e564b946fcd0d5481e3958a2d86b2339f8f9624fc9f4abd3898c4444c731638d6001d3f88ca414f2c05308d6789925602f170e2d31abc#npm:4.4.1", {\ + "packageLocation": "./.yarn/__virtual__/zustand-virtual-82eb135ac4/0/cache/zustand-npm-4.4.1-f6868a7326-80acd0fbf6.zip/node_modules/zustand/",\ + "packageDependencies": [\ + ["zustand", "virtual:572974bfd16fba63746e564b946fcd0d5481e3958a2d86b2339f8f9624fc9f4abd3898c4444c731638d6001d3f88ca414f2c05308d6789925602f170e2d31abc#npm:4.4.1"],\ + ["@types/immer", null],\ + ["@types/react", "npm:18.2.21"],\ + ["immer", null],\ + ["react", "npm:18.2.0"],\ + ["use-sync-external-store", "virtual:82eb135ac4e260fc91b07d128601dc104e2434498905227060bdfe7df89a42ce24f60543dd62a897728b2ba4af60e9da1f2f6083ec0ed2c8bd14ffa3edccd4b8#npm:1.2.0"]\ + ],\ + "packagePeers": [\ + "@types/immer",\ + "@types/react",\ + "immer",\ + "react"\ + ],\ + "linkType": "HARD"\ + }]\ ]]\ ]\ }'), {basePath: basePath || __dirname}); diff --git a/.yarn/cache/use-sync-external-store-npm-1.2.0-44f75d2564-5c639e0f8d.zip b/.yarn/cache/use-sync-external-store-npm-1.2.0-44f75d2564-5c639e0f8d.zip new file mode 100644 index 0000000..d737a8f Binary files /dev/null and b/.yarn/cache/use-sync-external-store-npm-1.2.0-44f75d2564-5c639e0f8d.zip differ diff --git a/.yarn/cache/zustand-npm-4.4.1-f6868a7326-80acd0fbf6.zip b/.yarn/cache/zustand-npm-4.4.1-f6868a7326-80acd0fbf6.zip new file mode 100644 index 0000000..3927ff2 Binary files /dev/null and b/.yarn/cache/zustand-npm-4.4.1-f6868a7326-80acd0fbf6.zip differ diff --git a/README.md b/README.md index df06f13..9658bf6 100644 --- a/README.md +++ b/README.md @@ -11,3 +11,5 @@ $ yarn start ``` ## 1회 Chip Component + +## 2회 Button Component \ No newline at end of file diff --git a/package.json b/package.json index 20dd2cb..66b0015 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "react-scripts": "5.0.1", "styled-components": "^6.0.7", "typescript": "^4.9.5", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "zustand": "^4.4.1" }, "scripts": { "start": "react-scripts start", diff --git a/public/index.html b/public/index.html index aa069f2..ef63374 100644 --- a/public/index.html +++ b/public/index.html @@ -1,14 +1,11 @@ - + - + + diff --git a/src/App.tsx b/src/App.tsx index b80e988..a29ceab 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,33 +1,24 @@ -// Img -import check from './assets/images/chip/check.png'; -import close from './assets/images/chip/close.png'; - -// Component -import Chip from './chip'; +import Modal from 'modal'; +import { useEffect } from 'react'; +import useModal from 'stores/useModal'; const App = () => { + const { openModal } = useModal(); + + useEffect(() => { + openModal({ + COMMON: { + component: <>COMMON, + callback() { + console.log('Call Back'); + }, + }, + }); + }, [openModal]); + return ( <> - - SUGGESTION - - - - ASSIST - - - - FILTER USE SELECT - - - - INPUT - - + ); }; diff --git a/src/button/index.tsx b/src/button/index.tsx new file mode 100644 index 0000000..c610705 --- /dev/null +++ b/src/button/index.tsx @@ -0,0 +1,33 @@ +import { ButtonHTMLAttributes, FC, PropsWithChildren } from 'react'; +import { StyledButton } from './style'; +import { Spinner } from '../loading/style'; + +interface IButton extends ButtonHTMLAttributes, PropsWithChildren { + isFetching?: boolean; + height?: string; + width?: string; + bgColor?: string; + borderRadius?: string; + hoverOption?: { + bgColor?: string; + color?: string; + }; +} +const Button: FC = ({ isFetching, children, ...rest }) => { + return ( + + {isFetching ? : children} + + ); +}; + +export default Button; + +Button.defaultProps = { + isFetching: false, + height: '50px', + width: '150px', + bgColor: undefined, + borderRadius: '2.778vw', + hoverOption: undefined, +}; diff --git a/src/button/style/index.tsx b/src/button/style/index.tsx new file mode 100644 index 0000000..19d13f9 --- /dev/null +++ b/src/button/style/index.tsx @@ -0,0 +1,33 @@ +import { styled } from 'styled-components'; + +export const StyledButton = styled.button<{ + isFetching?: boolean; + borderRadius?: string; + height?: string; + width?: string; + bgColor?: string; + hoverOption?: { + bgColor?: string; + color?: string; + }; +}>` + width: ${(props) => props.width}; + height: ${(props) => props.height}; + background-color: ${(props) => props.bgColor}; + border-color: ${(props) => props.bgColor}; + display: flex; + justify-content: center; + align-items: center; + border-radius: ${(props) => props.borderRadius}; + margin: 16px; + ${(props) => !props.disabled && 'cursor: pointer;'} + ${(props) => + !props.disabled && + ` + &:hover { + border-color: ${props.hoverOption?.bgColor}; + background-color: ${props.hoverOption?.bgColor}; + color: ${props.hoverOption?.color}; + } + `} +`; diff --git a/src/chip/index.tsx b/src/chip/index.tsx index c8c1948..b2f934f 100644 --- a/src/chip/index.tsx +++ b/src/chip/index.tsx @@ -1,7 +1,7 @@ import { FC, PropsWithChildren, useState } from 'react'; // Style -import { ChipContainer, ChipLeadingImg, ChipLabel, ChipTrailingImg } from './styled'; +import { ChipContainer, ChipLeadingImg, ChipLabel, ChipTrailingImg } from './style'; interface IChip extends PropsWithChildren { useSelected?: boolean; diff --git a/src/chip/styled/index.tsx b/src/chip/style/index.tsx similarity index 100% rename from src/chip/styled/index.tsx rename to src/chip/style/index.tsx diff --git a/src/chip/test.tsx b/src/chip/test.tsx new file mode 100644 index 0000000..90e0a26 --- /dev/null +++ b/src/chip/test.tsx @@ -0,0 +1,35 @@ +// Img +import check from './assets/images/chip/check.png'; +import close from './assets/images/chip/close.png'; + +// Component +import Chip from '.'; + +const ChipTest = () => { + return ( + <> + + SUGGESTION + + + + ASSIST + + + + FILTER USE SELECT + + + + INPUT + + + + ); +}; + +export default ChipTest; diff --git a/src/loading/style/index.tsx b/src/loading/style/index.tsx new file mode 100644 index 0000000..6d60d6d --- /dev/null +++ b/src/loading/style/index.tsx @@ -0,0 +1,26 @@ +import styled, { keyframes } from 'styled-components'; + +const rotation = keyframes` + from{ + transform: rotate(0deg); + } + + to{ + transform: rotate(360deg); + } +`; + +export const Spinner = styled.div<{ + height?: string; + width?: string; + color?: string; +}>` + position: relative; + height: ${(props) => props.height || '30px'}; + width: ${(props) => props.width || '30px'}; + border: 1px solid ${(props) => props.color || '#000'}; + border-radius: 50%; + border-top: none; + border-right: none; + animation: ${rotation} 1s linear infinite; +`; diff --git a/src/modal/index.tsx b/src/modal/index.tsx new file mode 100644 index 0000000..de14f23 --- /dev/null +++ b/src/modal/index.tsx @@ -0,0 +1,81 @@ +import { MouseEvent } from 'react'; +import { createPortal } from 'react-dom'; + +// Store +import useModal, { ModalInfo } from 'stores/useModal'; + +// Style +import * as S from 'styles/common/Modal'; + +const Modal = () => { + const { modalStack, closeModal } = useModal(); + + const modalContainer = document.getElementById('modal') as Element; + if (!modalContainer) { + alert('Modal Container is not exist!'); + + return null; + } + + const handleConfirm = (e: MouseEvent, type: string, callback?: () => void) => { + e.stopPropagation(); + + if (callback) callback(); + else closeModal({ customType: type }); + }; + + const handleClose = (e: MouseEvent, type: string) => { + e.stopPropagation(); + + closeModal({ customType: type }); + }; + + const renderModal = (type: string, { component, callback }: ModalInfo) => { + let children = component; + + switch (type) { + case 'COMMON': + children = ( + + {component} + + ); + break; + case 'ONLY_CONFIRM': + children = ( + + {component} + + handleConfirm(e, type, callback)}>확인 + + + ); + break; + case 'CONFIRM': + children = ( + + {component} + + handleConfirm(e, type, callback)}>확인 + handleClose(e, type)}>취소 + + + ); + break; + + default: + break; + } + + return createPortal( + + {children} + , + modalContainer, + ); + }; + + return <>{Object.entries(modalStack).map(([type, modalInfo]) => renderModal(type, modalInfo))}; +}; + +export default Modal; diff --git a/src/stores/useModal.ts b/src/stores/useModal.ts new file mode 100644 index 0000000..6e2c73a --- /dev/null +++ b/src/stores/useModal.ts @@ -0,0 +1,45 @@ +import { create } from 'zustand'; + +type ModalType = 'COMMON' | 'CUSTOM' | 'ONLY_CONFIRM' | 'CONFIRM'; +export type ModalInfo = { + component: JSX.Element; + callback?: () => void; +}; +type DefalutModal = Record; +type CustomModal = Record; +type ModalStack = DefalutModal | CustomModal; +interface IUseModal { + modalStack: ModalStack; + openModal: (props: ModalStack) => void; + closeModal: (props: { defaultType?: keyof DefalutModal; customType?: keyof CustomModal }) => void; + popModal: () => void; + resetModal: () => void; +} + +const useModal = create((set) => ({ + modalStack: {}, + openModal(props) { + set((prev) => ({ modalStack: { ...prev.modalStack, ...props } })); + }, + closeModal({ defaultType, customType }) { + set((prev) => { + delete prev.modalStack[defaultType || (customType as keyof ModalStack)]; + + return { modalStack: { ...prev.modalStack } }; + }); + }, + popModal() { + set((prev) => { + const list = Object.entries(prev.modalStack); + list.pop(); + const willUpdateModalStack = Object.fromEntries(list); + + return { modalStack: willUpdateModalStack }; + }); + }, + resetModal() { + set(() => ({ modalStack: {} })); + }, +})); + +export default useModal; diff --git a/src/styles/common/Modal.tsx b/src/styles/common/Modal.tsx new file mode 100644 index 0000000..5de4225 --- /dev/null +++ b/src/styles/common/Modal.tsx @@ -0,0 +1,76 @@ +import Button from 'button'; +import { styled } from 'styled-components'; + +export const ModalLayout = styled.div` + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: 999; + box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.2); + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.5); + touch-action: none; +`; + +export const ModalBox = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 100vw; + height: 100vh; +`; + +export const ModalTitle = styled.p` + margin-bottom: 6.667vmin; + font-size: 6.667vmin; + font-weight: 700; +`; + +export const ModalButton = styled(Button)` + width: 33.333vmin; + font-size: 4.444vmin; +`; + +export const DefaultModalBox = styled.div` + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 20px; + box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.5); + overflow: auto; + width: 66.667vmin; + padding: 5.556vmin; + background-color: #fff; +`; + +export const DefaultModalParagraph = styled.p` + font-size: 5vmin; + font-weight: 500; + text-align: center; + white-space: pre-wrap; +`; + +export const DefaultModalFunctionBox = styled.div<{ $isOneButton?: boolean }>` + display: flex; + flex-direction: row; + justify-content: ${({ $isOneButton }) => ($isOneButton ? 'space-around' : 'space-between')}; + align-items: center; + width: 100%; + margin-top: 6.667vmin; + + & button:nth-child(1) { + background-color: #f1658a; + color: #fff; + } + + & button:nth-child(2) { + margin-left: 2.222vmin; + background-color: #3a3a4d; + color: #fff; + } +`; diff --git a/tsconfig.json b/tsconfig.json index 9d379a3..5452a6d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,8 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "react-jsx" + "jsx": "react-jsx", + "baseUrl": "src" }, "include": ["src"] } diff --git a/yarn.lock b/yarn.lock index 7df0648..cbecc94 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12900,6 +12900,7 @@ __metadata: styled-components: ^6.0.7 typescript: ^4.9.5 web-vitals: ^2.1.4 + zustand: ^4.4.1 languageName: unknown linkType: soft @@ -13337,6 +13338,15 @@ __metadata: languageName: node linkType: hard +"use-sync-external-store@npm:1.2.0": + version: 1.2.0 + resolution: "use-sync-external-store@npm:1.2.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 5c639e0f8da3521d605f59ce5be9e094ca772bd44a4ce7322b055a6f58eeed8dda3c94cabd90c7a41fb6fa852210092008afe48f7038792fd47501f33299116a + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" @@ -14114,3 +14124,23 @@ __metadata: checksum: f77b3d8d00310def622123df93d4ee654fc6a0096182af8bd60679ddcdfb3474c56c6c7190817c84a2785648cdee9d721c0154eb45698c62176c322fb46fc700 languageName: node linkType: hard + +"zustand@npm:^4.4.1": + version: 4.4.1 + resolution: "zustand@npm:4.4.1" + dependencies: + use-sync-external-store: 1.2.0 + peerDependencies: + "@types/react": ">=16.8" + immer: ">=9.0" + react: ">=16.8" + peerDependenciesMeta: + "@types/react": + optional: true + immer: + optional: true + react: + optional: true + checksum: 80acd0fbf633782996642802c8692bbb80ae5c80a8dff4c501b88250acd5ccd468fbc6398bdce198475a25e3839c91385b81da921274f33ffb5c2d08c3eab400 + languageName: node + linkType: hard