From 749f8504c99c387ae75169e80ae1b804304d9858 Mon Sep 17 00:00:00 2001
From: chsua <113416448+chsua@users.noreply.github.com>
Date: Fri, 14 Jul 2023 13:21:08 +0900
Subject: [PATCH 1/4] =?UTF-8?q?=ED=97=A4=EB=8D=94=20=EC=BB=B4=ED=8F=AC?=
=?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84=5FFeat/#16=20(#39)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: (#16) 검색 아이콘 색상(검/흰) 분리
* feat: (#16) 검색바 컴포넌트 생성
* test: (#16) 검색바 컴포넌트 크기별 테스트
* feat: (#16) 탭, 모니터용 긴 헤더 컴포넌트 생성
* test: (#16) 탭, 모니터용 긴 헤더 컴포넌트 테스트 생성
* feat: (#16) 로고, 프로젝트명 버튼 컴포넌트 생성
* test: (#16) 로고, 프로젝트명 버튼 컴포넌트 테스트 생성
* refactor: 로고 버튼으로 기존 코드 변경
* feat: (#16) 모바일 버전 메인페이지 헤더 컴포넌트 생성
* test: (#16) 모바일 버전 메인페이지 헤더 컴포넌트 테스트 생성
* feat: (#16) 모바일용 짧은 헤더 템플릿 컴포넌트 생성
- 내용을 children 프롭스로 전달받도록 구현
- 상단 고정 등 공동의 스타일 공유하기 위해 컴포넌트 제작
* test: (#16) 모바일용 짧은 헤더 템플릿 컴포넌트 테스트
* design: (#16) 검색바 버튼 커서를 포인터로 수정
* test: (#16) 스토리명 파스칼케이스로 수정
* refactor: (#15) 로고 컴포넌트 프롭스명 수정
* test: (#15) 로고 컴포넌트 프롭스명 수정에 따른 테스트 수정
* style: (#15) 코드 컨벤션에 맞게 CSS 순서 정렬
* refactor: 짧은 기본 헤더 이름 수정
- NarrowOriginHeader > NarrowMainHeader
---
frontend/src/assets/search_black.svg | 3 ++
.../assets/{search.svg => search_white.svg} | 0
.../common/AddButton/AddButton.stories.tsx | 6 +--
.../HeaderTextButton.stories.tsx | 2 +-
.../common/IconButton/IconButton.stories.tsx | 6 +--
.../components/common/IconButton/index.tsx | 2 +-
.../common/LogoButton/LogoButton.stories.tsx | 25 ++++++++++
.../components/common/LogoButton/index.tsx | 47 +++++++++++++++++++
.../src/components/common/LogoButton/style.ts | 24 ++++++++++
.../NarrowMainHeader.stories.tsx | 15 ++++++
.../common/NarrowMainHeader/index.tsx | 14 ++++++
.../common/NarrowMainHeader/style.ts | 22 +++++++++
.../NarrowTemplateHeader.stories.tsx | 44 +++++++++++++++++
.../common/NarrowTemplateHeader/index.tsx | 7 +++
.../common/NarrowTemplateHeader/style.ts | 17 +++++++
.../common/SearchBar/SearchBar.stories.tsx | 26 ++++++++++
.../src/components/common/SearchBar/index.tsx | 22 +++++++++
.../src/components/common/SearchBar/style.ts | 47 +++++++++++++++++++
.../SquareButton/SquareButton.stories.tsx | 4 +-
.../common/WideHeader/WideHeader.stories.tsx | 14 ++++++
.../components/common/WideHeader/index.tsx | 13 +++++
.../src/components/common/WideHeader/style.ts | 25 ++++++++++
22 files changed, 375 insertions(+), 10 deletions(-)
create mode 100644 frontend/src/assets/search_black.svg
rename frontend/src/assets/{search.svg => search_white.svg} (100%)
create mode 100644 frontend/src/components/common/LogoButton/LogoButton.stories.tsx
create mode 100644 frontend/src/components/common/LogoButton/index.tsx
create mode 100644 frontend/src/components/common/LogoButton/style.ts
create mode 100644 frontend/src/components/common/NarrowMainHeader/NarrowMainHeader.stories.tsx
create mode 100644 frontend/src/components/common/NarrowMainHeader/index.tsx
create mode 100644 frontend/src/components/common/NarrowMainHeader/style.ts
create mode 100644 frontend/src/components/common/NarrowTemplateHeader/NarrowTemplateHeader.stories.tsx
create mode 100644 frontend/src/components/common/NarrowTemplateHeader/index.tsx
create mode 100644 frontend/src/components/common/NarrowTemplateHeader/style.ts
create mode 100644 frontend/src/components/common/SearchBar/SearchBar.stories.tsx
create mode 100644 frontend/src/components/common/SearchBar/index.tsx
create mode 100644 frontend/src/components/common/SearchBar/style.ts
create mode 100644 frontend/src/components/common/WideHeader/WideHeader.stories.tsx
create mode 100644 frontend/src/components/common/WideHeader/index.tsx
create mode 100644 frontend/src/components/common/WideHeader/style.ts
diff --git a/frontend/src/assets/search_black.svg b/frontend/src/assets/search_black.svg
new file mode 100644
index 000000000..5e4b6ac25
--- /dev/null
+++ b/frontend/src/assets/search_black.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/src/assets/search.svg b/frontend/src/assets/search_white.svg
similarity index 100%
rename from frontend/src/assets/search.svg
rename to frontend/src/assets/search_white.svg
diff --git a/frontend/src/components/common/AddButton/AddButton.stories.tsx b/frontend/src/components/common/AddButton/AddButton.stories.tsx
index 693af5bc1..0888f10a2 100644
--- a/frontend/src/components/common/AddButton/AddButton.stories.tsx
+++ b/frontend/src/components/common/AddButton/AddButton.stories.tsx
@@ -9,14 +9,14 @@ const meta: Meta = {
export default meta;
type Story = StoryObj;
-export const size_S: Story = {
+export const SizeS: Story = {
render: () => ,
};
-export const size_M: Story = {
+export const sizeM: Story = {
render: () => ,
};
-export const size_L: Story = {
+export const sizeL: Story = {
render: () => ,
};
diff --git a/frontend/src/components/common/HeaderTextButton/HeaderTextButton.stories.tsx b/frontend/src/components/common/HeaderTextButton/HeaderTextButton.stories.tsx
index fc25cc703..cca319fbd 100644
--- a/frontend/src/components/common/HeaderTextButton/HeaderTextButton.stories.tsx
+++ b/frontend/src/components/common/HeaderTextButton/HeaderTextButton.stories.tsx
@@ -9,6 +9,6 @@ const meta: Meta = {
export default meta;
type Story = StoryObj;
-export const defaultButton: Story = {
+export const DefaultButton: Story = {
render: () => 확 인,
};
diff --git a/frontend/src/components/common/IconButton/IconButton.stories.tsx b/frontend/src/components/common/IconButton/IconButton.stories.tsx
index 102c0dd79..d071655c2 100644
--- a/frontend/src/components/common/IconButton/IconButton.stories.tsx
+++ b/frontend/src/components/common/IconButton/IconButton.stories.tsx
@@ -10,14 +10,14 @@ const meta: Meta = {
export default meta;
type Story = StoryObj;
-export const category: Story = {
+export const Category: Story = {
render: () => ,
};
-export const back: Story = {
+export const Back: Story = {
render: () => ,
};
-export const search: Story = {
+export const Search: Story = {
render: () => ,
};
diff --git a/frontend/src/components/common/IconButton/index.tsx b/frontend/src/components/common/IconButton/index.tsx
index 21a9631b1..1fcf7f9cd 100644
--- a/frontend/src/components/common/IconButton/index.tsx
+++ b/frontend/src/components/common/IconButton/index.tsx
@@ -2,7 +2,7 @@ import { ButtonHTMLAttributes } from 'react';
import backIcon from '@assets/back.svg';
import categoryIcon from '@assets/category.svg';
-import searchIcon from '@assets/search.svg';
+import searchIcon from '@assets/search_white.svg';
import * as S from './style';
diff --git a/frontend/src/components/common/LogoButton/LogoButton.stories.tsx b/frontend/src/components/common/LogoButton/LogoButton.stories.tsx
new file mode 100644
index 000000000..a174f863f
--- /dev/null
+++ b/frontend/src/components/common/LogoButton/LogoButton.stories.tsx
@@ -0,0 +1,25 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import LogoButton from '.';
+
+const meta: Meta = {
+ component: LogoButton,
+ decorators: [
+ storyFn => {storyFn()}
,
+ ],
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Icon: Story = {
+ render: () => ,
+};
+
+export const Text: Story = {
+ render: () => ,
+};
+
+export const Full: Story = {
+ render: () => ,
+};
diff --git a/frontend/src/components/common/LogoButton/index.tsx b/frontend/src/components/common/LogoButton/index.tsx
new file mode 100644
index 000000000..d3ae88b39
--- /dev/null
+++ b/frontend/src/components/common/LogoButton/index.tsx
@@ -0,0 +1,47 @@
+import { ButtonHTMLAttributes } from 'react';
+
+import logo from '@assets/logo.svg';
+import votogether from '@assets/projectName.svg';
+
+import * as S from './style';
+
+type Content = 'icon' | 'text' | 'full';
+
+const contentCategory: { [key in Content]: { name: string; url: string } } = {
+ icon: {
+ name: '로고 아이콘',
+ url: logo,
+ },
+ text: {
+ name: 'votogether',
+ url: votogether,
+ },
+ full: {
+ name: 'votogether',
+ url: '',
+ },
+};
+
+interface LogoButtonProps extends ButtonHTMLAttributes {
+ content: Content;
+}
+
+export default function LogoButton({ content, ...rest }: LogoButtonProps) {
+ const src = contentCategory[content].url;
+ const ariaLabelText = contentCategory[content].name;
+
+ if (content === 'full') {
+ return (
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/frontend/src/components/common/LogoButton/style.ts b/frontend/src/components/common/LogoButton/style.ts
new file mode 100644
index 000000000..8ebdbf50d
--- /dev/null
+++ b/frontend/src/components/common/LogoButton/style.ts
@@ -0,0 +1,24 @@
+import { styled } from 'styled-components';
+
+type Content = 'icon' | 'text' | 'full';
+
+export const Button = styled.button<{ content: Content }>`
+ display: flex;
+ align-items: center;
+ gap: 10px;
+
+ background-color: rgba(0, 0, 0, 0);
+
+ height: 100%;
+
+ cursor: pointer;
+
+ & :first-child {
+ height: 100%;
+ border-radius: 5px;
+ }
+
+ & :last-child {
+ height: ${props => props.content !== 'icon' && '60%'};
+ }
+`;
diff --git a/frontend/src/components/common/NarrowMainHeader/NarrowMainHeader.stories.tsx b/frontend/src/components/common/NarrowMainHeader/NarrowMainHeader.stories.tsx
new file mode 100644
index 000000000..cf0410cba
--- /dev/null
+++ b/frontend/src/components/common/NarrowMainHeader/NarrowMainHeader.stories.tsx
@@ -0,0 +1,15 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import NarrowMainHeader from '.';
+
+const meta: Meta = {
+ component: NarrowMainHeader,
+ decorators: [storyFn => {storyFn()}
],
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Primary: Story = {
+ render: () => ,
+};
diff --git a/frontend/src/components/common/NarrowMainHeader/index.tsx b/frontend/src/components/common/NarrowMainHeader/index.tsx
new file mode 100644
index 000000000..3e783b531
--- /dev/null
+++ b/frontend/src/components/common/NarrowMainHeader/index.tsx
@@ -0,0 +1,14 @@
+import IconButton from '../IconButton';
+import LogoButton from '../LogoButton';
+
+import * as S from './style';
+
+export default function NarrowMainHeader() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/common/NarrowMainHeader/style.ts b/frontend/src/components/common/NarrowMainHeader/style.ts
new file mode 100644
index 000000000..ef5271a4a
--- /dev/null
+++ b/frontend/src/components/common/NarrowMainHeader/style.ts
@@ -0,0 +1,22 @@
+import { styled } from 'styled-components';
+
+export const Container = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 20px;
+
+ width: 100%;
+ height: 55px;
+ padding: 0 20px;
+
+ position: absolute;
+ top: 0;
+
+ background-color: #1f1f1f;
+
+ & :nth-child(2) {
+ margin-right: auto;
+ height: 60%;
+ }
+`;
diff --git a/frontend/src/components/common/NarrowTemplateHeader/NarrowTemplateHeader.stories.tsx b/frontend/src/components/common/NarrowTemplateHeader/NarrowTemplateHeader.stories.tsx
new file mode 100644
index 000000000..8a95c5871
--- /dev/null
+++ b/frontend/src/components/common/NarrowTemplateHeader/NarrowTemplateHeader.stories.tsx
@@ -0,0 +1,44 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import NarrowTemplateHeader from '.';
+
+const meta: Meta = {
+ component: NarrowTemplateHeader,
+ decorators: [storyFn => {storyFn()}
],
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const BothSideHeader: Story = {
+ render: () => (
+
+ 예시
+ 예시
+
+ ),
+};
+
+export const ThreeComponentHeaderLeft: Story = {
+ render: () => (
+
+
+ 예시
+
+ ),
+};
+
+export const ThreeComponentHeaderRight: Story = {
+ render: () => (
+
+ 예시
+
+
+ ),
+};
diff --git a/frontend/src/components/common/NarrowTemplateHeader/index.tsx b/frontend/src/components/common/NarrowTemplateHeader/index.tsx
new file mode 100644
index 000000000..0726aac35
--- /dev/null
+++ b/frontend/src/components/common/NarrowTemplateHeader/index.tsx
@@ -0,0 +1,7 @@
+import { ReactNode } from 'react';
+
+import * as S from './style';
+
+export default function NarrowTemplateHeader({ children }: { children: ReactNode }) {
+ return {children};
+}
diff --git a/frontend/src/components/common/NarrowTemplateHeader/style.ts b/frontend/src/components/common/NarrowTemplateHeader/style.ts
new file mode 100644
index 000000000..867fa2d34
--- /dev/null
+++ b/frontend/src/components/common/NarrowTemplateHeader/style.ts
@@ -0,0 +1,17 @@
+import { styled } from 'styled-components';
+
+export const Container = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 20px;
+
+ width: 100%;
+ height: 55px;
+ padding: 0 20px;
+
+ position: absolute;
+ top: 0;
+
+ background-color: #1f1f1f;
+`;
diff --git a/frontend/src/components/common/SearchBar/SearchBar.stories.tsx b/frontend/src/components/common/SearchBar/SearchBar.stories.tsx
new file mode 100644
index 000000000..f9c14b8b4
--- /dev/null
+++ b/frontend/src/components/common/SearchBar/SearchBar.stories.tsx
@@ -0,0 +1,26 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import SearchBar from '.';
+
+const meta: Meta = {
+ component: SearchBar,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const SizeSm: Story = {
+ render: () => ,
+};
+
+export const SizeMd: Story = {
+ render: () => ,
+};
+
+export const SizeLg: Story = {
+ render: () => ,
+};
+
+export const SizeFree: Story = {
+ render: () => ,
+};
diff --git a/frontend/src/components/common/SearchBar/index.tsx b/frontend/src/components/common/SearchBar/index.tsx
new file mode 100644
index 000000000..309f6b57b
--- /dev/null
+++ b/frontend/src/components/common/SearchBar/index.tsx
@@ -0,0 +1,22 @@
+import { FormHTMLAttributes } from 'react';
+
+import searchIcon from '@assets/search_black.svg';
+
+import { Size } from '../AddButton/type';
+
+import * as S from './style';
+
+interface SearchBarProps extends FormHTMLAttributes {
+ size: Size | 'free';
+}
+
+export default function SearchBar({ size, ...rest }: SearchBarProps) {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/common/SearchBar/style.ts b/frontend/src/components/common/SearchBar/style.ts
new file mode 100644
index 000000000..aea53b4c9
--- /dev/null
+++ b/frontend/src/components/common/SearchBar/style.ts
@@ -0,0 +1,47 @@
+import { styled } from 'styled-components';
+
+import { Size } from '../AddButton/type';
+
+interface SearchBarProps {
+ size: Size | 'free';
+}
+
+const formSize = {
+ sm: '170px',
+ md: '250px',
+ lg: '400px',
+};
+
+export const Form = styled.form`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 5px;
+
+ width: ${props => (props.size === 'free' ? '100%' : formSize[props.size])};
+ height: 36px;
+ padding: 5px 10px;
+ border-radius: 5px;
+
+ background-color: #cccccc;
+ color: red;
+
+ font-size: 1rem;
+`;
+
+export const Input = styled.input`
+ width: 100%;
+ height: 100%;
+ outline: 0;
+
+ background-color: rgba(0, 0, 0, 0);
+
+ font-size: 14px;
+ letter-spacing: 1px;
+`;
+
+export const Button = styled.button`
+ background-color: rgba(0, 0, 0, 0);
+
+ cursor: pointer;
+`;
diff --git a/frontend/src/components/common/SquareButton/SquareButton.stories.tsx b/frontend/src/components/common/SquareButton/SquareButton.stories.tsx
index 911ced831..4836088d0 100644
--- a/frontend/src/components/common/SquareButton/SquareButton.stories.tsx
+++ b/frontend/src/components/common/SquareButton/SquareButton.stories.tsx
@@ -10,10 +10,10 @@ const meta: Meta = {
export default meta;
type Story = StoryObj;
-export const color_blank: Story = {
+export const ColorBlank: Story = {
render: () => 확 인,
};
-export const color_fill: Story = {
+export const ColorFill: Story = {
render: () => 버 튼,
};
diff --git a/frontend/src/components/common/WideHeader/WideHeader.stories.tsx b/frontend/src/components/common/WideHeader/WideHeader.stories.tsx
new file mode 100644
index 000000000..8a5573d46
--- /dev/null
+++ b/frontend/src/components/common/WideHeader/WideHeader.stories.tsx
@@ -0,0 +1,14 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import WideHeader from '.';
+
+const meta: Meta = {
+ component: WideHeader,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Primary: Story = {
+ render: () => ,
+};
diff --git a/frontend/src/components/common/WideHeader/index.tsx b/frontend/src/components/common/WideHeader/index.tsx
new file mode 100644
index 000000000..42db7d696
--- /dev/null
+++ b/frontend/src/components/common/WideHeader/index.tsx
@@ -0,0 +1,13 @@
+import LogoButton from '../LogoButton';
+import SearchBar from '../SearchBar';
+
+import * as S from './style';
+
+export default function WideHeader() {
+ return (
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/common/WideHeader/style.ts b/frontend/src/components/common/WideHeader/style.ts
new file mode 100644
index 000000000..c8c025f32
--- /dev/null
+++ b/frontend/src/components/common/WideHeader/style.ts
@@ -0,0 +1,25 @@
+import { styled } from 'styled-components';
+
+export const Container = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ width: 100%;
+ height: 70px;
+
+ position: absolute;
+ top: 0;
+
+ background-color: #1f1f1f;
+
+ padding: 0 80px;
+
+ & :first-child {
+ height: 70%;
+
+ & :last-child {
+ height: 40%;
+ }
+ }
+`;
From 60a16c84b178e8a3614384919553e353bd0d3af7 Mon Sep 17 00:00:00 2001
From: chsua <113416448+chsua@users.noreply.github.com>
Date: Fri, 14 Jul 2023 15:17:06 +0900
Subject: [PATCH 2/4] =?UTF-8?q?=EB=A6=AC=EC=95=A1=ED=8A=B8=20=EC=BF=BC?=
=?UTF-8?q?=EB=A6=AC,=20msw=20=EC=84=A4=EC=B9=98=20=EB=B0=8F=20=EC=85=8B?=
=?UTF-8?q?=ED=8C=85=20(#48)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: (#46) msw, react-query, .env 적용
- 스토리북에도 적용
* feat: (#46) react-query 앱 컴포넌트에 적용
* refactor: (#46) env파일 삭제
* chore: (#46) env파일 gitignore에 추가
---
frontend/.gitignore | 3 +-
frontend/.storybook/main.ts | 1 +
frontend/.storybook/preview.tsx | 18 +-
.../.storybook/public/mockServiceWorker.js | 303 +++++
frontend/.storybook/webpack.config.js | 17 +
frontend/package-lock.json | 1106 ++++++++++++++++-
frontend/package.json | 8 +
frontend/public/mockServiceWorker.js | 303 +++++
frontend/src/App.tsx | 8 +-
frontend/src/index.tsx | 9 +-
frontend/src/mocks/example/get.ts | 7 +
frontend/src/mocks/handlers.ts | 3 +
frontend/src/mocks/worker.ts | 5 +
frontend/webpack.common.js | 4 +-
14 files changed, 1785 insertions(+), 10 deletions(-)
create mode 100644 frontend/.storybook/public/mockServiceWorker.js
create mode 100644 frontend/.storybook/webpack.config.js
create mode 100644 frontend/public/mockServiceWorker.js
create mode 100644 frontend/src/mocks/example/get.ts
create mode 100644 frontend/src/mocks/handlers.ts
create mode 100644 frontend/src/mocks/worker.ts
diff --git a/frontend/.gitignore b/frontend/.gitignore
index 30bc16279..7af7f0475 100644
--- a/frontend/.gitignore
+++ b/frontend/.gitignore
@@ -1 +1,2 @@
-/node_modules
\ No newline at end of file
+/node_modules
+.env
\ No newline at end of file
diff --git a/frontend/.storybook/main.ts b/frontend/.storybook/main.ts
index f75be279a..d47f8e0c4 100644
--- a/frontend/.storybook/main.ts
+++ b/frontend/.storybook/main.ts
@@ -33,5 +33,6 @@ const config: StorybookConfig = {
);
return config;
},
+ staticDirs: ['./public'],
};
export default config;
diff --git a/frontend/.storybook/preview.tsx b/frontend/.storybook/preview.tsx
index 03d1a9c88..d35ff88c1 100644
--- a/frontend/.storybook/preview.tsx
+++ b/frontend/.storybook/preview.tsx
@@ -1,7 +1,13 @@
import type { Preview } from '@storybook/react';
+import { initialize, mswDecorator } from 'msw-storybook-addon';
+
import { GlobalStyle } from '../src/styles/globalStyle';
import React from 'react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+
+const queryClient = new QueryClient();
+initialize();
const preview: Preview = {
parameters: {
@@ -14,13 +20,21 @@ const preview: Preview = {
},
},
decorators: [
+ mswDecorator,
Story => (
<>
-
-
+
+
+
+
>
),
],
};
+if (typeof global.process === 'undefined') {
+ const { worker } = require('../src/mocks/worker');
+ worker.start();
+}
+
export default preview;
diff --git a/frontend/.storybook/public/mockServiceWorker.js b/frontend/.storybook/public/mockServiceWorker.js
new file mode 100644
index 000000000..8ee70b3e4
--- /dev/null
+++ b/frontend/.storybook/public/mockServiceWorker.js
@@ -0,0 +1,303 @@
+/* eslint-disable */
+/* tslint:disable */
+
+/**
+ * Mock Service Worker (1.2.2).
+ * @see https://github.com/mswjs/msw
+ * - Please do NOT modify this file.
+ * - Please do NOT serve this file on production.
+ */
+
+const INTEGRITY_CHECKSUM = '3d6b9f06410d179a7f7404d4bf4c3c70'
+const activeClientIds = new Set()
+
+self.addEventListener('install', function () {
+ self.skipWaiting()
+})
+
+self.addEventListener('activate', function (event) {
+ event.waitUntil(self.clients.claim())
+})
+
+self.addEventListener('message', async function (event) {
+ const clientId = event.source.id
+
+ if (!clientId || !self.clients) {
+ return
+ }
+
+ const client = await self.clients.get(clientId)
+
+ if (!client) {
+ return
+ }
+
+ const allClients = await self.clients.matchAll({
+ type: 'window',
+ })
+
+ switch (event.data) {
+ case 'KEEPALIVE_REQUEST': {
+ sendToClient(client, {
+ type: 'KEEPALIVE_RESPONSE',
+ })
+ break
+ }
+
+ case 'INTEGRITY_CHECK_REQUEST': {
+ sendToClient(client, {
+ type: 'INTEGRITY_CHECK_RESPONSE',
+ payload: INTEGRITY_CHECKSUM,
+ })
+ break
+ }
+
+ case 'MOCK_ACTIVATE': {
+ activeClientIds.add(clientId)
+
+ sendToClient(client, {
+ type: 'MOCKING_ENABLED',
+ payload: true,
+ })
+ break
+ }
+
+ case 'MOCK_DEACTIVATE': {
+ activeClientIds.delete(clientId)
+ break
+ }
+
+ case 'CLIENT_CLOSED': {
+ activeClientIds.delete(clientId)
+
+ const remainingClients = allClients.filter((client) => {
+ return client.id !== clientId
+ })
+
+ // Unregister itself when there are no more clients
+ if (remainingClients.length === 0) {
+ self.registration.unregister()
+ }
+
+ break
+ }
+ }
+})
+
+self.addEventListener('fetch', function (event) {
+ const { request } = event
+ const accept = request.headers.get('accept') || ''
+
+ // Bypass server-sent events.
+ if (accept.includes('text/event-stream')) {
+ return
+ }
+
+ // Bypass navigation requests.
+ if (request.mode === 'navigate') {
+ return
+ }
+
+ // Opening the DevTools triggers the "only-if-cached" request
+ // that cannot be handled by the worker. Bypass such requests.
+ if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
+ return
+ }
+
+ // Bypass all requests when there are no active clients.
+ // Prevents the self-unregistered worked from handling requests
+ // after it's been deleted (still remains active until the next reload).
+ if (activeClientIds.size === 0) {
+ return
+ }
+
+ // Generate unique request ID.
+ const requestId = Math.random().toString(16).slice(2)
+
+ event.respondWith(
+ handleRequest(event, requestId).catch((error) => {
+ if (error.name === 'NetworkError') {
+ console.warn(
+ '[MSW] Successfully emulated a network error for the "%s %s" request.',
+ request.method,
+ request.url,
+ )
+ return
+ }
+
+ // At this point, any exception indicates an issue with the original request/response.
+ console.error(
+ `\
+[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`,
+ request.method,
+ request.url,
+ `${error.name}: ${error.message}`,
+ )
+ }),
+ )
+})
+
+async function handleRequest(event, requestId) {
+ const client = await resolveMainClient(event)
+ const response = await getResponse(event, client, requestId)
+
+ // Send back the response clone for the "response:*" life-cycle events.
+ // Ensure MSW is active and ready to handle the message, otherwise
+ // this message will pend indefinitely.
+ if (client && activeClientIds.has(client.id)) {
+ ;(async function () {
+ const clonedResponse = response.clone()
+ sendToClient(client, {
+ type: 'RESPONSE',
+ payload: {
+ requestId,
+ type: clonedResponse.type,
+ ok: clonedResponse.ok,
+ status: clonedResponse.status,
+ statusText: clonedResponse.statusText,
+ body:
+ clonedResponse.body === null ? null : await clonedResponse.text(),
+ headers: Object.fromEntries(clonedResponse.headers.entries()),
+ redirected: clonedResponse.redirected,
+ },
+ })
+ })()
+ }
+
+ return response
+}
+
+// Resolve the main client for the given event.
+// Client that issues a request doesn't necessarily equal the client
+// that registered the worker. It's with the latter the worker should
+// communicate with during the response resolving phase.
+async function resolveMainClient(event) {
+ const client = await self.clients.get(event.clientId)
+
+ if (client?.frameType === 'top-level') {
+ return client
+ }
+
+ const allClients = await self.clients.matchAll({
+ type: 'window',
+ })
+
+ return allClients
+ .filter((client) => {
+ // Get only those clients that are currently visible.
+ return client.visibilityState === 'visible'
+ })
+ .find((client) => {
+ // Find the client ID that's recorded in the
+ // set of clients that have registered the worker.
+ return activeClientIds.has(client.id)
+ })
+}
+
+async function getResponse(event, client, requestId) {
+ const { request } = event
+ const clonedRequest = request.clone()
+
+ function passthrough() {
+ // Clone the request because it might've been already used
+ // (i.e. its body has been read and sent to the client).
+ const headers = Object.fromEntries(clonedRequest.headers.entries())
+
+ // Remove MSW-specific request headers so the bypassed requests
+ // comply with the server's CORS preflight check.
+ // Operate with the headers as an object because request "Headers"
+ // are immutable.
+ delete headers['x-msw-bypass']
+
+ return fetch(clonedRequest, { headers })
+ }
+
+ // Bypass mocking when the client is not active.
+ if (!client) {
+ return passthrough()
+ }
+
+ // Bypass initial page load requests (i.e. static assets).
+ // The absence of the immediate/parent client in the map of the active clients
+ // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
+ // and is not ready to handle requests.
+ if (!activeClientIds.has(client.id)) {
+ return passthrough()
+ }
+
+ // Bypass requests with the explicit bypass header.
+ // Such requests can be issued by "ctx.fetch()".
+ if (request.headers.get('x-msw-bypass') === 'true') {
+ return passthrough()
+ }
+
+ // Notify the client that a request has been intercepted.
+ const clientMessage = await sendToClient(client, {
+ type: 'REQUEST',
+ payload: {
+ id: requestId,
+ url: request.url,
+ method: request.method,
+ headers: Object.fromEntries(request.headers.entries()),
+ cache: request.cache,
+ mode: request.mode,
+ credentials: request.credentials,
+ destination: request.destination,
+ integrity: request.integrity,
+ redirect: request.redirect,
+ referrer: request.referrer,
+ referrerPolicy: request.referrerPolicy,
+ body: await request.text(),
+ bodyUsed: request.bodyUsed,
+ keepalive: request.keepalive,
+ },
+ })
+
+ switch (clientMessage.type) {
+ case 'MOCK_RESPONSE': {
+ return respondWithMock(clientMessage.data)
+ }
+
+ case 'MOCK_NOT_FOUND': {
+ return passthrough()
+ }
+
+ case 'NETWORK_ERROR': {
+ const { name, message } = clientMessage.data
+ const networkError = new Error(message)
+ networkError.name = name
+
+ // Rejecting a "respondWith" promise emulates a network error.
+ throw networkError
+ }
+ }
+
+ return passthrough()
+}
+
+function sendToClient(client, message) {
+ return new Promise((resolve, reject) => {
+ const channel = new MessageChannel()
+
+ channel.port1.onmessage = (event) => {
+ if (event.data && event.data.error) {
+ return reject(event.data.error)
+ }
+
+ resolve(event.data)
+ }
+
+ client.postMessage(message, [channel.port2])
+ })
+}
+
+function sleep(timeMs) {
+ return new Promise((resolve) => {
+ setTimeout(resolve, timeMs)
+ })
+}
+
+async function respondWithMock(response) {
+ await sleep(response.delay)
+ return new Response(response.body, response)
+}
diff --git a/frontend/.storybook/webpack.config.js b/frontend/.storybook/webpack.config.js
new file mode 100644
index 000000000..7eca295d9
--- /dev/null
+++ b/frontend/.storybook/webpack.config.js
@@ -0,0 +1,17 @@
+const path = require('path');
+const Dotenv = require('dotenv-webpack');
+
+const envPath =
+ process.env.NODE_ENV === 'development'
+ ? path.join(__dirname, '../webpack/.env.development')
+ : path.join(__dirname, '../webpack/.env.production');
+
+module.exports = ({ config }) => {
+ config.plugins.push(
+ new Dotenv({
+ path: envPath,
+ })
+ );
+
+ return config;
+};
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index f748625d3..f0c1cbcbb 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -9,6 +9,8 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
+ "@tanstack/react-query": "^4.29.19",
+ "dotenv": "^16.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.14.1"
@@ -30,6 +32,7 @@
"@types/react-dom": "^18.2.6",
"@types/styled-components": "^5.1.26",
"clean-webpack-plugin": "^4.0.0",
+ "dotenv-webpack": "^8.0.1",
"eslint": "^8.44.0",
"eslint-config-prettier": "^8.8.0",
"eslint-config-react-app": "^7.0.1",
@@ -38,6 +41,8 @@
"html-webpack-plugin": "^5.5.3",
"jest": "^29.6.0",
"jest-environment-jsdom": "^29.6.0",
+ "msw": "^1.2.2",
+ "msw-storybook-addon": "^1.8.0",
"prettier": "^2.8.8",
"storybook": "^7.0.26",
"styled-components": "^6.0.2",
@@ -3890,6 +3895,47 @@
"react": ">=16"
}
},
+ "node_modules/@mswjs/cookies": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@mswjs/cookies/-/cookies-0.2.2.tgz",
+ "integrity": "sha512-mlN83YSrcFgk7Dm1Mys40DLssI1KdJji2CMKN8eOlBqsTADYzj2+jWzsANsUTFbxDMWPD5e9bfA1RGqBpS3O1g==",
+ "dev": true,
+ "dependencies": {
+ "@types/set-cookie-parser": "^2.4.0",
+ "set-cookie-parser": "^2.4.6"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@mswjs/interceptors": {
+ "version": "0.17.9",
+ "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.17.9.tgz",
+ "integrity": "sha512-4LVGt03RobMH/7ZrbHqRxQrS9cc2uh+iNKSj8UWr8M26A2i793ju+csaB5zaqYltqJmA2jUq4VeYfKmVqvsXQg==",
+ "dev": true,
+ "dependencies": {
+ "@open-draft/until": "^1.0.3",
+ "@types/debug": "^4.1.7",
+ "@xmldom/xmldom": "^0.8.3",
+ "debug": "^4.3.3",
+ "headers-polyfill": "^3.1.0",
+ "outvariant": "^1.2.1",
+ "strict-event-emitter": "^0.2.4",
+ "web-encoding": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@mswjs/interceptors/node_modules/strict-event-emitter": {
+ "version": "0.2.8",
+ "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.2.8.tgz",
+ "integrity": "sha512-KDf/ujU8Zud3YaLtMCcTI4xkZlZVIYxTLr+XIULexP+77EEVWixeXroLUXQXiVtH4XH2W7jr/3PT1v3zBuvc3A==",
+ "dev": true,
+ "dependencies": {
+ "events": "^3.3.0"
+ }
+ },
"node_modules/@ndelangen/get-tarball": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@ndelangen/get-tarball/-/get-tarball-3.0.9.tgz",
@@ -3961,6 +4007,12 @@
"node": ">= 8"
}
},
+ "node_modules/@open-draft/until": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-1.0.3.tgz",
+ "integrity": "sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==",
+ "dev": true
+ },
"node_modules/@pmmmwh/react-refresh-webpack-plugin": {
"version": "0.5.10",
"resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz",
@@ -6812,6 +6864,41 @@
"url": "https://opencollective.com/storybook"
}
},
+ "node_modules/@tanstack/query-core": {
+ "version": "4.29.19",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.29.19.tgz",
+ "integrity": "sha512-uPe1DukeIpIHpQi6UzIgBcXsjjsDaLnc7hF+zLBKnaUlh7jFE/A+P8t4cU4VzKPMFB/C970n/9SxtpO5hmIRgw==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "4.29.19",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.29.19.tgz",
+ "integrity": "sha512-XiTIOHHQ5Cw1WUlHaD4fmVUMhoWjuNJlAeJGq7eM4BraI5z7y8WkZO+NR8PSuRnQGblpuVdjClQbDFtwxTtTUw==",
+ "dependencies": {
+ "@tanstack/query-core": "4.29.19",
+ "use-sync-external-store": "^1.2.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-native": "*"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@testing-library/dom": {
"version": "8.20.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz",
@@ -7136,6 +7223,21 @@
"@types/node": "*"
}
},
+ "node_modules/@types/cookie": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
+ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==",
+ "dev": true
+ },
+ "node_modules/@types/debug": {
+ "version": "4.1.8",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz",
+ "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/ms": "*"
+ }
+ },
"node_modules/@types/detect-port": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@types/detect-port/-/detect-port-1.3.3.tgz",
@@ -7332,6 +7434,12 @@
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
"dev": true
},
+ "node_modules/@types/js-levenshtein": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@types/js-levenshtein/-/js-levenshtein-1.1.1.tgz",
+ "integrity": "sha512-qC4bCqYGy1y/NP7dDVr7KJarn+PbX1nSpwA7JXdu0HxT3QYjO8MJ+cntENtHFVy2dRAyBV23OZ6MxsW1AM1L8g==",
+ "dev": true
+ },
"node_modules/@types/jsdom": {
"version": "20.0.1",
"resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz",
@@ -7385,6 +7493,12 @@
"integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
"dev": true
},
+ "node_modules/@types/ms": {
+ "version": "0.7.31",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
+ "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==",
+ "dev": true
+ },
"node_modules/@types/node": {
"version": "20.3.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.3.tgz",
@@ -7517,6 +7631,15 @@
"@types/node": "*"
}
},
+ "node_modules/@types/set-cookie-parser": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/@types/set-cookie-parser/-/set-cookie-parser-2.4.2.tgz",
+ "integrity": "sha512-fBZgytwhYAUkj/jC/FAV4RQ5EerRup1YQsXQCh8rZfiHkc4UahC192oH0smGwsXol3cL3A5oETuAHeQHmhXM4w==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/sockjs": {
"version": "0.3.33",
"resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz",
@@ -8081,6 +8204,15 @@
}
}
},
+ "node_modules/@xmldom/xmldom": {
+ "version": "0.8.9",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.9.tgz",
+ "integrity": "sha512-4VSbbcMoxc4KLjb1gs96SRmi7w4h1SF+fCoiK0XaQX62buCc1G5d0DC5bJ9xJBNPDSVCmIrcl8BiYxzjrqaaJA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/@xtuc/ieee754": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@@ -8108,6 +8240,13 @@
"esbuild": ">=0.10.0"
}
},
+ "node_modules/@zxing/text-encoding": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
+ "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==",
+ "dev": true,
+ "optional": true
+ },
"node_modules/abab": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
@@ -9357,6 +9496,12 @@
"node": ">=10"
}
},
+ "node_modules/chardet": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
+ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
+ "dev": true
+ },
"node_modules/chokidar": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
@@ -9498,6 +9643,15 @@
"@colors/colors": "1.5.0"
}
},
+ "node_modules/cli-width": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz",
+ "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
"node_modules/cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
@@ -10531,7 +10685,6 @@
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
"integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
- "dev": true,
"engines": {
"node": ">=12"
},
@@ -10539,6 +10692,24 @@
"url": "https://github.com/motdotla/dotenv?sponsor=1"
}
},
+ "node_modules/dotenv-defaults": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/dotenv-defaults/-/dotenv-defaults-2.0.2.tgz",
+ "integrity": "sha512-iOIzovWfsUHU91L5i8bJce3NYK5JXeAwH50Jh6+ARUdLiiGlYWfGw6UkzsYqaXZH/hjE/eCd/PlfM/qqyK0AMg==",
+ "dev": true,
+ "dependencies": {
+ "dotenv": "^8.2.0"
+ }
+ },
+ "node_modules/dotenv-defaults/node_modules/dotenv": {
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz",
+ "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/dotenv-expand": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz",
@@ -10548,6 +10719,21 @@
"node": ">=12"
}
},
+ "node_modules/dotenv-webpack": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/dotenv-webpack/-/dotenv-webpack-8.0.1.tgz",
+ "integrity": "sha512-CdrgfhZOnx4uB18SgaoP9XHRN2v48BbjuXQsZY5ixs5A8579NxQkmMxRtI7aTwSiSQcM2ao12Fdu+L3ZS3bG4w==",
+ "dev": true,
+ "dependencies": {
+ "dotenv-defaults": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "webpack": "^4 || ^5"
+ }
+ },
"node_modules/duplexify": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
@@ -11834,6 +12020,20 @@
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"dev": true
},
+ "node_modules/external-editor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
+ "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
+ "dev": true,
+ "dependencies": {
+ "chardet": "^0.7.0",
+ "iconv-lite": "^0.4.24",
+ "tmp": "^0.0.33"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/extract-zip": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz",
@@ -11958,6 +12158,21 @@
"integrity": "sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ==",
"dev": true
},
+ "node_modules/figures": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
+ "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
+ "dev": true,
+ "dependencies": {
+ "escape-string-regexp": "^1.0.5"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -12807,6 +13022,15 @@
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true
},
+ "node_modules/graphql": {
+ "version": "16.7.1",
+ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.7.1.tgz",
+ "integrity": "sha512-DRYR9tf+UGU0KOsMcKAlXeFfX89UiiIZ0dRU3mR0yJfu6OjZqUcp68NnFLnqQU5RexygFoDy1EW+ccOYcPfmHg==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
+ }
+ },
"node_modules/gunzip-maybe": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz",
@@ -12947,6 +13171,12 @@
"he": "bin/he"
}
},
+ "node_modules/headers-polyfill": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-3.1.2.tgz",
+ "integrity": "sha512-tWCK4biJ6hcLqTviLXVR9DTRfYGQMXEIUj3gwJ2rZ5wO/at3XtkI4g8mCvFdUF9l1KMBNCfmNAdnahm1cgavQA==",
+ "dev": true
+ },
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@@ -13340,6 +13570,102 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
+ "node_modules/inquirer": {
+ "version": "8.2.5",
+ "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz",
+ "integrity": "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^4.1.1",
+ "cli-cursor": "^3.1.0",
+ "cli-width": "^3.0.0",
+ "external-editor": "^3.0.3",
+ "figures": "^3.0.0",
+ "lodash": "^4.17.21",
+ "mute-stream": "0.0.8",
+ "ora": "^5.4.1",
+ "run-async": "^2.4.0",
+ "rxjs": "^7.5.5",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0",
+ "through": "^2.3.6",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/inquirer/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/inquirer/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/inquirer/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/inquirer/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/inquirer/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/inquirer/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/internal-slot": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz",
@@ -13632,6 +13958,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-node-process": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz",
+ "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==",
+ "dev": true
+ },
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -16335,6 +16667,15 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
+ "node_modules/js-levenshtein": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
+ "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -17213,6 +17554,202 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
+ "node_modules/msw": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/msw/-/msw-1.2.2.tgz",
+ "integrity": "sha512-GsW3PE/Es/a1tYThXcM8YHOZ1S1MtivcS3He/LQbbTCx3rbWJYCtWD5XXyJ53KlNPT7O1VI9sCW3xMtgFe8XpQ==",
+ "dev": true,
+ "hasInstallScript": true,
+ "dependencies": {
+ "@mswjs/cookies": "^0.2.2",
+ "@mswjs/interceptors": "^0.17.5",
+ "@open-draft/until": "^1.0.3",
+ "@types/cookie": "^0.4.1",
+ "@types/js-levenshtein": "^1.1.1",
+ "chalk": "4.1.1",
+ "chokidar": "^3.4.2",
+ "cookie": "^0.4.2",
+ "graphql": "^15.0.0 || ^16.0.0",
+ "headers-polyfill": "^3.1.2",
+ "inquirer": "^8.2.0",
+ "is-node-process": "^1.2.0",
+ "js-levenshtein": "^1.1.6",
+ "node-fetch": "^2.6.7",
+ "outvariant": "^1.4.0",
+ "path-to-regexp": "^6.2.0",
+ "strict-event-emitter": "^0.4.3",
+ "type-fest": "^2.19.0",
+ "yargs": "^17.3.1"
+ },
+ "bin": {
+ "msw": "cli/index.js"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mswjs"
+ },
+ "peerDependencies": {
+ "typescript": ">= 4.4.x <= 5.1.x"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/msw-storybook-addon": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/msw-storybook-addon/-/msw-storybook-addon-1.8.0.tgz",
+ "integrity": "sha512-dw3vZwqjixmiur0vouRSOax7wPSu9Og2Hspy9JZFHf49bZRjwDiLF0Pfn2NXEkGviYJOJiGxS1ejoTiUwoSg4A==",
+ "dev": true,
+ "dependencies": {
+ "is-node-process": "^1.0.1"
+ },
+ "peerDependencies": {
+ "msw": ">=0.35.0 <2.0.0"
+ }
+ },
+ "node_modules/msw/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/msw/node_modules/chalk": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
+ "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/msw/node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/msw/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/msw/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/msw/node_modules/cookie": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
+ "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/msw/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/msw/node_modules/path-to-regexp": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz",
+ "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==",
+ "dev": true
+ },
+ "node_modules/msw/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/msw/node_modules/type-fest": {
+ "version": "2.19.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
+ "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/msw/node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/msw/node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/multicast-dns": {
"version": "7.2.5",
"resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz",
@@ -17226,6 +17763,12 @@
"multicast-dns": "cli.js"
}
},
+ "node_modules/mute-stream": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
+ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
+ "dev": true
+ },
"node_modules/nanoid": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
@@ -17703,6 +18246,21 @@
"node": ">=8"
}
},
+ "node_modules/os-tmpdir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/outvariant": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.0.tgz",
+ "integrity": "sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw==",
+ "dev": true
+ },
"node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
@@ -19039,6 +19597,15 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/run-async": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
+ "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -19062,6 +19629,15 @@
"queue-microtask": "^1.2.2"
}
},
+ "node_modules/rxjs": {
+ "version": "7.8.1",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
+ "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
+ "dev": true,
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -19349,6 +19925,12 @@
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"dev": true
},
+ "node_modules/set-cookie-parser": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz",
+ "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==",
+ "dev": true
+ },
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -19711,6 +20293,12 @@
"integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==",
"dev": true
},
+ "node_modules/strict-event-emitter": {
+ "version": "0.4.6",
+ "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.4.6.tgz",
+ "integrity": "sha512-12KWeb+wixJohmnwNFerbyiBrAlq5qJLwIt38etRtKtmmHyDSoGlIqFE9wx+4IwG0aDjI7GV8tc8ZccjWZZtTg==",
+ "dev": true
+ },
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -20316,6 +20904,12 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
+ "node_modules/through": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
+ "dev": true
+ },
"node_modules/through2": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
@@ -20362,6 +20956,18 @@
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
"dev": true
},
+ "node_modules/tmp": {
+ "version": "0.0.33",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
+ "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+ "dev": true,
+ "dependencies": {
+ "os-tmpdir": "~1.0.2"
+ },
+ "engines": {
+ "node": ">=0.6.0"
+ }
+ },
"node_modules/tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -21013,6 +21619,14 @@
"react-dom": "16.8.0 - 18"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
@@ -21141,6 +21755,18 @@
"defaults": "^1.0.3"
}
},
+ "node_modules/web-encoding": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/web-encoding/-/web-encoding-1.1.5.tgz",
+ "integrity": "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==",
+ "dev": true,
+ "dependencies": {
+ "util": "^0.12.3"
+ },
+ "optionalDependencies": {
+ "@zxing/text-encoding": "0.9.0"
+ }
+ },
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
@@ -24391,6 +25017,43 @@
"@types/react": ">=16"
}
},
+ "@mswjs/cookies": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@mswjs/cookies/-/cookies-0.2.2.tgz",
+ "integrity": "sha512-mlN83YSrcFgk7Dm1Mys40DLssI1KdJji2CMKN8eOlBqsTADYzj2+jWzsANsUTFbxDMWPD5e9bfA1RGqBpS3O1g==",
+ "dev": true,
+ "requires": {
+ "@types/set-cookie-parser": "^2.4.0",
+ "set-cookie-parser": "^2.4.6"
+ }
+ },
+ "@mswjs/interceptors": {
+ "version": "0.17.9",
+ "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.17.9.tgz",
+ "integrity": "sha512-4LVGt03RobMH/7ZrbHqRxQrS9cc2uh+iNKSj8UWr8M26A2i793ju+csaB5zaqYltqJmA2jUq4VeYfKmVqvsXQg==",
+ "dev": true,
+ "requires": {
+ "@open-draft/until": "^1.0.3",
+ "@types/debug": "^4.1.7",
+ "@xmldom/xmldom": "^0.8.3",
+ "debug": "^4.3.3",
+ "headers-polyfill": "^3.1.0",
+ "outvariant": "^1.2.1",
+ "strict-event-emitter": "^0.2.4",
+ "web-encoding": "^1.1.5"
+ },
+ "dependencies": {
+ "strict-event-emitter": {
+ "version": "0.2.8",
+ "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.2.8.tgz",
+ "integrity": "sha512-KDf/ujU8Zud3YaLtMCcTI4xkZlZVIYxTLr+XIULexP+77EEVWixeXroLUXQXiVtH4XH2W7jr/3PT1v3zBuvc3A==",
+ "dev": true,
+ "requires": {
+ "events": "^3.3.0"
+ }
+ }
+ }
+ },
"@ndelangen/get-tarball": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@ndelangen/get-tarball/-/get-tarball-3.0.9.tgz",
@@ -24450,6 +25113,12 @@
"fastq": "^1.6.0"
}
},
+ "@open-draft/until": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-1.0.3.tgz",
+ "integrity": "sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==",
+ "dev": true
+ },
"@pmmmwh/react-refresh-webpack-plugin": {
"version": "0.5.10",
"resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz",
@@ -26513,6 +27182,20 @@
"file-system-cache": "2.3.0"
}
},
+ "@tanstack/query-core": {
+ "version": "4.29.19",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.29.19.tgz",
+ "integrity": "sha512-uPe1DukeIpIHpQi6UzIgBcXsjjsDaLnc7hF+zLBKnaUlh7jFE/A+P8t4cU4VzKPMFB/C970n/9SxtpO5hmIRgw=="
+ },
+ "@tanstack/react-query": {
+ "version": "4.29.19",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.29.19.tgz",
+ "integrity": "sha512-XiTIOHHQ5Cw1WUlHaD4fmVUMhoWjuNJlAeJGq7eM4BraI5z7y8WkZO+NR8PSuRnQGblpuVdjClQbDFtwxTtTUw==",
+ "requires": {
+ "@tanstack/query-core": "4.29.19",
+ "use-sync-external-store": "^1.2.0"
+ }
+ },
"@testing-library/dom": {
"version": "8.20.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz",
@@ -26776,6 +27459,21 @@
"@types/node": "*"
}
},
+ "@types/cookie": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
+ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==",
+ "dev": true
+ },
+ "@types/debug": {
+ "version": "4.1.8",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz",
+ "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==",
+ "dev": true,
+ "requires": {
+ "@types/ms": "*"
+ }
+ },
"@types/detect-port": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@types/detect-port/-/detect-port-1.3.3.tgz",
@@ -26965,6 +27663,12 @@
}
}
},
+ "@types/js-levenshtein": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@types/js-levenshtein/-/js-levenshtein-1.1.1.tgz",
+ "integrity": "sha512-qC4bCqYGy1y/NP7dDVr7KJarn+PbX1nSpwA7JXdu0HxT3QYjO8MJ+cntENtHFVy2dRAyBV23OZ6MxsW1AM1L8g==",
+ "dev": true
+ },
"@types/jsdom": {
"version": "20.0.1",
"resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz",
@@ -27018,6 +27722,12 @@
"integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
"dev": true
},
+ "@types/ms": {
+ "version": "0.7.31",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
+ "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==",
+ "dev": true
+ },
"@types/node": {
"version": "20.3.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.3.tgz",
@@ -27150,6 +27860,15 @@
"@types/node": "*"
}
},
+ "@types/set-cookie-parser": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/@types/set-cookie-parser/-/set-cookie-parser-2.4.2.tgz",
+ "integrity": "sha512-fBZgytwhYAUkj/jC/FAV4RQ5EerRup1YQsXQCh8rZfiHkc4UahC192oH0smGwsXol3cL3A5oETuAHeQHmhXM4w==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@types/sockjs": {
"version": "0.3.33",
"resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz",
@@ -27571,6 +28290,12 @@
"dev": true,
"requires": {}
},
+ "@xmldom/xmldom": {
+ "version": "0.8.9",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.9.tgz",
+ "integrity": "sha512-4VSbbcMoxc4KLjb1gs96SRmi7w4h1SF+fCoiK0XaQX62buCc1G5d0DC5bJ9xJBNPDSVCmIrcl8BiYxzjrqaaJA==",
+ "dev": true
+ },
"@xtuc/ieee754": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@@ -27592,6 +28317,13 @@
"tslib": "^2.4.0"
}
},
+ "@zxing/text-encoding": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
+ "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==",
+ "dev": true,
+ "optional": true
+ },
"abab": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
@@ -28523,6 +29255,12 @@
"integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==",
"dev": true
},
+ "chardet": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
+ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
+ "dev": true
+ },
"chokidar": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
@@ -28612,6 +29350,12 @@
"string-width": "^4.2.0"
}
},
+ "cli-width": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz",
+ "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==",
+ "dev": true
+ },
"cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
@@ -29437,8 +30181,24 @@
"dotenv": {
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
- "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
- "dev": true
+ "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ=="
+ },
+ "dotenv-defaults": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/dotenv-defaults/-/dotenv-defaults-2.0.2.tgz",
+ "integrity": "sha512-iOIzovWfsUHU91L5i8bJce3NYK5JXeAwH50Jh6+ARUdLiiGlYWfGw6UkzsYqaXZH/hjE/eCd/PlfM/qqyK0AMg==",
+ "dev": true,
+ "requires": {
+ "dotenv": "^8.2.0"
+ },
+ "dependencies": {
+ "dotenv": {
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz",
+ "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==",
+ "dev": true
+ }
+ }
},
"dotenv-expand": {
"version": "10.0.0",
@@ -29446,6 +30206,15 @@
"integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==",
"dev": true
},
+ "dotenv-webpack": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/dotenv-webpack/-/dotenv-webpack-8.0.1.tgz",
+ "integrity": "sha512-CdrgfhZOnx4uB18SgaoP9XHRN2v48BbjuXQsZY5ixs5A8579NxQkmMxRtI7aTwSiSQcM2ao12Fdu+L3ZS3bG4w==",
+ "dev": true,
+ "requires": {
+ "dotenv-defaults": "^2.0.2"
+ }
+ },
"duplexify": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
@@ -30441,6 +31210,17 @@
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"dev": true
},
+ "external-editor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
+ "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
+ "dev": true,
+ "requires": {
+ "chardet": "^0.7.0",
+ "iconv-lite": "^0.4.24",
+ "tmp": "^0.0.33"
+ }
+ },
"extract-zip": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz",
@@ -30555,6 +31335,15 @@
"integrity": "sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ==",
"dev": true
},
+ "figures": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
+ "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
+ "dev": true,
+ "requires": {
+ "escape-string-regexp": "^1.0.5"
+ }
+ },
"file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -31195,6 +31984,12 @@
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true
},
+ "graphql": {
+ "version": "16.7.1",
+ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.7.1.tgz",
+ "integrity": "sha512-DRYR9tf+UGU0KOsMcKAlXeFfX89UiiIZ0dRU3mR0yJfu6OjZqUcp68NnFLnqQU5RexygFoDy1EW+ccOYcPfmHg==",
+ "dev": true
+ },
"gunzip-maybe": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz",
@@ -31291,6 +32086,12 @@
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true
},
+ "headers-polyfill": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-3.1.2.tgz",
+ "integrity": "sha512-tWCK4biJ6hcLqTviLXVR9DTRfYGQMXEIUj3gwJ2rZ5wO/at3XtkI4g8mCvFdUF9l1KMBNCfmNAdnahm1cgavQA==",
+ "dev": true
+ },
"hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@@ -31573,6 +32374,80 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
+ "inquirer": {
+ "version": "8.2.5",
+ "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz",
+ "integrity": "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==",
+ "dev": true,
+ "requires": {
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^4.1.1",
+ "cli-cursor": "^3.1.0",
+ "cli-width": "^3.0.0",
+ "external-editor": "^3.0.3",
+ "figures": "^3.0.0",
+ "lodash": "^4.17.21",
+ "mute-stream": "0.0.8",
+ "ora": "^5.4.1",
+ "run-async": "^2.4.0",
+ "rxjs": "^7.5.5",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0",
+ "through": "^2.3.6",
+ "wrap-ansi": "^7.0.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
"internal-slot": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz",
@@ -31769,6 +32644,12 @@
"integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
"dev": true
},
+ "is-node-process": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz",
+ "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==",
+ "dev": true
+ },
"is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -33776,6 +34657,12 @@
}
}
},
+ "js-levenshtein": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
+ "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==",
+ "dev": true
+ },
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -34458,6 +35345,143 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
+ "msw": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/msw/-/msw-1.2.2.tgz",
+ "integrity": "sha512-GsW3PE/Es/a1tYThXcM8YHOZ1S1MtivcS3He/LQbbTCx3rbWJYCtWD5XXyJ53KlNPT7O1VI9sCW3xMtgFe8XpQ==",
+ "dev": true,
+ "requires": {
+ "@mswjs/cookies": "^0.2.2",
+ "@mswjs/interceptors": "^0.17.5",
+ "@open-draft/until": "^1.0.3",
+ "@types/cookie": "^0.4.1",
+ "@types/js-levenshtein": "^1.1.1",
+ "chalk": "4.1.1",
+ "chokidar": "^3.4.2",
+ "cookie": "^0.4.2",
+ "graphql": "^15.0.0 || ^16.0.0",
+ "headers-polyfill": "^3.1.2",
+ "inquirer": "^8.2.0",
+ "is-node-process": "^1.2.0",
+ "js-levenshtein": "^1.1.6",
+ "node-fetch": "^2.6.7",
+ "outvariant": "^1.4.0",
+ "path-to-regexp": "^6.2.0",
+ "strict-event-emitter": "^0.4.3",
+ "type-fest": "^2.19.0",
+ "yargs": "^17.3.1"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "chalk": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
+ "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "requires": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "cookie": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
+ "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
+ "path-to-regexp": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz",
+ "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ },
+ "type-fest": {
+ "version": "2.19.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
+ "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
+ "dev": true
+ },
+ "yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "requires": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ }
+ },
+ "yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true
+ }
+ }
+ },
+ "msw-storybook-addon": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/msw-storybook-addon/-/msw-storybook-addon-1.8.0.tgz",
+ "integrity": "sha512-dw3vZwqjixmiur0vouRSOax7wPSu9Og2Hspy9JZFHf49bZRjwDiLF0Pfn2NXEkGviYJOJiGxS1ejoTiUwoSg4A==",
+ "dev": true,
+ "requires": {
+ "is-node-process": "^1.0.1"
+ }
+ },
"multicast-dns": {
"version": "7.2.5",
"resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz",
@@ -34468,6 +35492,12 @@
"thunky": "^1.0.2"
}
},
+ "mute-stream": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
+ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
+ "dev": true
+ },
"nanoid": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
@@ -34819,6 +35849,18 @@
}
}
},
+ "os-tmpdir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
+ "dev": true
+ },
+ "outvariant": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.0.tgz",
+ "integrity": "sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw==",
+ "dev": true
+ },
"p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
@@ -35815,6 +36857,12 @@
}
}
},
+ "run-async": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
+ "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==",
+ "dev": true
+ },
"run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -35824,6 +36872,15 @@
"queue-microtask": "^1.2.2"
}
},
+ "rxjs": {
+ "version": "7.8.1",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
+ "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
+ "dev": true,
+ "requires": {
+ "tslib": "^2.1.0"
+ }
+ },
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -36065,6 +37122,12 @@
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"dev": true
},
+ "set-cookie-parser": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz",
+ "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==",
+ "dev": true
+ },
"setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -36358,6 +37421,12 @@
"integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==",
"dev": true
},
+ "strict-event-emitter": {
+ "version": "0.4.6",
+ "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.4.6.tgz",
+ "integrity": "sha512-12KWeb+wixJohmnwNFerbyiBrAlq5qJLwIt38etRtKtmmHyDSoGlIqFE9wx+4IwG0aDjI7GV8tc8ZccjWZZtTg==",
+ "dev": true
+ },
"string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -36803,6 +37872,12 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
+ "through": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
+ "dev": true
+ },
"through2": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
@@ -36851,6 +37926,15 @@
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
"dev": true
},
+ "tmp": {
+ "version": "0.0.33",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
+ "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+ "dev": true,
+ "requires": {
+ "os-tmpdir": "~1.0.2"
+ }
+ },
"tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -37327,6 +38411,12 @@
"@juggle/resize-observer": "^3.3.1"
}
},
+ "use-sync-external-store": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+ "requires": {}
+ },
"util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
@@ -37437,6 +38527,16 @@
"defaults": "^1.0.3"
}
},
+ "web-encoding": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/web-encoding/-/web-encoding-1.1.5.tgz",
+ "integrity": "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==",
+ "dev": true,
+ "requires": {
+ "@zxing/text-encoding": "0.9.0",
+ "util": "^0.12.3"
+ }
+ },
"webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 4a44b6126..f4f92b520 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -16,6 +16,8 @@
"test": "jest"
},
"dependencies": {
+ "@tanstack/react-query": "^4.29.19",
+ "dotenv": "^16.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.14.1"
@@ -44,6 +46,7 @@
"@types/react-dom": "^18.2.6",
"@types/styled-components": "^5.1.26",
"clean-webpack-plugin": "^4.0.0",
+ "dotenv-webpack": "^8.0.1",
"eslint": "^8.44.0",
"eslint-config-prettier": "^8.8.0",
"eslint-config-react-app": "^7.0.1",
@@ -52,6 +55,8 @@
"html-webpack-plugin": "^5.5.3",
"jest": "^29.6.0",
"jest-environment-jsdom": "^29.6.0",
+ "msw": "^1.2.2",
+ "msw-storybook-addon": "^1.8.0",
"prettier": "^2.8.8",
"storybook": "^7.0.26",
"styled-components": "^6.0.2",
@@ -62,5 +67,8 @@
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1",
"webpack-merge": "^5.9.0"
+ },
+ "msw": {
+ "workerDirectory": "public"
}
}
diff --git a/frontend/public/mockServiceWorker.js b/frontend/public/mockServiceWorker.js
new file mode 100644
index 000000000..8ee70b3e4
--- /dev/null
+++ b/frontend/public/mockServiceWorker.js
@@ -0,0 +1,303 @@
+/* eslint-disable */
+/* tslint:disable */
+
+/**
+ * Mock Service Worker (1.2.2).
+ * @see https://github.com/mswjs/msw
+ * - Please do NOT modify this file.
+ * - Please do NOT serve this file on production.
+ */
+
+const INTEGRITY_CHECKSUM = '3d6b9f06410d179a7f7404d4bf4c3c70'
+const activeClientIds = new Set()
+
+self.addEventListener('install', function () {
+ self.skipWaiting()
+})
+
+self.addEventListener('activate', function (event) {
+ event.waitUntil(self.clients.claim())
+})
+
+self.addEventListener('message', async function (event) {
+ const clientId = event.source.id
+
+ if (!clientId || !self.clients) {
+ return
+ }
+
+ const client = await self.clients.get(clientId)
+
+ if (!client) {
+ return
+ }
+
+ const allClients = await self.clients.matchAll({
+ type: 'window',
+ })
+
+ switch (event.data) {
+ case 'KEEPALIVE_REQUEST': {
+ sendToClient(client, {
+ type: 'KEEPALIVE_RESPONSE',
+ })
+ break
+ }
+
+ case 'INTEGRITY_CHECK_REQUEST': {
+ sendToClient(client, {
+ type: 'INTEGRITY_CHECK_RESPONSE',
+ payload: INTEGRITY_CHECKSUM,
+ })
+ break
+ }
+
+ case 'MOCK_ACTIVATE': {
+ activeClientIds.add(clientId)
+
+ sendToClient(client, {
+ type: 'MOCKING_ENABLED',
+ payload: true,
+ })
+ break
+ }
+
+ case 'MOCK_DEACTIVATE': {
+ activeClientIds.delete(clientId)
+ break
+ }
+
+ case 'CLIENT_CLOSED': {
+ activeClientIds.delete(clientId)
+
+ const remainingClients = allClients.filter((client) => {
+ return client.id !== clientId
+ })
+
+ // Unregister itself when there are no more clients
+ if (remainingClients.length === 0) {
+ self.registration.unregister()
+ }
+
+ break
+ }
+ }
+})
+
+self.addEventListener('fetch', function (event) {
+ const { request } = event
+ const accept = request.headers.get('accept') || ''
+
+ // Bypass server-sent events.
+ if (accept.includes('text/event-stream')) {
+ return
+ }
+
+ // Bypass navigation requests.
+ if (request.mode === 'navigate') {
+ return
+ }
+
+ // Opening the DevTools triggers the "only-if-cached" request
+ // that cannot be handled by the worker. Bypass such requests.
+ if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
+ return
+ }
+
+ // Bypass all requests when there are no active clients.
+ // Prevents the self-unregistered worked from handling requests
+ // after it's been deleted (still remains active until the next reload).
+ if (activeClientIds.size === 0) {
+ return
+ }
+
+ // Generate unique request ID.
+ const requestId = Math.random().toString(16).slice(2)
+
+ event.respondWith(
+ handleRequest(event, requestId).catch((error) => {
+ if (error.name === 'NetworkError') {
+ console.warn(
+ '[MSW] Successfully emulated a network error for the "%s %s" request.',
+ request.method,
+ request.url,
+ )
+ return
+ }
+
+ // At this point, any exception indicates an issue with the original request/response.
+ console.error(
+ `\
+[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`,
+ request.method,
+ request.url,
+ `${error.name}: ${error.message}`,
+ )
+ }),
+ )
+})
+
+async function handleRequest(event, requestId) {
+ const client = await resolveMainClient(event)
+ const response = await getResponse(event, client, requestId)
+
+ // Send back the response clone for the "response:*" life-cycle events.
+ // Ensure MSW is active and ready to handle the message, otherwise
+ // this message will pend indefinitely.
+ if (client && activeClientIds.has(client.id)) {
+ ;(async function () {
+ const clonedResponse = response.clone()
+ sendToClient(client, {
+ type: 'RESPONSE',
+ payload: {
+ requestId,
+ type: clonedResponse.type,
+ ok: clonedResponse.ok,
+ status: clonedResponse.status,
+ statusText: clonedResponse.statusText,
+ body:
+ clonedResponse.body === null ? null : await clonedResponse.text(),
+ headers: Object.fromEntries(clonedResponse.headers.entries()),
+ redirected: clonedResponse.redirected,
+ },
+ })
+ })()
+ }
+
+ return response
+}
+
+// Resolve the main client for the given event.
+// Client that issues a request doesn't necessarily equal the client
+// that registered the worker. It's with the latter the worker should
+// communicate with during the response resolving phase.
+async function resolveMainClient(event) {
+ const client = await self.clients.get(event.clientId)
+
+ if (client?.frameType === 'top-level') {
+ return client
+ }
+
+ const allClients = await self.clients.matchAll({
+ type: 'window',
+ })
+
+ return allClients
+ .filter((client) => {
+ // Get only those clients that are currently visible.
+ return client.visibilityState === 'visible'
+ })
+ .find((client) => {
+ // Find the client ID that's recorded in the
+ // set of clients that have registered the worker.
+ return activeClientIds.has(client.id)
+ })
+}
+
+async function getResponse(event, client, requestId) {
+ const { request } = event
+ const clonedRequest = request.clone()
+
+ function passthrough() {
+ // Clone the request because it might've been already used
+ // (i.e. its body has been read and sent to the client).
+ const headers = Object.fromEntries(clonedRequest.headers.entries())
+
+ // Remove MSW-specific request headers so the bypassed requests
+ // comply with the server's CORS preflight check.
+ // Operate with the headers as an object because request "Headers"
+ // are immutable.
+ delete headers['x-msw-bypass']
+
+ return fetch(clonedRequest, { headers })
+ }
+
+ // Bypass mocking when the client is not active.
+ if (!client) {
+ return passthrough()
+ }
+
+ // Bypass initial page load requests (i.e. static assets).
+ // The absence of the immediate/parent client in the map of the active clients
+ // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
+ // and is not ready to handle requests.
+ if (!activeClientIds.has(client.id)) {
+ return passthrough()
+ }
+
+ // Bypass requests with the explicit bypass header.
+ // Such requests can be issued by "ctx.fetch()".
+ if (request.headers.get('x-msw-bypass') === 'true') {
+ return passthrough()
+ }
+
+ // Notify the client that a request has been intercepted.
+ const clientMessage = await sendToClient(client, {
+ type: 'REQUEST',
+ payload: {
+ id: requestId,
+ url: request.url,
+ method: request.method,
+ headers: Object.fromEntries(request.headers.entries()),
+ cache: request.cache,
+ mode: request.mode,
+ credentials: request.credentials,
+ destination: request.destination,
+ integrity: request.integrity,
+ redirect: request.redirect,
+ referrer: request.referrer,
+ referrerPolicy: request.referrerPolicy,
+ body: await request.text(),
+ bodyUsed: request.bodyUsed,
+ keepalive: request.keepalive,
+ },
+ })
+
+ switch (clientMessage.type) {
+ case 'MOCK_RESPONSE': {
+ return respondWithMock(clientMessage.data)
+ }
+
+ case 'MOCK_NOT_FOUND': {
+ return passthrough()
+ }
+
+ case 'NETWORK_ERROR': {
+ const { name, message } = clientMessage.data
+ const networkError = new Error(message)
+ networkError.name = name
+
+ // Rejecting a "respondWith" promise emulates a network error.
+ throw networkError
+ }
+ }
+
+ return passthrough()
+}
+
+function sendToClient(client, message) {
+ return new Promise((resolve, reject) => {
+ const channel = new MessageChannel()
+
+ channel.port1.onmessage = (event) => {
+ if (event.data && event.data.error) {
+ return reject(event.data.error)
+ }
+
+ resolve(event.data)
+ }
+
+ client.postMessage(message, [channel.port2])
+ })
+}
+
+function sleep(timeMs) {
+ return new Promise((resolve) => {
+ setTimeout(resolve, timeMs)
+ })
+}
+
+async function respondWithMock(response) {
+ await sleep(response.delay)
+ return new Response(response.body, response)
+}
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index f8c506c6e..c6df8dd8c 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,12 +1,16 @@
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+
import Home from '@pages/Home';
import Example from '@components/Example';
+const queryClient = new QueryClient();
+
const App = () => (
- <>
+
- >
+
);
export default App;
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx
index 90b7aadbe..e494a44a0 100644
--- a/frontend/src/index.tsx
+++ b/frontend/src/index.tsx
@@ -1,7 +1,14 @@
-import App from './App';
import React from 'react';
+
import ReactDOM from 'react-dom/client';
+import App from './App';
+import { worker } from './mocks/worker';
+
+if (process.env.NODE_ENV === 'development') {
+ worker.start();
+}
+
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
diff --git a/frontend/src/mocks/example/get.ts b/frontend/src/mocks/example/get.ts
new file mode 100644
index 000000000..7e54b94c4
--- /dev/null
+++ b/frontend/src/mocks/example/get.ts
@@ -0,0 +1,7 @@
+import { rest } from 'msw';
+
+export const example = [
+ rest.get('/example', (req, res, ctx) => {
+ return res(ctx.status(200));
+ }),
+];
diff --git a/frontend/src/mocks/handlers.ts b/frontend/src/mocks/handlers.ts
new file mode 100644
index 000000000..f6e89fef8
--- /dev/null
+++ b/frontend/src/mocks/handlers.ts
@@ -0,0 +1,3 @@
+import { example } from './example/get';
+
+export const handlers = [...example];
diff --git a/frontend/src/mocks/worker.ts b/frontend/src/mocks/worker.ts
new file mode 100644
index 000000000..9c10cad95
--- /dev/null
+++ b/frontend/src/mocks/worker.ts
@@ -0,0 +1,5 @@
+import { setupWorker } from 'msw';
+
+import { handlers } from './handlers';
+
+export const worker = setupWorker(...handlers);
diff --git a/frontend/webpack.common.js b/frontend/webpack.common.js
index 65fdb5edd..e91a13e9d 100644
--- a/frontend/webpack.common.js
+++ b/frontend/webpack.common.js
@@ -1,6 +1,7 @@
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
+const DotenvWebpack = require('dotenv-webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
@@ -52,10 +53,11 @@ module.exports = {
template: './public/index.html',
}),
new CleanWebpackPlugin(),
+ new DotenvWebpack(),
],
devtool: 'inline-source-map',
devServer: {
- static: './dist',
+ static: 'public',
hot: true,
open: true,
},
From d3ffc40c0d40d64d93cd554f8f443e7827e0df66 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EA=B9=80=EC=98=81=EA=B8=B8/KIM=20YOUNG=20GIL?=
<80146176+Gilpop8663@users.noreply.github.com>
Date: Fri, 14 Jul 2023 17:34:05 +0900
Subject: [PATCH 3/4] =?UTF-8?q?fetch=20=EC=9C=A0=ED=8B=B8=20=ED=95=A8?=
=?UTF-8?q?=EC=88=98=20=EA=B5=AC=ED=98=84,=20=EC=98=88=EC=8B=9C=20useQuery?=
=?UTF-8?q?=20=ED=9B=85=20=EA=B5=AC=ED=98=84=20,=20=EC=98=88=EC=8B=9C=20ap?=
=?UTF-8?q?i=20=ED=95=A8=EC=88=98=20=EA=B5=AC=ED=98=84=20=20(#51)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: (#49) fetch 유틸 함수 구현 (GET, POST, PUT, PATCH, DELETE)
* feat: (#49) api 폴더 안에 컨벤션이 될 예시 함수 구현
* feat: (#49) 컨벤션이 될 예시 useQuery 훅 구현
---
frontend/src/api/example.ts | 37 ++++++++++++
frontend/src/hooks/query/useExample.tsx | 19 ++++++
frontend/src/utils/fetch.ts | 77 +++++++++++++++++++++++++
3 files changed, 133 insertions(+)
create mode 100644 frontend/src/api/example.ts
create mode 100644 frontend/src/hooks/query/useExample.tsx
create mode 100644 frontend/src/utils/fetch.ts
diff --git a/frontend/src/api/example.ts b/frontend/src/api/example.ts
new file mode 100644
index 000000000..00a060f29
--- /dev/null
+++ b/frontend/src/api/example.ts
@@ -0,0 +1,37 @@
+// 장바구니을 가져오는 예시 코드
+
+/**
+ * 게시물 get: api/cart
+ * 게시물 Cart: api/cart
+ */
+import { deleteFetch, getFetch, patchFetch, postFetch, putFetch } from '@utils/fetch';
+
+export interface Cart {
+ id: number;
+ text: string;
+}
+
+export const getCartList = async () => {
+ return await getFetch('api/cart');
+};
+
+export const createCart = async () => {
+ return await postFetch('api/cart', { id: 12, text: '생성' });
+};
+
+export const editCart = async () => {
+ return await putFetch('api/cart', { id: 12, text: '생성' });
+};
+
+// remove or delete
+export const deleteCart = async () => {
+ return await deleteFetch('api/cart/1');
+};
+
+/**
+ *
+ * patch와 put은 edit을 접두어로 붙힌다. 어색하다면 바꾼다.
+ */
+export const editOption = async () => {
+ return await patchFetch('api/cart/1/213123');
+};
diff --git a/frontend/src/hooks/query/useExample.tsx b/frontend/src/hooks/query/useExample.tsx
new file mode 100644
index 000000000..804f042aa
--- /dev/null
+++ b/frontend/src/hooks/query/useExample.tsx
@@ -0,0 +1,19 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { Cart, getCartList } from '@api/example';
+
+const convertCartList = (cartList: { id: number; text: string }[]) => {
+ return cartList.map(cart => ({ id: cart.id, content: cart.text }));
+};
+
+export const useExample = () => {
+ const { data, error, isLoading } = useQuery(['carts'], getCartList, {
+ onSuccess: data => {
+ const updatedData = convertCartList(data);
+
+ return updatedData;
+ },
+ });
+
+ return { data, error, isLoading };
+};
diff --git a/frontend/src/utils/fetch.ts b/frontend/src/utils/fetch.ts
new file mode 100644
index 000000000..d6f05ff8f
--- /dev/null
+++ b/frontend/src/utils/fetch.ts
@@ -0,0 +1,77 @@
+const headers = {
+ 'Content-Type': 'application/json;charset=utf-8',
+ Authorization: `Bearer `,
+};
+
+export const getFetch = async (url: string): Promise => {
+ const response = await fetch(url, {
+ method: 'GET',
+ headers,
+ });
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.message);
+ }
+
+ return data;
+};
+
+export const postFetch = async (url: string, body: T): Promise => {
+ const response = await fetch(url, {
+ method: 'POST',
+ body: JSON.stringify(body),
+ headers,
+ });
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.message);
+ }
+
+ return data;
+};
+
+export const putFetch = async (url: string, body: T): Promise => {
+ const response = await fetch(url, {
+ method: 'PUT',
+ body: JSON.stringify(body),
+ headers,
+ });
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.message);
+ }
+
+ return data;
+};
+
+export const patchFetch = async (url: string) => {
+ const response = await fetch(url, {
+ method: 'PATCH',
+ headers,
+ });
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.message);
+ }
+};
+
+export const deleteFetch = async (url: string) => {
+ const response = await fetch(url, {
+ method: 'DELETE',
+ headers,
+ });
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.message);
+ }
+};
From 1f36fed2c17fd496e5327945b093bb7d0109272c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EA=B9=80=EC=98=81=EA=B8=B8/KIM=20YOUNG=20GIL?=
<80146176+Gilpop8663@users.noreply.github.com>
Date: Fri, 14 Jul 2023 18:10:23 +0900
Subject: [PATCH 4/4] =?UTF-8?q?=ED=88=AC=ED=91=9C=20=EC=9E=91=EC=84=B1=20?=
=?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EC=9D=98=20=ED=88=AC=ED=91=9C=20?=
=?UTF-8?q?=EC=84=A0=ED=83=9D=EC=A7=80=20=EC=BB=B4=ED=8F=AC=EB=84=8C?=
=?UTF-8?q?=ED=8A=B8=20UI=20=EA=B5=AC=ED=98=84=20=20(#40)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: (#20) 삭제, 파일 업로드 버튼 컴포넌트 UI 구현
* feat: (#20) 이미지 업로드 버튼을 눌렀을 때 이미지 업로드 창이 나오도록 구현 및 파일명 변경
* feat: (#20) 투표 선택지 아이템 컴포넌트 UI 구현
* feat: (#20) 투표 선택지 작성 리스트 컴포넌트 UI 구현
* feat: (#20) 훅 테스트 코드 작성 시작
* test: (#20) 투표 선택지 작성에 사용하는 훅 테스트 작성
* feat: (#20) 투표 선택지 작성 훅 구현
* feat: (#20) 투표 선택지 작성 훅 적용 및 UI 구현
* feat: (#20) 50자 이상 적었을 때 사용자에게 안내 기능 구현
* feat: (#20) 사진의 이미지가 5MB가 넘어갈 경우 유저에게 안내하도록 구현
* design: (#20): 삭제 버튼을 감싼 태그가 항상 왼쪽의 공간을 차지하도록 CSS 변경
* refactor: (#20) svg 코드를 assets 폴더로 이동 후 import 하여 사용하도록 수정
회색 버튼을 cssText로 관리하여 공통으로 관리하도록 수정
* refactor: (#20) 코드 가독성을 위한 함수명, 변수명 수정
* design: (#20) 화면 크기에 따라 폰트, 버튼 사이즈 변경되도록 구현
* style: (#20) CSS 속성 순서 변경 및 불필요한 타입 선언 제거
* chore: (#20) 함수 동작 과정에 대한 설명 주석 추가
* chore: (#20) 테스트 문구 변경
---
.../__test__/hooks/useWritingOption.test.tsx | 132 ++++++++++++++++++
frontend/src/assets/photo_white.svg | 3 +
frontend/src/assets/x_mark_white.svg | 3 +
.../OptionCancelButton.stories.tsx | 14 ++
.../OptionCancelButton/index.tsx | 15 ++
.../OptionCancelButton/style.ts | 9 ++
.../OptionUploadImageButton.stories.tsx | 14 ++
.../OptionUploadImageButton/index.tsx | 25 ++++
.../OptionUploadImageButton/style.ts | 19 +++
.../WritingVoteOption.stories.tsx | 53 +++++++
.../WritingVoteOption/index.tsx | 71 ++++++++++
.../WritingVoteOption/style.ts | 100 +++++++++++++
.../WritingVoteOptionList.stories.tsx | 66 +++++++++
.../WritingVoteOptionList/index.tsx | 46 ++++++
.../optionList/WritingVoteOptionList/style.ts | 16 +++
frontend/src/hooks/useWritingOption.tsx | 88 ++++++++++++
16 files changed, 674 insertions(+)
create mode 100644 frontend/__test__/hooks/useWritingOption.test.tsx
create mode 100644 frontend/src/assets/photo_white.svg
create mode 100644 frontend/src/assets/x_mark_white.svg
create mode 100644 frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionCancelButton/OptionCancelButton.stories.tsx
create mode 100644 frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionCancelButton/index.tsx
create mode 100644 frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionCancelButton/style.ts
create mode 100644 frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/OptionUploadImageButton.stories.tsx
create mode 100644 frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/index.tsx
create mode 100644 frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/style.ts
create mode 100644 frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/WritingVoteOption.stories.tsx
create mode 100644 frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/index.tsx
create mode 100644 frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/style.ts
create mode 100644 frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOptionList.stories.tsx
create mode 100644 frontend/src/components/optionList/WritingVoteOptionList/index.tsx
create mode 100644 frontend/src/components/optionList/WritingVoteOptionList/style.ts
create mode 100644 frontend/src/hooks/useWritingOption.tsx
diff --git a/frontend/__test__/hooks/useWritingOption.test.tsx b/frontend/__test__/hooks/useWritingOption.test.tsx
new file mode 100644
index 000000000..8af754a1b
--- /dev/null
+++ b/frontend/__test__/hooks/useWritingOption.test.tsx
@@ -0,0 +1,132 @@
+import { renderHook, act } from '@testing-library/react';
+
+import { useWritingOption } from '../../src/hooks/useWritingOption';
+
+/**
+ * @jest-environment jsdom
+ */
+
+const MOCK_MAX_VOTE_OPTION = [
+ { id: 12341, text: '', imageUrl: '' },
+ { id: 123451, text: '', imageUrl: '' },
+ {
+ id: 1234221,
+ text: '방학 때 강릉으로 강아지와 기차여행을 하려했지만 장마가 와서 취소했어요. 여행을 별로 좋',
+ imageUrl: '',
+ },
+ {
+ id: 12342261,
+ text: '방학 때 강릉으로 강아지와 기차여행을 하려했지만 장마가 와서 취소했어요. 여행을 별로 좋',
+ imageUrl: 'https://source.unsplash.com/random',
+ },
+ {
+ id: 1234451,
+ text: '',
+ imageUrl: 'https://source.unsplash.com/random',
+ },
+];
+
+const MOCK_MIN_VOTE_OPTION = [
+ { id: 12341, text: '', imageUrl: '' },
+ { id: 123341, text: '', imageUrl: '' },
+];
+describe('useWritingOption 훅을 테스트 한다.', () => {
+ test('초기 값이 없다면 기본으로 2개의 선택지가 존재해야 한다.', () => {
+ const { result } = renderHook(() => useWritingOption());
+
+ const { optionList } = result.current;
+
+ expect(optionList.length).toBe(2);
+
+ expect(optionList[0].text).toBe('');
+
+ expect(optionList[0].imageUrl).toBe('');
+ });
+
+ test('기존 데이터가 있는 경우 기존 데이터가 선택지의 초기 값으로 존재해야 한다.', () => {
+ const { result } = renderHook(() => useWritingOption(MOCK_MIN_VOTE_OPTION));
+
+ const { optionList } = result.current;
+
+ expect(optionList).toEqual(MOCK_MIN_VOTE_OPTION);
+ });
+
+ test('투표 선택지를 추가할 수 있어야 한다. 생성된 선택지는 text와 imageUrl 값을 가지고 있다.', () => {
+ const { result } = renderHook(() => useWritingOption(MOCK_MIN_VOTE_OPTION));
+
+ const { addOption } = result.current;
+
+ act(() => {
+ addOption();
+ });
+
+ const { optionList } = result.current;
+
+ expect(optionList.length).toBe(MOCK_MIN_VOTE_OPTION.length + 1);
+
+ expect(optionList[2].text).toBe('');
+
+ expect(optionList[2].imageUrl).toBe('');
+ });
+
+ test('투표 선택지가 5개일 땐 투표 선택지를 추가할 수 없다', () => {
+ const { result } = renderHook(() => useWritingOption(MOCK_MAX_VOTE_OPTION));
+
+ const { addOption } = result.current;
+
+ act(() => {
+ addOption();
+ });
+
+ const { optionList } = result.current;
+
+ expect(optionList).toEqual(MOCK_MAX_VOTE_OPTION);
+ });
+
+ test('투표 선택지가 3개 이상일때는 투표 선택지의 아이디를 이용하여 삭제할 수 있다.', () => {
+ const { result } = renderHook(() => useWritingOption(MOCK_MAX_VOTE_OPTION));
+
+ const { deleteOption } = result.current;
+
+ act(() => {
+ deleteOption(MOCK_MAX_VOTE_OPTION[0].id);
+ });
+
+ const { optionList } = result.current;
+
+ expect(optionList).toEqual(MOCK_MAX_VOTE_OPTION.slice(1, 5));
+ });
+
+ test('투표 선택지가 2개일때는 삭제할 수 없다.', () => {
+ const { result } = renderHook(() => useWritingOption(MOCK_MIN_VOTE_OPTION));
+
+ const { deleteOption } = result.current;
+
+ act(() => {
+ deleteOption(MOCK_MIN_VOTE_OPTION[0].id);
+ });
+
+ const { optionList } = result.current;
+
+ expect(optionList).toEqual(MOCK_MIN_VOTE_OPTION);
+ });
+
+ test('선택한 이미지가 있을 때 취소할 수 있다.', () => {
+ const MOCK_IMAGE_OPTION = [
+ { id: 12341, text: '', imageUrl: 'https' },
+ { id: 123412, text: '', imageUrl: 'imageUrl' },
+ ];
+
+ const { result } = renderHook(() => useWritingOption(MOCK_IMAGE_OPTION));
+
+ const { removeImage } = result.current;
+
+ act(() => {
+ removeImage(MOCK_MIN_VOTE_OPTION[0].id);
+ });
+
+ const { optionList } = result.current;
+
+ expect(optionList[0].imageUrl).toBe('');
+ });
+});
diff --git a/frontend/src/assets/photo_white.svg b/frontend/src/assets/photo_white.svg
new file mode 100644
index 000000000..330feedd6
--- /dev/null
+++ b/frontend/src/assets/photo_white.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/src/assets/x_mark_white.svg b/frontend/src/assets/x_mark_white.svg
new file mode 100644
index 000000000..11b81b1ea
--- /dev/null
+++ b/frontend/src/assets/x_mark_white.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionCancelButton/OptionCancelButton.stories.tsx b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionCancelButton/OptionCancelButton.stories.tsx
new file mode 100644
index 000000000..e6444c985
--- /dev/null
+++ b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionCancelButton/OptionCancelButton.stories.tsx
@@ -0,0 +1,14 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import OptionCancelButton from '.';
+
+const meta: Meta = {
+ component: OptionCancelButton,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ render: () => ,
+};
diff --git a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionCancelButton/index.tsx b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionCancelButton/index.tsx
new file mode 100644
index 000000000..a28be23b6
--- /dev/null
+++ b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionCancelButton/index.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+
+import xMarkIcon from '@assets/x_mark_white.svg';
+
+import * as S from './style';
+
+interface OptionCancelButtonProps extends React.ButtonHTMLAttributes {}
+
+export default function OptionCancelButton({ ...rest }: OptionCancelButtonProps) {
+ return (
+
+
+
+ );
+}
diff --git a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionCancelButton/style.ts b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionCancelButton/style.ts
new file mode 100644
index 000000000..e241c1217
--- /dev/null
+++ b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionCancelButton/style.ts
@@ -0,0 +1,9 @@
+import { styled } from 'styled-components';
+
+import { ButtonCssText, IconImage } from '../style';
+
+export const Container = styled.button`
+ ${ButtonCssText}
+`;
+
+export const Image = styled(IconImage)``;
diff --git a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/OptionUploadImageButton.stories.tsx b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/OptionUploadImageButton.stories.tsx
new file mode 100644
index 000000000..3d8982234
--- /dev/null
+++ b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/OptionUploadImageButton.stories.tsx
@@ -0,0 +1,14 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import OptionUploadImageButton from '.';
+
+const meta: Meta = {
+ component: OptionUploadImageButton,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ render: () => ,
+};
diff --git a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/index.tsx b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/index.tsx
new file mode 100644
index 000000000..8e276b236
--- /dev/null
+++ b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/index.tsx
@@ -0,0 +1,25 @@
+import React from 'react';
+
+import photoIcon from '@assets/photo_white.svg';
+
+import * as S from './style';
+
+interface OptionUploadImageButtonProps extends React.InputHTMLAttributes {
+ optionId: number;
+}
+
+export default function OptionUploadImageButton({
+ optionId,
+ ...rest
+}: OptionUploadImageButtonProps) {
+ const id = optionId.toString();
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/style.ts b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/style.ts
new file mode 100644
index 000000000..a8083d4aa
--- /dev/null
+++ b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/OptionUploadImageButton/style.ts
@@ -0,0 +1,19 @@
+import { styled } from 'styled-components';
+
+import { ButtonCssText, IconImage } from '../style';
+
+export const Container = styled.div`
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+`;
+
+export const Label = styled.label`
+ ${ButtonCssText}
+`;
+
+export const FileInput = styled.input`
+ visibility: hidden;
+`;
+
+export const Image = styled(IconImage)``;
diff --git a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/WritingVoteOption.stories.tsx b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/WritingVoteOption.stories.tsx
new file mode 100644
index 000000000..b3fb3684d
--- /dev/null
+++ b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/WritingVoteOption.stories.tsx
@@ -0,0 +1,53 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import WritingVoteOption from '.';
+
+const meta: Meta = {
+ component: WritingVoteOption,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const IsDeletable: Story = {
+ render: () => (
+ {}}
+ handleRemoveImageClick={() => {}}
+ handleUploadImage={() => {}}
+ optionId={Math.floor(Math.random() * 100000)}
+ text="방학 때 강릉으로 강아지와 기차여행을 하려했지
+ 만 장마가 와서 취소했어요. 여행을 별로 좋"
+ isDeletable={true}
+ />
+ ),
+};
+
+export const IsNotDeletable: Story = {
+ render: () => (
+ {}}
+ handleRemoveImageClick={() => {}}
+ handleUploadImage={() => {}}
+ optionId={Math.floor(Math.random() * 100000)}
+ text="방학 때 강릉으로 강아지와 기차여행을 하려했지
+ 만 장마가 와서 취소했어요. 여행을 별로 좋"
+ isDeletable={false}
+ />
+ ),
+};
+
+export const ShowImage: Story = {
+ render: () => (
+ {}}
+ handleRemoveImageClick={() => {}}
+ handleUploadImage={() => {}}
+ optionId={Math.floor(Math.random() * 100000)}
+ text="방학 때 강릉으로 강아지와 기차여행을 하려했지
+ 만 장마가 와서 취소했어요. 여행을 별로 좋"
+ isDeletable={true}
+ imageUrl="https://source.unsplash.com/random"
+ />
+ ),
+};
diff --git a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/index.tsx b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/index.tsx
new file mode 100644
index 000000000..2e26a1ea3
--- /dev/null
+++ b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/index.tsx
@@ -0,0 +1,71 @@
+import React, { ChangeEvent } from 'react';
+
+import OptionCancelButton from './OptionCancelButton';
+import OptionUploadImageButton from './OptionUploadImageButton';
+import * as S from './style';
+
+interface WritingVoteOptionProps {
+ optionId: number;
+ text: string;
+ isDeletable: boolean;
+ handleDeleteOptionClick: () => void;
+ handleRemoveImageClick: () => void;
+ handleUploadImage: (event: React.ChangeEvent) => void;
+ imageUrl?: string;
+}
+
+const MAX_WRITING_LENGTH = 50;
+
+export default function WritingVoteOption({
+ optionId,
+ text,
+ isDeletable,
+ handleDeleteOptionClick,
+ handleRemoveImageClick,
+ handleUploadImage,
+ imageUrl,
+}: WritingVoteOptionProps) {
+ const handleTextChange = (event: ChangeEvent) => {
+ const { value } = event.target;
+ const standard = value.length;
+
+ if (standard === MAX_WRITING_LENGTH) {
+ event.target.setCustomValidity(`선택지 내용은 ${MAX_WRITING_LENGTH}자까지 입력 가능합니다.`);
+ event.target.reportValidity();
+ return;
+ }
+
+ event.target.setCustomValidity('');
+ };
+
+ return (
+
+
+ {isDeletable && (
+
+ )}
+
+
+
+
+ {!imageUrl && (
+
+ )}
+
+ {imageUrl && (
+
+
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/style.ts b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/style.ts
new file mode 100644
index 000000000..5a061ebd6
--- /dev/null
+++ b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOption/style.ts
@@ -0,0 +1,100 @@
+import { styled } from 'styled-components';
+
+export const Container = styled.li`
+ display: flex;
+ gap: 10px;
+`;
+
+export const OptionContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ width: 100%;
+ padding: 20px;
+ border-radius: 4px;
+
+ background-color: #e6e6e6;
+`;
+
+export const ContentContainer = styled.div`
+ display: flex;
+ justify-content: space-between;
+ gap: 10px;
+
+ width: 100%;
+`;
+
+export const ContentTextArea = styled.textarea`
+ width: 100%;
+ height: 90px;
+ padding: 8px;
+
+ font-size: 1.3rem;
+ line-height: 2.4rem;
+
+ background-color: #e6e6e6;
+
+ resize: none;
+
+ @media (min-width: 960px) {
+ height: 120px;
+
+ font-size: 1.6rem;
+ }
+`;
+
+export const ImageContainer = styled.div`
+ width: 80%;
+ margin-top: 20px;
+
+ position: relative;
+`;
+
+export const Image = styled.img`
+ width: 100%;
+ border-radius: 4px;
+
+ aspect-ratio: 1/1;
+ object-fit: cover;
+`;
+
+export const ImageCancelWrapper = styled.div`
+ position: absolute;
+ top: 10px;
+ right: 10px;
+`;
+
+export const CancelButtonWrapper = styled.div`
+ width: 34px;
+ height: 100%;
+`;
+
+export const ButtonCssText = `
+display: flex;
+justify-content: center;
+align-items: center;
+
+width: 24px;
+height: 24px;
+border-radius: 50%;
+
+background-color: #bebebe;
+
+cursor: pointer;
+
+@media (min-width: 960px) {
+ width:28px;
+ height:28px;
+}
+`;
+
+export const IconImage = styled.img`
+ width: 14px;
+ height: 14px;
+
+ @media (min-width: 960px) {
+ width: 16px;
+ height: 16px;
+ }
+`;
diff --git a/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOptionList.stories.tsx b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOptionList.stories.tsx
new file mode 100644
index 000000000..ead20aaa2
--- /dev/null
+++ b/frontend/src/components/optionList/WritingVoteOptionList/WritingVoteOptionList.stories.tsx
@@ -0,0 +1,66 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { styled } from 'styled-components';
+
+import WritingVoteOptionList from '.';
+
+const meta: Meta = {
+ component: WritingVoteOptionList,
+};
+
+export default meta;
+type Story = StoryObj;
+
+const ListWrapper = styled.div`
+ width: 100%;
+ max-width: 320px;
+`;
+
+const MOCK_MAX_VOTE_OPTION = [
+ { id: 1234123, text: '', imageUrl: '' },
+ { id: 1234177, text: '', imageUrl: '' },
+ {
+ id: 1234221,
+ text: '방학 때 강릉으로 강아지와 기차여행을 하려했지만 장마가 와서 취소했어요. 여행을 별로 좋',
+ imageUrl: '',
+ },
+ {
+ id: 1834221,
+ text: '방학 때 강릉으로 강아지와 기차여행을 하려했지만 장마가 와서 취소했어요. 여행을 별로 좋',
+ imageUrl: 'https://source.unsplash.com/random',
+ },
+ {
+ id: 1234451,
+ text: '',
+ imageUrl: 'https://source.unsplash.com/random',
+ },
+];
+
+const MOCK_MIN_VOTE_OPTION = [
+ { id: 123741, text: '', imageUrl: '' },
+ { id: 123415, text: '', imageUrl: '' },
+];
+
+export const DefaultOptionList: Story = {
+ render: () => (
+
+
+
+ ),
+};
+
+export const MaxCountOptionList: Story = {
+ render: () => (
+
+
+
+ ),
+};
+
+export const MinCountOptionList: Story = {
+ render: () => (
+
+
+
+ ),
+};
diff --git a/frontend/src/components/optionList/WritingVoteOptionList/index.tsx b/frontend/src/components/optionList/WritingVoteOptionList/index.tsx
new file mode 100644
index 000000000..233c80147
--- /dev/null
+++ b/frontend/src/components/optionList/WritingVoteOptionList/index.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+
+import { useWritingOption } from '@hooks/useWritingOption';
+import type { WritingVoteOptionType } from '@hooks/useWritingOption';
+
+import AddButton from '@components/common/AddButton';
+
+import * as S from './style';
+import WritingVoteOption from './WritingVoteOption';
+
+interface WritingVoteOptionListProps {
+ initialOptionList?: WritingVoteOptionType[];
+}
+
+const MINIMUM_COUNT = 2;
+const MAXIMUM_COUNT = 5;
+
+export default function WritingVoteOptionList({ initialOptionList }: WritingVoteOptionListProps) {
+ const { optionList, addOption, deleteOption, removeImage, handleUploadImage } =
+ useWritingOption(initialOptionList);
+ const isDeletable = optionList.length > MINIMUM_COUNT;
+
+ return (
+
+ {optionList.map(optionItem => (
+ deleteOption(optionItem.id)}
+ handleRemoveImageClick={() => removeImage(optionItem.id)}
+ handleUploadImage={(event: React.ChangeEvent) =>
+ handleUploadImage(event, optionItem.id)
+ }
+ imageUrl={optionItem.imageUrl}
+ />
+ ))}
+ {optionList.length < MAXIMUM_COUNT && (
+
+
+
+ )}
+
+ );
+}
diff --git a/frontend/src/components/optionList/WritingVoteOptionList/style.ts b/frontend/src/components/optionList/WritingVoteOptionList/style.ts
new file mode 100644
index 000000000..d3f33342c
--- /dev/null
+++ b/frontend/src/components/optionList/WritingVoteOptionList/style.ts
@@ -0,0 +1,16 @@
+import { styled } from 'styled-components';
+
+export const Container = styled.ul`
+ display: flex;
+ flex-direction: column;
+
+ gap: 20px;
+`;
+
+export const AddButtonWrapper = styled.div`
+ display: flex;
+ justify-content: center;
+
+ position: relative;
+ left: 17px;
+`;
diff --git a/frontend/src/hooks/useWritingOption.tsx b/frontend/src/hooks/useWritingOption.tsx
new file mode 100644
index 000000000..d7d87bb21
--- /dev/null
+++ b/frontend/src/hooks/useWritingOption.tsx
@@ -0,0 +1,88 @@
+import React, { useState } from 'react';
+
+export interface WritingVoteOptionType {
+ id: number;
+ text: string;
+ imageUrl?: string;
+}
+
+const MIN_COUNT = 2;
+const MAX_COUNT = 5;
+
+const MAX_FILE_SIZE = 5000000;
+
+const INIT_OPTION_LIST = [
+ { id: Math.floor(Math.random() * 100000), text: '', imageUrl: '' },
+ { id: Math.floor(Math.random() * 100000), text: '', imageUrl: '' },
+];
+
+export const useWritingOption = (initialOptionList: WritingVoteOptionType[] = INIT_OPTION_LIST) => {
+ const [optionList, setOptionList] = useState(initialOptionList);
+
+ const addOption = () => {
+ if (optionList.length >= MAX_COUNT) return;
+
+ const updatedOptionList = [
+ ...optionList,
+ { id: Math.floor(Math.random() * 100000), text: '', imageUrl: '' },
+ ];
+
+ setOptionList(updatedOptionList);
+ };
+
+ const deleteOption = (optionId: number) => {
+ if (optionList.length <= MIN_COUNT) return;
+
+ const removedOptionList = optionList.filter(optionItem => optionItem.id !== optionId);
+
+ setOptionList(removedOptionList);
+ };
+
+ const removeImage = (optionId: number) => {
+ const updatedOptionList = optionList.map(optionItem => {
+ if (optionItem.id === optionId) {
+ return { ...optionItem, imageUrl: '' };
+ }
+
+ return optionItem;
+ });
+
+ setOptionList(updatedOptionList);
+ };
+
+ const handleUploadImage = (event: React.ChangeEvent, optionId: number) => {
+ const { files } = event.target;
+
+ if (!files) return;
+
+ const file = files[0];
+
+ event.target.setCustomValidity('');
+
+ if (file.size > MAX_FILE_SIZE) {
+ event.target.setCustomValidity('사진의 용량은 5MB 이하만 가능합니다.');
+ event.target.reportValidity();
+
+ return;
+ }
+
+ const reader = new FileReader();
+
+ // readAsDataURL 메서드를 통해 파일을 모두 읽고 나면 reader의 loadend 이벤트에서 이미지 미리보기 결과를 확인할 수 있습니다.
+ reader.readAsDataURL(file);
+
+ reader.onloadend = () => {
+ const updatedOptionList = optionList.map(optionItem => {
+ if (optionItem.id === optionId) {
+ return { ...optionItem, imageUrl: reader.result?.toString() };
+ }
+
+ return optionItem;
+ });
+
+ setOptionList(updatedOptionList);
+ };
+ };
+
+ return { optionList, addOption, deleteOption, removeImage, handleUploadImage };
+};