diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..b2ac79f --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,11 @@ +## What is this PR? πŸ” +- 무슨 λͺ©μ /이유둜 κ΅¬ν˜„ν–ˆλŠ”μ§€ + +### πŸ› οΈ Issue +- Closes #number + +## Changes πŸ“ +- μž‘μ—…ν•œ λ‚΄μš© μ μ–΄μ£Όμ„Έμš” + +## To Reviewers πŸ“’ +- 이 λΆ€λΆ„ μ’€ 같이 λ΄μ£Όμ„Έμš” / ν˜Ήμ€ ν•˜κ³  싢은 말 diff --git a/package.json b/package.json index 72ed6bf..6256ef3 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", + "serve": "vite preview", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint:css": "stylelint './src/**/*.tsx'", "preview": "vite preview", @@ -24,7 +25,8 @@ "react-router-dom": "^6.16.0", "styled-components": "^6.0.8", "styled-normalize": "^8.0.7", - "swiper": "^11.0.4" + "swiper": "^11.0.4", + "zustand": "^4.5.0" }, "devDependencies": { "@storybook/addon-essentials": "^7.4.0", diff --git a/src/apis/oauth/signup.ts b/src/apis/oauth/signup.ts index 7b2dd66..9cba33d 100644 --- a/src/apis/oauth/signup.ts +++ b/src/apis/oauth/signup.ts @@ -1,5 +1,7 @@ import { useMutation } from '@tanstack/react-query'; +import { OAuthResponse } from '@interfaces/api/oauth'; + import client from '@apis/fetch'; interface SingnUpRequestDTO { @@ -10,6 +12,13 @@ interface SingnUpRequestDTO { job: string; } +type TermsConsetResponseDTO = Omit; + +interface TermsConsentRequestDTO { + memberId: number; + listen_marketing: boolean; +} + const signup = (req: SingnUpRequestDTO) => { return client.post({ path: '/auth/signup/profile', @@ -17,9 +26,20 @@ const signup = (req: SingnUpRequestDTO) => { }); }; +const terms = (req: TermsConsentRequestDTO) => { + return client.post({ + path: '/auth/signup/terms', + body: req, + }); +}; + const useSignup = () => { return useMutation({ mutationFn: signup }); }; -export { useSignup }; -export type { SingnUpRequestDTO }; +const useTerms = () => { + return useMutation({ mutationFn: terms }); +}; + +export { useSignup, useTerms }; +export type { SingnUpRequestDTO, TermsConsentRequestDTO }; diff --git a/src/assets/icons/check.svg b/src/assets/icons/check.svg new file mode 100644 index 0000000..1b52798 --- /dev/null +++ b/src/assets/icons/check.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/icons/index.ts b/src/assets/icons/index.ts index 18d87d3..0206198 100644 --- a/src/assets/icons/index.ts +++ b/src/assets/icons/index.ts @@ -6,6 +6,7 @@ import NewAlarmIcon from './alarm-new.svg?react'; import AppleIcon from './apple.svg?react'; import BLogoIcon from './b-logo.svg?react'; import BigDownChevronIcon from './big-down-chevron.svg?react'; +import CheckIcon from './check.svg?react'; import ClockIcon from './clock.svg?react'; import CloseIcon from './close.svg?react'; import CommentIcon from './comment.svg?react'; @@ -37,6 +38,7 @@ export { AppleIcon, BigDownChevronIcon, BLogoIcon, + CheckIcon, ClockIcon, CloseIcon, CommentIcon, diff --git a/src/components/commons/BottomSheet/BottomSheet.tsx b/src/components/commons/BottomSheet/BottomSheet.tsx index 3b62b71..ac784ed 100644 --- a/src/components/commons/BottomSheet/BottomSheet.tsx +++ b/src/components/commons/BottomSheet/BottomSheet.tsx @@ -16,6 +16,7 @@ interface BottomSheetProps { setIsOpen: React.Dispatch>; snapPoints?: number[]; initialSnap?: number; + transparent?: boolean; children: React.ReactNode; } @@ -24,6 +25,7 @@ const BottomSheet = ({ setIsOpen, snapPoints = [0.9, 0.7, 0], initialSnap = 0.7, + transparent = true, children, }: BottomSheetProps) => { const screenHeight = window.innerHeight; @@ -110,6 +112,7 @@ const BottomSheet = ({ ` position: fixed; top: 0; left: 0; @@ -166,7 +169,8 @@ const Backdrop = styled(motion.div)` width: 100%; height: 100%; - /* background: rgb(0 0 0 / 50%); */ + ${({ transparent }) => + transparent ? 'background: transparent;' : 'background: rgb(0 0 0 / 50%);'} `; const Wrapper = styled(motion.div)` diff --git a/src/components/commons/CheckBox/CheckBox.tsx b/src/components/commons/CheckBox/CheckBox.tsx new file mode 100644 index 0000000..d02689d --- /dev/null +++ b/src/components/commons/CheckBox/CheckBox.tsx @@ -0,0 +1,58 @@ +import styled from 'styled-components'; + +import { CheckIcon } from '@icons/index'; + +import { Row } from '../Flex/Flex'; +import Text from '../Text/Text'; + +interface CheckboxProps { + id: string; + checked: boolean; + onChange: (e: React.ChangeEvent) => void; + children: React.ReactNode; +} + +function Checkbox({ id, checked, onChange, children }: CheckboxProps) { + return ( + + + + {checked && } + + + + ); +} + +export default Checkbox; + +const CheckBoxLabel = styled.label<{ checked: boolean }>` + position: relative; + display: inline-block; + width: 22px; + height: 22px; + cursor: pointer; + background: ${({ checked, theme }) => + checked ? `${theme.colors.purple}` : 'rgb(77 59 124 / 20%)'}; + border-radius: 3px; + + & > svg { + position: absolute; + top: 3px; + left: 3.5px; + } +`; + +const HiddenCheckbox = styled.input` + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: 0; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; + border: 0; +`; diff --git a/src/constants/signup.ts b/src/constants/signup.ts index b5c5ffb..d6d959f 100644 --- a/src/constants/signup.ts +++ b/src/constants/signup.ts @@ -8,10 +8,10 @@ export const JOBS = [ export const GENDERS = [ { label: '남성', - value: 'male', + value: 'MALE', }, { label: 'μ—¬μ„±', - value: 'female', + value: 'FEMALE', }, ]; diff --git a/src/hooks/useBottomSheet/useBottomSheet.tsx b/src/hooks/useBottomSheet/useBottomSheet.tsx index 67d2b22..25ae377 100644 --- a/src/hooks/useBottomSheet/useBottomSheet.tsx +++ b/src/hooks/useBottomSheet/useBottomSheet.tsx @@ -5,10 +5,11 @@ import BottomSheet from '@components/commons/BottomSheet/BottomSheet'; interface UseBottomSheetProps { snapPoints?: number[]; initialSnap?: number; + transparent?: boolean; } const useBottomSheet = (props: UseBottomSheetProps) => { - const { snapPoints = [0.9, 0.7, 0], initialSnap = 0.7 } = props; + const { snapPoints = [0.9, 0.7, 0], initialSnap = 0.7, transparent = true } = props; const [isOpen, setIsOpen] = useState(false); const toggleSheet = useCallback(() => { @@ -21,6 +22,7 @@ const useBottomSheet = (props: UseBottomSheetProps) => { setIsOpen={setIsOpen} snapPoints={snapPoints} initialSnap={initialSnap} + transparent={transparent} > {children} diff --git a/src/interfaces/models/user.ts b/src/interfaces/models/user.ts new file mode 100644 index 0000000..2b01e0f --- /dev/null +++ b/src/interfaces/models/user.ts @@ -0,0 +1,6 @@ +export interface User { + memberId: number; + nickname?: string; + accessToken?: string; + refrehToken?: string; +} diff --git a/src/routes/Auth/kakao/KakaoLogin.tsx b/src/routes/Auth/kakao/KakaoLogin.tsx index 4402c61..188c8a5 100644 --- a/src/routes/Auth/kakao/KakaoLogin.tsx +++ b/src/routes/Auth/kakao/KakaoLogin.tsx @@ -1,13 +1,17 @@ import React, { useEffect } from 'react'; import { useNavigate } from 'react-router'; +import { useAuthStore } from 'src/store/auth'; import { kakaoLogin } from '@apis/oauth/kakao'; import { ResponseError } from '@apis/fetch'; +import Login from '../login/Login'; + import { Container } from './KakaoLogin.styles'; const KakaoLogin = () => { + const setUser = useAuthStore((state) => state.setUser); const kakaoCode = new URL(window.location.href).searchParams.get('code'); const navigate = useNavigate(); @@ -17,7 +21,14 @@ const KakaoLogin = () => { try { const response = await kakaoLogin(kakaoCode); if (response && response.accessToken) { - response.newMember ? navigate('/onboard') : navigate('/'); + if (response.newMember) { + navigate(`/signup`, { + state: { memberId: response.memberId }, + }); + } else { + setUser({ memberId: response.memberId, accessToken: response.accessToken }); + navigate('/'); + } } } catch (err) { if (err instanceof ResponseError) { @@ -38,6 +49,10 @@ const KakaoLogin = () => { handleKakaoLogin(); }, []); - return μΉ΄μΉ΄μ˜€λ‘œλ”©ν™”λ©΄; + return ( + + + + ); }; export default KakaoLogin; diff --git "a/src/routes/Auth/signup/\354\240\225\353\263\264\354\236\205\353\240\245.styles.tsx" b/src/routes/Auth/signup/Signup.styles.tsx similarity index 100% rename from "src/routes/Auth/signup/\354\240\225\353\263\264\354\236\205\353\240\245.styles.tsx" rename to src/routes/Auth/signup/Signup.styles.tsx diff --git a/src/routes/Auth/signup/Signup.tsx b/src/routes/Auth/signup/Signup.tsx index c3c6b11..9239c75 100644 --- a/src/routes/Auth/signup/Signup.tsx +++ b/src/routes/Auth/signup/Signup.tsx @@ -1,27 +1,144 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; +import { FormProvider, SubmitHandler, useForm } from 'react-hook-form'; import { useLocation } from 'react-router-dom'; +import { CONFIG, INPUT_TYPE } from 'src/constants/form'; +import { GENDERS, JOBS } from 'src/constants/signup'; -import useFunnel from '@hooks/useFunnel/useFunnel'; +import { SingnUpRequestDTO, useSignup } from '@apis/oauth/signup'; +import { Col } from '@components/commons/Flex/Flex'; +import InputField from '@components/commons/InputField/InputField'; +import Layout from '@components/commons/Layout/Layout'; +import RadioInput from '@components/commons/RadioInput/RadioInput'; +import SelectInput from '@components/commons/SelectInput/SelectInput'; +import Text from '@components/commons/Text/Text'; +import TextInput from '@components/commons/TextInput/TextInput'; +import useBottomSheet from '@hooks/useBottomSheet/useBottomSheet'; -import κ°€μž…μ„±κ³΅ from './κ°€μž…μ„±κ³΅'; -import μ •λ³΄μž…λ ₯ from './μ •λ³΄μž…λ ₯'; +import { colors } from '@styles/theme'; -const Signup = () => { - const steps = ['μ •λ³΄μž…λ ₯', 'κ°€μž…μ„±κ³΅']; +import { ResponseError } from '@apis/fetch'; + +import { FormContainer, NextButton } from './Signup.styles'; +import Terms from './Terms'; + +type SignupForm = Omit; - const [Funnel, setStep] = useFunnel(steps); +const MAX_NICKNAME_LENGTH = 8; + +const Signup = () => { const location = useLocation(); - const [registerData, setRegisterData] = useState(); + const methods = useForm>({ mode: 'onChange' }); + const signupMutation = useSignup(); + const { BottomSheet: TermsSheet, toggleSheet } = useBottomSheet({ + snapPoints: [0.5, 0.5, 0], + initialSnap: 0.5, + transparent: false, + }); + + const memberId = location.state.memberId as number; + + const birthdayInput = methods.watch(INPUT_TYPE.BIRTHDAY); + const nicknameProgress = methods.watch(INPUT_TYPE.NICKNAME) + ? `${methods.watch(INPUT_TYPE.NICKNAME)?.length}/${MAX_NICKNAME_LENGTH}` + : ''; + + const handleBirthdayInputKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Backspace' && (birthdayInput?.length === 6 || birthdayInput?.length === 9)) { + methods.setValue(INPUT_TYPE.BIRTHDAY, birthdayInput.slice(0, -2)); + } + if (e.key === 'Backspace' && (birthdayInput?.length === 5 || birthdayInput?.length === 8)) { + methods.setValue(INPUT_TYPE.BIRTHDAY, birthdayInput.slice(0, -1)); + } + }; + + const handleSubmitForm: SubmitHandler = async (data) => { + try { + await signupMutation.mutateAsync({ + ...data, + birth: data.birth.replace(/\//g, '-'), + memberId, + }); + + toggleSheet(); + } catch (error) { + if (error instanceof ResponseError) { + if (error.errorData.errorContent.hint.includes('PERSONAL_REGISTERED')) { + toggleSheet(); + } else { + alert(error.errorData.errorContent.message); + } + } + } + }; + + useEffect(() => { + if (birthdayInput?.length === 4 || birthdayInput?.length === 7) { + methods.setValue(INPUT_TYPE.BIRTHDAY, birthdayInput + '/'); + } + }, [birthdayInput, methods]); + + if (!memberId) { + return
잘λͺ»λœ μ ‘κ·Όμž…λ‹ˆλ‹€.
; + } return ( - - - <μ •λ³΄μž…λ ₯ memberId={location.state.memberId} /> - - - <κ°€μž…μ„±κ³΅ /> - - + ( + + νšŒμ›μ •λ³΄ μž…λ ₯ + + )} + > + + + + + ( + + {nicknameProgress} + + )} + /> + + + + + + + + + + + + + λ‹€μŒ + + + + + + + ); }; diff --git a/src/routes/Auth/signup/Terms.tsx b/src/routes/Auth/signup/Terms.tsx new file mode 100644 index 0000000..d7b1acb --- /dev/null +++ b/src/routes/Auth/signup/Terms.tsx @@ -0,0 +1,115 @@ +import React, { ChangeEvent, useState } from 'react'; +import { useAuthStore } from 'src/store/auth'; + +import { useTerms } from '@apis/oauth/signup'; +import Checkbox from '@components/commons/CheckBox/CheckBox'; +import { Col, Row } from '@components/commons/Flex/Flex'; +import Text from '@components/commons/Text/Text'; + +import { colors } from '@styles/theme'; + +import { NextButton } from './Signup.styles'; + +interface TermsProps { + memberId: number; +} + +const Terms = ({ memberId }: TermsProps) => { + const consentToTermMutation = useTerms(); + const setUser = useAuthStore((state) => state.setUser); + const [all, setAll] = useState(false); + const [consentToTerm, setConsentToTerm] = useState(false); + const [consentToCollectAndUseInfo, setConsentToCollectAndUseInfo] = useState(false); + const [consetToMarketing, setConsetToMarketing] = useState(false); + + const disabled = !consentToTerm || !consentToCollectAndUseInfo; + + const handleConsetAll = (e: ChangeEvent) => { + if (e.target.checked) { + setAll(true); + setConsentToTerm(true); + setConsentToCollectAndUseInfo(true); + setConsetToMarketing(true); + } else { + setAll(false); + setConsentToTerm(false); + setConsentToCollectAndUseInfo(false); + setConsetToMarketing(false); + } + }; + + const handleSubmitConsetToTerm = async () => { + if (disabled) return; + + const response = await consentToTermMutation.mutateAsync({ + memberId, + listen_marketing: consetToMarketing, + }); + setUser({ + memberId: response.memberId, + accessToken: response.accessToken, + }); + }; + + return ( + + + + + + λͺ¨λ‘ λ™μ˜ν•˜κΈ° + + + +
+ + setConsentToTerm(e.target.checked)} + > + + μ„œλΉ„μŠ€ 이용 μ•½κ΄€(ν•„μˆ˜) + + + + + setConsentToCollectAndUseInfo(e.target.checked)} + > + + κ°œμΈμ •λ³΄ μˆ˜μ§‘ 및 μ΄μš©λ™μ˜(ν•„μˆ˜) + + + + + setConsetToMarketing(e.target.checked)} + > + + λ§ˆμΌ€νŒ… 정보 μˆ˜μ‹  λ™μ˜(선택) + + + + + + {/* SAFE AREA */} + + λ‹€μŒ + + + + ); +}; + +export default Terms; diff --git "a/src/routes/Auth/signup/\352\260\200\354\236\205\354\204\261\352\263\265.styles.tsx" "b/src/routes/Auth/signup/\352\260\200\354\236\205\354\204\261\352\263\265.styles.tsx" deleted file mode 100644 index 844b157..0000000 --- "a/src/routes/Auth/signup/\352\260\200\354\236\205\354\204\261\352\263\265.styles.tsx" +++ /dev/null @@ -1,19 +0,0 @@ -import { styled } from 'styled-components'; - -export const Container = styled.div` - position: relative; - display: flex; - flex-direction: column; - justify-content: center; - height: 100vh; - background-color: ${(props) => props.theme.colors.navy}; -`; - -export const NextButton = styled.button` - width: 100%; - height: 57px; - color: #fff; - cursor: pointer; - background-color: #3c3457; - border-radius: 10px; -`; diff --git "a/src/routes/Auth/signup/\352\260\200\354\236\205\354\204\261\352\263\265.tsx" "b/src/routes/Auth/signup/\352\260\200\354\236\205\354\204\261\352\263\265.tsx" deleted file mode 100644 index 408cedf..0000000 --- "a/src/routes/Auth/signup/\352\260\200\354\236\205\354\204\261\352\263\265.tsx" +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; - -import { Container, NextButton } from './κ°€μž…μ„±κ³΅.styles'; - -const κ°€μž…μ„±κ³΅ = () => { - const handleNextButton = () => { - //λ‹€μŒλ²„νŠΌ λˆ„λ₯΄λ©΄ - }; - - return ( - - λ‹€μŒ - - ); -}; - -export default κ°€μž…μ„±κ³΅; diff --git "a/src/routes/Auth/signup/\354\240\225\353\263\264\354\236\205\353\240\245.tsx" "b/src/routes/Auth/signup/\354\240\225\353\263\264\354\236\205\353\240\245.tsx" deleted file mode 100644 index 42c8a92..0000000 --- "a/src/routes/Auth/signup/\354\240\225\353\263\264\354\236\205\353\240\245.tsx" +++ /dev/null @@ -1,105 +0,0 @@ -import React, { useEffect } from 'react'; -import { FormProvider, SubmitHandler, useForm } from 'react-hook-form'; -import { CONFIG, INPUT_TYPE } from 'src/constants/form'; -import { GENDERS, JOBS } from 'src/constants/signup'; - -import { SingnUpRequestDTO, useSignup } from '@apis/oauth/signup'; -import { Col } from '@components/commons/Flex/Flex'; -import InputField from '@components/commons/InputField/InputField'; -import Layout from '@components/commons/Layout/Layout'; -import RadioInput from '@components/commons/RadioInput/RadioInput'; -import SelectInput from '@components/commons/SelectInput/SelectInput'; -import Text from '@components/commons/Text/Text'; -import TextInput from '@components/commons/TextInput/TextInput'; - -import { colors } from '@styles/theme'; - -import { FormContainer, NextButton } from './μ •λ³΄μž…λ ₯.styles'; - -type SignupForm = Omit; - -const μ •λ³΄μž…λ ₯ = ({ memberId }: { memberId: number }) => { - const methods = useForm>({ mode: 'onChange' }); - const signupMutation = useSignup(); - - const birthdayInput = methods.watch(INPUT_TYPE.BIRTHDAY); - const nicknameProgress = methods.watch(INPUT_TYPE.NICKNAME) - ? `${methods.watch(INPUT_TYPE.NICKNAME)?.length}/8` - : ''; - - const handleBirthdayInputKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Backspace' && (birthdayInput?.length === 6 || birthdayInput?.length === 9)) { - methods.setValue(INPUT_TYPE.BIRTHDAY, birthdayInput.slice(0, -2)); - } - if (e.key === 'Backspace' && (birthdayInput?.length === 5 || birthdayInput?.length === 8)) { - methods.setValue(INPUT_TYPE.BIRTHDAY, birthdayInput.slice(0, -1)); - } - }; - - const handleSubmitForm: SubmitHandler = (data) => { - signupMutation.mutate({ ...data, memberId }); - }; - - useEffect(() => { - if (birthdayInput?.length === 4 || birthdayInput?.length === 7) { - methods.setValue(INPUT_TYPE.BIRTHDAY, birthdayInput + '/'); - } - }, [birthdayInput, methods]); - - return ( - ( - - νšŒμ›μ •λ³΄ μž…λ ₯ - - )} - > - - - - - ( - - {nicknameProgress} - - )} - /> - - - - - - - - - - - - λ‹€μŒ - - - - ); -}; - -export default μ •λ³΄μž…λ ₯; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 12441e1..d36c288 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { RouteObject, RouterProvider, createBrowserRouter } from 'react-router-dom'; +import { useAuthStore } from 'src/store/auth'; import GoogleLogin from './Auth/google/GoogleLogin'; import KakaoLogin from './Auth/kakao/KakaoLogin'; @@ -11,7 +12,8 @@ import TopicCreate from './Topic/TopicCreate'; import TopicSideSelection from './Topic/TopicSideSelection'; const Router = () => { - const [isAuthorized, setIsAuthorized] = React.useState(true); + const user = useAuthStore((state) => state.user); + const isAuthorized = user !== null; const authorizedRoutes: RouteObject[] = [ { diff --git a/src/store/auth.ts b/src/store/auth.ts new file mode 100644 index 0000000..13560b4 --- /dev/null +++ b/src/store/auth.ts @@ -0,0 +1,18 @@ +import { create } from 'zustand'; + +import { User } from '@interfaces/models/user'; + +interface AuthState { + user: User | null; +} + +interface AuthAction { + setUser: (user: User) => void; +} + +export const useAuthStore = create((set) => ({ + user: null, + setUser: (user: User) => { + set({ user: user }); + }, +})); diff --git a/yarn.lock b/yarn.lock index aef1611..0bc6545 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9368,6 +9368,11 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" +use-sync-external-store@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -9832,3 +9837,10 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zustand@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.0.tgz#141354af56f91de378aa6c4b930032ab338f3ef0" + integrity sha512-zlVFqS5TQ21nwijjhJlx4f9iGrXSL0o/+Dpy4txAP22miJ8Ti6c1Ol1RLNN98BMib83lmDH/2KmLwaNXpjrO1A== + dependencies: + use-sync-external-store "1.2.0"